linux实现底半部的机制主要有tasklet、workqueue、softirq。
1.tasklet
tasklet的使用较为简单,它的执行上下文是软中断,所以在tasklet中不能睡眠,它的执行时机通常是中断顶半部返回的时候。我们只需要定义tasklet及其处理函数,并将两者关联起来即可,例如:
1 void my_tasklet_func(unsigned long); /* 定义一个处理函数 */ 2 DECLARE_TASKLET(my_tasklet, my_tasklet_func, data); /* 定义一个tasklet结构my_tasklet,与my_tasklet_func(data)函数相关联 */
代码 DECLEARE_TASKLET(my_tasklet, my_tasklet_func, data)实现了定义名称为my_tasklet的tasklet,并将其与my_tasklet_func()这个函数绑定,而传入这个函数的参数为data。在需要调度tasklet的时候引用一个tasklet_schedule()函数就能使系统在适当的时候进行调度运行:
tasklet_schedule(&my_tasklet);
使用tasklet作为底半部处理中断的设备驱动程序模板如下:
![]()
struct xxx_struct {
int xxx_irq;
...
...
};
static unsigned long data;
/* 定义 tasklet 和底半部函数并将它们关联 */
DECLARE_TASKLET(xxx_tasklet, xxx_do_tasklet, (unsigned long)&data);
/* 中断处理底半部 */
void xxx_do_tasklet(unsigned long data)
{
struct xxx_struct *pdata = (void *)*(unsigned long *)data;
...
}
/* 中断处理顶半部 */
irqreturn_t xxx_interrupt(int irq, void *dev_id)
{
...
data = (unsigned long)dev_id;
tasklet_schedule(&xxx_tasklet);
...
}
/* 设备驱动模块探测函数 */
static int xxx_probe(struct platform_device *pdev)
{
...
struct xxx_struct *pdata = devm_kzalloc(&pdev->dev, sizeof(struct xxx_struct), GFP_KERNEL);
if (!pdata) {
dev_err(&pdev->dev, "Out of memory\n");
return -ENOMEM;
}
platform_set_drvdata(pdev, pdata);
/* 申请中断 */
result = request_irq(pdata->xxx_irq, xxx_interrupt, 0, "xxx", pdata);
...
return IRQ_HANDLED;
}
/* 设备驱动模块remove函数 */
static int xxx_remove(struct platforn_device *pdev)
{
struct xxx_struct *pdata = platform_get_drvdata(pdev);
...
/* 释放中断 */
free_irq(pdata->xxx_irq, NULL);
...
}
![]()
上述程序在模块加载函数中申请中断,并在模块卸载函数中释放它。对应于xxx_irq的中断处理程序被设置为xxx_interrupt()函数,在这个函数中,tasklet_schedule(&xxx_tasklet)调度被定义的tasklet函数xxx_do_tasklet()在适当的时候执行。
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); /* 调度工作队列执行 */
使用工作队列处理中断底半部的设备驱动程序模板代码如下:
![]()
struct xxx_struct {
int xxx_irq;
struct work_struct xxx_wq; /* 定义工作队列 */
...
...
};
/* 中断处理底半部 */
void xxx_do_work(struct work_struct *work)
{
struct xxx_struct *pdata = container_of(work, struct xxx_struct, xxx_wq);
...
}
/* 中断处理顶半部 */
irqreturn_t xxx_interrupt(int irq, void *dev_id)
{
struct xxx_struct *pdata = (struct xxx_struct *)dev_id;
...
schedule_work(&pdata->xxx_wq);
...
return IRQ_HANDLED;
}
/* 设备驱动模块探测函数 */
int xxx_probe(struct platform_device *pdev)
{
...
struct xxx_struct *pdata = devm_kzalloc(&pdev->dev, sizeof(struct xxx_struct ), GFP_KERNEL);
/*申请中断*/
result = request_irq(pdata->xxx_irq, xxx_interrupt, 0, "xxx", pdata);
...
/* 初始化工作队列 */
INIT_WORK(&pdata->xxx_wq, xxx_do_work);
platform_set_drvdata(pdev, pdata);
...
}
/* 设备驱动模块remove函数 */
int xxx_remove(struct platform_device *pdev)
{
struct xxx_struct *pdata = platform_get_drvdata(pdev);
...
cancel_work_sync(&pdata->xxx_wq);
/* 释放中断 */
free_irq(pdata->xxx_irq, NULL);
...
}
![]()
工作队列早期的实现是在每个CPU核上创建一个worker内核线程,所有在这个核上调度的工作都在该worker线程中执行,其并发性显然差强人意。在linux 2.6.36以后,转而实现了 “Concurrency-managed workqueues”,简称"cmwq",cmwq会自动维护工作队列的线程池以提高并发性,同时保持了API的向后兼容。
延时工作队列
1. 数据结构delayed_work用于处理延迟执行
![]()
struct delayed_work {
struct work_struct work;
struct timer_list timer;
/* target workqueue and CPU ->timer uses to queue ->work */
struct workqueue_struct *wq;
int cpu;
};
![]()
2.在工作队列中被调用的函数原形如下
typedef void (*work_func_t)(struct work_struct *work);
3. 初始化数据结构
INIT_DELAYED_WORK(struct delayed_work *work, work_func_t func)
4. 提交延时任务到工作队列
int schedule_delayed_work(struct delayed_work *work, unsigned long delay);
5.删除提交到工作队列的任务
int cancel_delayed_work(strcut delayed_work *work);
6. 刷新默认工作队列(常跟cancle_delayed_work一起使用)
void flush_schedlue_work(void); 或者 int cancel_delayed_work_sync(strcut delayed_work *work);
使用延时工作队列处理中断底半部的设备驱动程序模板代码如下:
![]()
struct xxx_struct {
int xxx_irq;
struct delayed_work xxx_dwork; /* 定义延时工作队列 */
...
...
};
/* 中断处理底半部 */
void xxx_do_work(struct work_struct *work)
{
struct xxx_struct *pdata = container_of(work, struct xxx_struct, xxx_dwork.work);
...
}
/* 中断处理顶半部 */
irqreturn_t xxx_interrupt(int irq, void *dev_id)
{
struct xxx_struct *pdata = (struct xxx_struct *)dev_id;
...
/*
* delay 2000ms
*/
schedule_delayed_work(&pdata->xxx_dwork, msesc_to_jiffies(2000));
...
return IRQ_HANDLED;
}
/* 设备驱动模块探测函数 */
int xxx_probe(struct platform_device *pdev)
{
...
struct xxx_struct *pdata = devm_kzalloc(&pdev->dev, sizeof(struct xxx_struct ), GFP_KERNEL);
/*申请中断*/
result = request_irq(pdata->xxx_irq, xxx_interrupt, 0, "xxx", pdata);
...
/* 初始化工作队列 */
INIT_DELAYED_WORK(&pdata->xxx_dwork, xxx_do_work);
platform_set_drvdata(pdev, pdata);
...
}
/* 设备驱动模块remove函数 */
int xxx_remove(struct platform_device *pdev)
{
struct xxx_struct *pdata = platform_get_drvdata(pdev);
...
cancel_delayed_work(&pdata->xxx_dwork);
cancel_delayed_work_sync(&pdata->xxx_dwork);
/* 释放中断 */
free_irq(pdata->xxx_irq, NULL);
...
}
![]()
3.软中断
软中断(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 内核线程中执行。总的来说,中断优先级高于软中断,软中断优先级又高于任何一个线程。软中断适度线程化,可以缓解高负载情况下系统的响应。



















