linux中断

中断是指CPU在执行程序执行中,出现了突发事件,CPU必须暂停当前程序的执行,处理突发事件,处理完后又返回程序到中断的位置继续进行。

根据中断入口跳转方法不同,分为向量中断和非向量中断,采用向量中断的CPU通常为不同的中断分配不同的中断号,当检测到某中断到来后,自动跳转到该中断号对应的地址执行。不同中断号的中断有不同的入口地址。非向量中断的多个中断共享一个入口地址,进入该入口地址后,再通过软件判断中断标志来标识,具体哪个中断。

申请和释放中断

request_irq 函数用于申请中断, request_irq函数可能会导致睡眠,因此不能在中断上下文或者其他禁止睡眠的代码段中使用 request_irq 函数。 request_irq 函数会激活(使能)中断,所以不需要我们手动去使能中断。

1
2
3
4
5
int request_irq(unsigned int irq,
irq_handler_t handler,
unsigned long flags,
const char *name,
void *dev)

irq:要申请中断的中断号。

每个中断都有一个中断号,通过中断号即可区分不同的中断 。

handler:中断处理函数,当中断发生以后就会执行此中断处理函数。
flags:中断标志,可以在文件 include/linux/interrupt.h 里面查看所有的中断标志

name:中断名字,设置以后可以在/proc/interrupts 文件中看到对应的中断名字。
dev: 如果将 flags 设置为 IRQF_SHARED 的话, dev 用来区分不同的中断,一般情况下将
dev 设置为设备结构体, dev 会传递给中断处理函数 irq_handler_t 的第二个参数。
返回值: 0 中断申请成功,其他负值 中断申请失败,如果返回-EBUSY 的话表示中断已经被申请了。

标志 描述
IRQF_SHARED 多个设备共享一个中断线,共享的所有中断都必须指定此标志。如果使用共享中断的话, request_irq 函数的 dev 参数就是唯一区分的标志
IRQF_ONESHOT 单次中断,中断执行一次就结束。
IRQF_TRIGGER_NONE 无触发
IRQF_TRIGGER_RISING 上升沿触发
IRQF_TRIGGER_FALLING 下降沿触发
IRQF_TRIGGER_HIGH 高电平触发
IRQF_TRIGGER_LOW 低电平触发

free_irq 函数

通过 free_irq 函数释放掉相应的中断。 如果中断不是共享的,那么 free_irq 会删除中断处理函数并且禁止中断。

1
void free_irq(unsigned int irq, void *dev)

第一个参数是要中断处理函数要相应的中断号。第二个参数是一个指向 void 的指针,也就是个通用指针,需要与 request_irq 函数的 dev 参数保持一致。用于区分共享中断的不同设备,dev 也可以指向设备数据结构。

中断处理函数的返回值为 irqreturn_t 类型, irqreturn_t 类型定义如下所示:

1
2
3
4
5
6
enum irqreturn {
IRQ_NONE = (0 << 0),
IRQ_HANDLED = (1 << 0),
IRQ_WAKE_THREAD = (1 << 1),
};
typedef enum irqreturn irqreturn_t;

可以看出 irqreturn_t 是个枚举类型,一共有三种返回值。一般中断服务函数返回值使用如下形式:

1
return IRQ_RETVAL(IRQ_HANDLED) 

中断使能与禁止函数

1
2
3
4
5
6
7
void enable_irq(unsigned int irq)//使能中断irq为中断号
void disable_irq(unsigned int irq)//禁止中断
void disable_irq_nosync(unsigned int irq)//需要保证不会产生新的中断,并且确保所有已经开始执行的中断处理程序已经全部退出,调用以后立即返回,不会等待当前中断处理程序执行完毕
local_irq_enable()//使能当前处理器中断系统
local_irq_disable()//禁止当前中断系统
local_irq_save(flags)//用于禁止中断,并且将中断状态保存在 flags 中
local_irq_restore(flags)//用于恢复中断,将中断到 flags 状态

