下图表述了Linux内核的中断处理机制,为了在中断执行时间尽量短和中断处理需完成的工作尽量大之间找到一
个平衡点, Linux将中断处理程序分解为两个半部: 顶半部(Top Half) 和底半部(Bottom Half) 。
顶半部: 用于完成尽量少的比较紧急的功能, 它往往只是简单地读取寄存器中的中断状态, 并在清除中断标志后就
进行“登记中断”的工作。 “登记中断 意味着将底半部处理程序挂到该设备的底半部执行队列中去。 这样, 顶半部
执行的速度就会很快, 从而可以服务更多的中断请求。
底半部: 几乎做了中断处理程序所有的事情, 而且可以被新的中断打断, 这也是底半部和顶半部的最大不同, 因为顶半部往往被设计成不可中断。 底半部相对来说并不是非常紧急的, 而且相对比较耗时, 不在硬件中断服务程序中执行。
Linux实现底半部的机制主要有tasklet、 工作队列、 软中断和线程化irq。
先来个比较:
中断下半部机制 | 上下文 | 是否可被打断 | 是否可睡眠 | 实时性 |
---|---|---|---|---|
tasklet | 运行于软中断上下文 | 可被硬中断打断,不会被定时器中断打断 | 不可睡眠 | 会阻塞应用程序 |
工作队列 | 运行于内核线程上下文 | 可被硬中断、定时器中断打断 | 可睡眠 | 同一个链表中可能挂多个work 所以实时性会降低 |
threaded_irq | 运行于内核线程上下文 | 可被硬中断、定时器中断打断 | 可睡眠 | 对于每个中断都有独立的内核线程 实时性高 |
1. tasklet
tasklet的使用较简单, 它的执行上下文是软中断, 执行时机通常是顶半部返回的时候。 我们只需要定义tasklet及
其处理函数, 并将两者关联则可, 例如:
/* 定义一个处理函数 */
void my_tasklet_func(unsigned long);
/* 定义一个 tasklet 结构 my_tasklet , 与 my_tasklet_func(data) 函数相关联 */
DECLARE_TASKLET(my_tasklet, my_tasklet_func, data);
或
/* 定义一个 tasklet_struct 结构体 */
struct tasklet_struct tasklet;
/* 定义一个处理函数 */
void my_tasklet_func(unsigned long);
/* 关联 tasklet 与 处理函数 */
void tasklet_init(struct tasklet_struct *t,
void (*func)(unsigned long),
unsigned long data)
在需要调度tasklet的时候引用一个tasklet_schedule() 函数就能使系统在适当的时候进行调度运行:
tasklet_schedule(struct tasklet_struct *tasklet);
销毁 tasklet
tasklet_kill(struct tasklet_struct *tasklet);
taskelet 运行于软中断上下文,所以不能睡眠,会阻塞应用程序,可被外部中断打断,不会被内核定时器中断打断
/* 中断下半部处理函数 不能睡眠
可以被外部中断打断,但不会被定时中断打断 */
static void my_tasklet_func(unsigned long arg)
{
int i = 0;
printk("my_tasklet_func gpio = %d\n", (int)arg);
/* 循环期间,APP程序得不到执行 */
while(i < 500){
printk("%d ",i++);
if(i % 20 == 0)
printk("\n");
}
printk("\n");
}
测试结果:
tasklet 可以被按键中断,但不能被定时器中断,也不能睡眠(测试了添加msleep后,加载驱动直接一直输出报错)
tasklet 使用模板
/* 定义 tasklet 和底半部函数并将它们关联 */
void xxx_do_tasklet(unsigned long);
DECLARE_TASKLET(xxx_tasklet, xxx_do_tasklet, 0);
/* 中断处理底半部 */
void xxx_do_tasklet(unsigned long)
{
...
}
/* 中断处理顶半部 */
irqreturn_t xxx_interrupt(int irq, void *dev_id)
{
...
tasklet_schedule(&xxx_tasklet);
...
}
/* 设备驱动模块加载函数 */
int __init xxx_init(void)
{
...
/* 申请中断 */
result = request_irq(xxx_irq, xxx_interrupt,0, "xxx", NULL);
...
return IRQ_HANDLED;
}
/* 设备驱动模块卸载函数 */
void __exit xxx_exit(void)
{
...
/* 释放中断 */
free_irq(xxx_irq, xxx_interrupt);
...
}
2. 工作队列
工作队列的使用方法和tasklet非常相似, 但是工作队列的执行上下文是内核线程, 因此可以调度和睡眠。 下面的
代码用于定义一个工作队列和一个底半部执行函数:
struct work_struct my_wq; /* 定义一个工作队列 */
void my_wq_func(struct work_struct *work); /* 定义一个处理函数 */
通过INIT_WORK() 可以初始化这个工作队列并将工作队列与处理函数绑定:
/* 初始化工作队列并将其与处理函数绑定 */
INIT_WORK(&my_wq, my_wq_func);
与tasklet_schedule() 对应的用于调度工作队列执行的函数为schedule_work() , 如:
/* 调度工作队列执行 */
schedule_work(&my_wq);
假设有两个cpu,当workqueue有新任务时,会将任务放进cpu0和cpu1左侧的work链表中(所有cpu的work链表都放),调度任务时从两个cpu的work_pool中选择一个普通优先级线程放入执行(总共只选一个)
工作队列使用模板
/* 定义工作队列和关联函数 */
struct work_struct xxx_wq;
void xxx_do_work(struct work_struct *work);
/* 中断处理底半部 */
void xxx_do_work(struct work_struct *work)
{
...
}
/* 中断处理顶半部 */
irqreturn_t xxx_interrupt(int irq, void *dev_id)
{
...
schedule_work(&xxx_wq);
...
return IRQ_HANDLED;
}
/* 设备驱动模块加载函数 */
int xxx_init(void)
{
...
/* 申请中断 */
result = request_irq(xxx_irq, xxx_interrupt,0, "xxx", NULL);
...
/* 初始化工作队列 */
INIT_WORK(&xxx_wq, xxx_do_work);
...
}
/* 设备驱动模块卸载函数 */
void xxx_exit(void)
{
...
/* 释放中断 */
free_irq(xxx_irq, xxx_interrupt);
...
}
测试结果:
工作队列可以被按键和定时器中断,也可以睡眠
3. 软中断(Softirq)
- 软中断(Softirq) 也是一种传统的底半部处理机制, 它的执行时机通常是顶半部返回的时候, tasklet是基于软中断实现的, 因此也运行于软中断上下文。在Linux内核中, 用 softirq_action 结构体表征一个软中断, 这个结构体包含软中断处理函数指针和传递给该函数的参数。 使用 open_softirq() 函数可以注册软中断对应的处理函数, 而 raise_softirq() 函数可以触发一个软中断。
- 软中断和tasklet运行于软中断上下文, 仍然属于原子上下文的一种, 而工作队列则运行于进程上下文。 因此, 在
软中断和tasklet处理函数中不允许睡眠, 而在工作队列处理函数中允许睡眠。 - local_bh_disable() 和local_bh_enable() 是内核中用于禁止和使能软中断及tasklet底半部机制的函数。
内核中采用softirq的地方包括HI_SOFTIRQ、 TIMER_SOFTIRQ、 NET_TX_SOFTIRQ、 NET_RX_SOFTIRQ、
SCSI_SOFTIRQ、 TASKLET_SOFTIRQ等, 一般来说, 驱动的编写者不会也不宜直接使用softirq。 - 总结一下硬中断、 软中断和信号的区别: 硬中断是外部设备对CPU的中断, 软中断是中断底半部的一种处理机制, 而信号则是由内核(或其他进程) 对某个进程的中断。
- 在涉及系统调用的场合, 人们也常说通过软中断(例如ARM为swi) 陷入内核, 此时软中断的概念是指由软件指令引发的中断, 和我们这个地方说的softirq是两个完全不同的概念, 一个是software, 一个是soft。
- 需要特别说明的是, 软中断以及基于软中断的tasklet如果在某段时间内大量出现的话, 内核会把后续软中断放入
ksoftirqd内核线程中执行。 总的来说, 中断优先级高于软中断, 软中断又高于任何一个线程。 软中断适度线程
化, 可以缓解高负载情况下系统的响应。
4. threaded_irq(中断线程化)
在内核中, 除了可以通过request_irq() 、 devm_request_irq() 申请中断以外, 还可以通过
request_threaded_irq() 和devm_request_threaded_irq() 申请。 这两个函数的原型为:
int request_threaded_irq(unsigned int irq, irq_handler_t handler, irq_handler_t thread_fn,
unsigned long flags, const char *name, void *dev);
int devm_request_threaded_irq(struct device *dev, unsigned int irq,
irq_handler_t handler, irq_handler_t thread_fn,
unsigned long irqflags, const char *devname,
void *dev_id);
- 由此可见, 它们比request_irq() 、 devm_request_irq() 多了一个参数thread_fn。 用这两个API申请中断的时
候, 内核会为相应的中断号分配一个对应的内核线程。 注意这个线程只针对这个中断号, 如果其他中断也通过
request_threaded_irq() 申请, 自然会得到新的内核线程。 - 参数handler对应的函数执行于中断上下文, thread_fn参数对应的函数则执行于内核线程。 如果handler结束的时候, 返回值是IRQ_WAKE_THREAD, 内核会调度对应线程执行thread_fn对应的函数。
- request_threaded_irq() 和devm_request_threaded_irq() 支持在irqflags中设置IRQF_ONESHOT标记, 这样内核会
自动帮助我们在中断上下文中屏蔽对应的中断号, 而在内核调度thread_fn执行后, 重新使能该中断号。 对于我们
无法在上半部清除中断的情况, IRQF_ONESHOT特别有用, 避免了中断服务程序一退出, 中断就洪泛的情况。 - handler参数可以设置为NULL, 这种情况下, 内核会用默认的irq_default_primary_handler() 代替handler, 并会使
用IRQF_ONESHOT标记。 irq_default_primary_handler() 定义为:
/*
* Default primary interrupt handler for threaded interrupts. Is
* assigned as primary handler when request_threaded_irq is called
* with handler == NULL. Useful for oneshot interrupts.
*/
static irqreturn_t irq_default_primary_handler(int irq, void *dev_id)
{
return IRQ_WAKE_THREAD;
}
threaded_irq 使用案例 (可通过<asm/current.h>头文件的current对象获取线程名和pid)
/* 定义工作队列和关联函数 */
struct work_struct xxx_wq;
void xxx_do_work(struct work_struct *work);
/* 中断线程化:存在于内核线程 */
static irqreturn_t key_threaded_func_handler(int irq, void *dev)
{
int i = 0;
printk("the process is\"%s\"(pid %d)\n",current->comm, current->pid); //打印出线程名和pid
while(i < 5){
printk("%d ",i++);
msleep(500);
}
printk("\n");
return IRQ_HANDLED;
}
/* 中断处理顶半部 */
static irqreturn_t key_irq_handler(int irq, void *dev)
{
struct gpioirq *girq = dev;
...
return IRQ_WAKE_THREAD;
}
/* 设备驱动模块加载函数 */
int xxx_init(void)
{
...
/* 申请中断 */
/* 中断下半部:threaded_irq IRQF_ONESHOT:在上下文中屏蔽对应中断号 */
request_threaded_irq(xxx_irq, key_irq_handler, key_threaded_func_handler,
IRQF_TRIGGER_FALLING, "hc_key_irq", xxx_arg);
...
}
/* 设备驱动模块卸载函数 */
void xxx_exit(void)
{
...
/* 释放中断 */
free_irq(xxx_irq, xxx_arg);
...
}
测试结果:
threaded_irq可以被中断、定时器打断,可以睡眠
PS:扩展一个 container_of 函数,它可以倒推导一个结构体的起始地址