中断上下部

中断处理过程就分为了两部分:
上半部:上半部就是中断处理函数,那些处理过程比较快,不会占用很长时间的处理就可以放在上半部完成。
下半部:如果中断处理过程比较耗时,那么就将这些比较耗时的代码提出来,交给下半部去执行,这样中断处理函数就会快进快出。

对于网卡的中断处理,上半部会执行通知硬件、拷贝网络数据报到内存并继续读取新数据包,这些重要、紧急且与硬件相关的工作,因为网卡接收的网络数据包的缓存大小通常是固定的、有限的,一旦被延迟可能造成缓存溢出。而数据包的处理等操作,则由下半部来完成。

Linux 内核将中断分为上半部和下半部的主要目的就是实现中断处理函数的快进快出,那些对时间敏感、执行速度快的操作可以放到中断处理函数中,也就是上半部。剩下的所有工作都可以放到下半部去执行,比如在上半部将数据拷贝到内存中,关于数据的具体处理就可以放到下半部去执行。至于哪些代码属于上半部,哪些代码属于下半部并没有明确的规定 。

  1. 如果要处理的内容不希望被其他中断打断,那么可以放到上半部。
  2. 如果要处理的任务对时间敏感,可以放到上半部。
  3. 如果要处理的任务与硬件有关,可以放到上半部
  4. 除了上述三点以外的其他任务,优先考虑放到下半部。

软中断

2.5版本以前使用BH(bottom half)实现下半部分,之后都是使用软中断核tasklet代替BH机制。Linux内核使用结构体softirq_action表示软中断。

1
2
3
4
struct softirq_action
{
void (*action)(struct softirq_action *);
};

在kernel/softirq.c文件中一共定义了10个软中断

1
static struct softirq_action softirq_vec[NR_SOFTIRQS];

NR_SOFTIRQS是枚举类型,定义在文件include/linux/interrupt.h中

1
2
3
4
5
6
7
8
9
10
11
12
13
14
enum
{
HI_SOFTIRQ=0, /* 高优先级软中断 */
TIMER_SOFTIRQ, /* 定时器软中断 */
NET_TX_SOFTIRQ, /* 网络数据发送软中断 */
NET_RX_SOFTIRQ, /* 网络数据接收软中断 */
BLOCK_SOFTIRQ,
BLOCK_IOPOLL_SOFTIRQ,
TASKLET_SOFTIRQ, /* tasklet 软中断 */
SCHED_SOFTIRQ, /* 调度软中断 */
HRTIMER_SOFTIRQ, /* 高精度定时器软中断 */
RCU_SOFTIRQ, /* RCU 软中断 */
NR_SOFTIRQS
};

softirq_action 结构体中的 action 成员变量就是软中断的服务函数,数组 softirq_vec 是个全局数组,因此所有的 CPU(对于 SMP 系统而言)都可以访问到,每个 CPU 都有自己的触发和控制机制,并且只执行自己所触发的软中断。但是各个 CPU 所执行的软中断服务函数确是相同的,都是数组 softirq_vec 中定义的 action 函数。

要使用软中断,必须先使用 open_softirq 函数注册对应的软中断处理函数

1
2
3
4
void open_softirq(int nr, void (*action)(struct softirq_action *))
//nr:要开启的软中断,
//action:软中断对应的处理函数。
//返回值: 没有返回值。

注册好软中断以后需要通过 raise_softirq 函数触发

1
2
3
void raise_softirq(unsigned int nr)
//nr:要触发的软中断
//返回值: 没有返回值。

软中断必须在编译的时候静态注册! Linux 内核使用 softirq_init 函数初始化软中断,softirq_init 函数定义在 kernel/softirq.c 文件里面,

1
2
3
4
5
6
7
8
9
10
11
12
void __init softirq_init(void)
{
int cpu;

for_each_possible_cpu(cpu) {
per_cpu(tasklet_vec, cpu).tail = &per_cpu(tasklet_vec, cpu).head;
per_cpu(tasklet_hi_vec, cpu).tail = &per_cpu(tasklet_hi_vec, cpu).head;
}

open_softirq(TASKLET_SOFTIRQ, tasklet_action);
open_softirq(HI_SOFTIRQ, tasklet_hi_action);
}

tasklet 是利用软中断来实现的另外一种下半部机制

1
2
3
4
5
6
7
8
9
10
11
12
13
struct tasklet_struct//结构体
{
struct tasklet_struct *next; /* 下一个 tasklet */
unsigned long state; /* tasklet 状态 */
atomic_t count; /* 计数器,记录对 tasklet 的引用数 */
void (*func)(unsigned long); /* tasklet 执行的函数 */
unsigned long data; /* 函数 func 的参数 */
};
void tasklet_init(struct tasklet_struct *t, void (*func)(unsigned long), unsigned long data)
//t:要初始化的 tasklet
//func: tasklet 的处理函数。
//data: 要传递给 func 函数的参数
//返回值: 没有返回值。

在上半部,也就是中断处理函数中调用 tasklet_schedule 函数就能使 tasklet 在合适的时间运行

1
2
void tasklet_schedule(struct tasklet_struct *t)
//t:要调度的 tasklet,也就是 DECLARE_TASKLET 宏里面的 name。

tasklet的使用

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
/* 定义 taselet */
struct tasklet_struct testtasklet;
/* tasklet 处理函数 */
void testtasklet_func(unsigned long data)
{
/* tasklet 具体处理内容 */
}
/* 中断处理函数 */
irqreturn_t test_handler(int irq, void *dev_id)
{
......
/* 调度 tasklet */
tasklet_schedule(&testtasklet);
......
}
/* 驱动入口函数 */
static int __init xxxx_init(void)
{
......
/* 初始化 tasklet */
tasklet_init(&testtasklet, testtasklet_func, data);
/* 注册中断处理函数 */
request_irq(xxx_irq, test_handler, 0, "xxx", &xxx_dev);
......
}

工作队列

工作队列是另外一种下半部执行方式,工作队列在进程上下文执行,工作队列将要推后的工作交给一个内核线程去执行,因为工作队列工作在进程上下文,因此工作队列允许睡眠或重新调度。因此如果你要推后的工作可以睡眠那么就可以选择工作队列,否则的话就只能选择软中断或 tasklet。

工作队列结构体workqueue_struct结构体

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
struct workqueue_struct {
struct list_head pwqs;
struct list_head list;
struct mutex mutex;
int work_color;
int flush_color;
atomic_t nr_pwqs_to_flush;
struct wq_flusher *first_flusher;
struct list_head flusher_queue;
struct list_head flusher_overflow;
struct list_head maydays;
struct worker *rescuer;
int nr_drainers;
int saved_max_active;
struct workqueue_attrs *unbound_attrs;
struct pool_workqueue *dfl_pwq;
char name[WQ_NAME_LEN];
struct rcu_head rcu;
unsigned int flags ____cacheline_aligned;
struct pool_workqueue __percpu *cpu_pwqs;
struct pool_workqueue __rcu *numa_pwq_tbl[];
};

内核使用工作者线程(worker thread)来处理工作队列的各个工作,Linux使用worker结构体表示工作者线程。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
struct worker {
union {
struct list_head entry;
struct hlist_node hentry;
};
struct work_struct *current_work;
work_func_t current_func;
struct pool_workqueue *current_pwq;
bool desc_valid;
struct list_head scheduled;
struct task_struct *task;
struct worker_pool *pool;
struct list_head node;
unsigned long last_active;
unsigned int flags;
int id;
char desc[WORKER_DESC_LEN];
struct workqueue_struct *rescue_wq;//工作队列
};

简单创建工作很简单,直接定义一个 work_struct 结构体变量即可,然后使用 INIT_WORK 宏来初始化工作, INIT_WORK 宏定义如下:

1
2
#define INIT_WORK(_work, _func)
//work 表示要初始化的工作, _func 是工作对应的处理函数。

也可以使用 DECLARE_WORK 宏一次性完成工作的创建和初始化,宏定义如下:

1
2
#define DECLARE_WORK(n, f)
//n 表示定义的工作(work_struct), f 表示工作对应的处理函数。

和 tasklet 一样,工作也是需要调度才能运行的,工作的调度函数为 schedule_work,函数原型如下所示:

1
2
3
bool schedule_work(struct work_struct *work)
//work: 要调度的工作。
//返回值: 0 成功,其他值 失败。

工作队列的使用

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
/* 定义工作(work) */
struct work_struct testwork;
/* work 处理函数 */
void testwork_func_t(struct work_struct *work);
{
/* work 具体处理内容 */
}
/* 中断处理函数 */
irqreturn_t test_handler(int irq, void *dev_id)
{
......
/* 调度 work */
schedule_work(&testwork);
......
}
/* 驱动入口函数 */
static int __init xxxx_init(void)
{
......
/* 初始化 work */
INIT_WORK(&testwork, testwork_func_t);
/* 注册中断处理函数 */
request_irq(xxx_irq, test_handler, 0, "xxx", &xxx_dev);
......
}

补充杂项

volatile关键字

Volatile意思是“易变的”,直接从变量地址中读取数据。“易变”是因为外在因素引起的,像多线程,中断等。

volatile提醒编译器它后面所定义的变量随时都有可能改变,因此编译后的程序每次需要存储或读取这个变量的时候,告诉编译器对该变量不做优化,都会直接从变量内存地址中读取数据,从而可以提供对特殊地址的稳定访问。

如果没有volatile关键字,则编译器可能优化读取和存储,可能暂时使用寄存器中的值,如果这个变量由别的程序更新了的话,将出现不一致的现象。(简洁的说就是:volatile关键词影响编译器编译的结果,用volatile声明的变量表示该变量随时可能发生变化,与该变量有关的运算,不要进行编译优化,以免出错)

使用场景

  1. 并行设备的硬件寄存器(如:状态寄存器) 存储器映射的硬件寄存器通常也要加 voliate,因为每次对它的读写都可能有不同意义。
  2. 中断服务程序中修改的供其它程序检测的变量,需要加volatile;
  3. 多任务环境下各个任务间的共享标志,应该加volatile
  4. 存储器映射的硬件寄存器通常叶家volatile说明,因为每次对它的读写都可能有不同的意义。
volatile 问题和总结
  1. 一个参数既可以是const还可以是volatile吗?

    可以的,例如只读的状态寄存器。它是volatile因为它可能被意想不到地改变。它是const因为程序不应该试图去修改它。

  2. 一个指针可以是volatile吗?

    可以,当一个中服务子程序修改一个指向buffer的指针时。

volatile long可以保证其可见性。(多线程安全三大特性:可见性,原子性,有序性)

通常来说,Volatile这个关键字的作用仅保证数据的可见性,并不保证原子性,而对于double和long的读写,Volatile还额外保证了读写的原子性。Volatile对于double、long的读写原子性仅指单次读或单次写,如果一个操作同时具有读写。

原子性:操作是不可分的。其表现在于对于共享变量的某些操作,应该是不可分的,必须连续完成。

可见性:可见性是指一个线程对共享变量的修改,另外一个线程能够立刻看到;

有序性:指程序在执行的时候,程序的代码执行顺序和语句的顺序是一致的。

那为什么会出现不一致的情况呢?
这是由于重排序的缘故。

在执行程序时,为了提高性能,编译器和处理器常常会对指令做重排序;重排序不会影响单线程的执行结果,但是在并发情况下,可能会出现诡异的BUG。