中断处理的tasklet(小任务)机制和workqueue(工作队列)机制

本文介绍了Linux内核中的中断处理机制,包括tasklet(小任务)和工作队列两种下半部处理方式。tasklet适用于不需要睡眠的延后处理任务,而工作队列则允许任务在进程上下文中执行,支持睡眠。

一、中断处理的tasklet(小任务)机制

中断服务程序一般都是在中断请求关闭的条件下执行的,以避免嵌套而使中断控制复杂化。但是,中断是一个随机事件,它随时会到来,如果关中断的时间太长,CPU就不能及时响应其他的中断请求,从而造成中断的丢失。因此,Linux内核的目标就是尽可能快的处理完中断请求,尽其所能把更多的处理向后推迟。例如,假设一个数据块已经达到了网线,当中断控制器接受到这个中断请求信号时,Linux内核只是简单地标志数据到来了,然后让处理器恢复到它以前运行的状态,其余的处理稍后再进行(如把数据移入一个缓冲区,接受数据的进程就可以在缓冲区找到数据)。因此,内核把中断处理分为两部分:上半部(tophalf)和下半部(bottomhalf),上半部(就是中断服务程序)内核立即执行,而下半部(就是一些内核函数)留着稍后处理,

首先,一个快速的“上半部”来处理硬件发出的请求,它必须在一个新的中断产生之前终止。通常,除了在设备和一些内存缓冲区(如果你的设备用到了DMA,就不止这些)之间移动或传送数据,确定硬件是否处于健全的状态之外,这一部分做的工作很少。

下半部运行时是允许中断请求的,而上半部运行时是关中断的,这是二者之间的主要区别。

但是,内核到底什时候执行下半部,以何种方式组织下半部?这就是我们要讨论的下半部实现机制,这种机制在内核的演变过程中不断得到改进,在以前的内核中,这个机制叫做bottomhalf(简称bh),在2.4以后的版本中有了新的发展和改进,改进的目标使下半部可以在多处理机上并行执行,并有助于驱动程序的开发者进行驱动程序的开发。下面主要介绍常用的小任务(Tasklet)机制及2.6内核中的工作队列机制。


小任务机制   

这里的小任务是指对要推迟执行的函数进行组织的一种机制其数据结构为tasklet_struct,每个结构代表一个独立的小任务,其定义如下:

structtasklet_struct {
structtasklet_struct *next;         /*指向链表中的下一个结构*/
          unsignedlong state;                /* 小任务的状态*/
          atomic_tcount;        /* 引用计数器*/
          void(*func) (unsigned long);                /* 要调用的函数*/
          unsignedlong data;                 /* 传递给函数的参数*/
};
结构中的func域就是下半部中要推迟执行的函数,data是它唯一的参数。
State域的取值为TASKLET_STATE_SCHED或TASKLET_STATE_RUN。TASKLET_STATE_SCHED表示小任务已被调度,正准备投入运行,TASKLET_STATE_RUN表示小任务正在运行。TASKLET_STATE_RUN只有在多处理器系统上才使用,单处理器系统什么时候都清楚一个小任务是不是正在运行(它要么就是当前正在执行的代码,要么不是)。
Count域是小任务的引用计数器。如果它不为0,则小任务被禁止,不允许执行;只有当它为零,小任务才被激活,并且在被设置为挂起时,小任务才能够执行。
1. 声明和使用小任务大多数情况下,为了控制一个寻常的硬件设备,小任务机制是实现下半部的最佳选择。小任务可以动态创建,使用方便,执行起来也比较快。
我们既可以静态地创建小任务,也可以动态地创建它。选择那种方式取决于到底是想要对小任务进行直接引用还是一个间接引用。如果准备静态地创建一个小任务(也就是对它直接引用),使用下面两个宏中的一个:
DECLARE_TASKLET(name,func, data)
DECLARE_TASKLET_DISABLED(name,func, data)
这两个宏都能根据给定的名字静态地创建一个tasklet_struct结构。当该小任务被调度以后,给定的函数func会被执行,它的参数由data给出。这两个宏之间的区别在于引用计数器的初始值设置不同。第一个宏把创建的小任务的引用计数器设置为0,因此,该小任务处于激活状态。另一个把引用计数器设置为1,所以该小任务处于禁止状态。例如:
DECLARE_TASKLET(my_tasklet,my_tasklet_handler, dev);
这行代码其实等价于
structtasklet_struct my_tasklet = { NULL, 0, ATOMIC_INIT(0),
                                                        tasklet_handler,dev};
这样就创建了一个名为my_tasklet的小任务,其处理程序为tasklet_handler,并且已被激活。当处理程序被调用的时候,dev就会被传递给它。
2.  编写自己的小任务处理程序小任务处理程序必须符合如下的函数类型:
voidtasklet_handler(unsigned long data)
由于小任务不能睡眠,因此不能在小任务中使用信号量或者其它产生阻塞的函数。但是小任务运行时可以响应中断。
3. 调度自己的小任务通过调用tasklet_schedule()函数并传递给它相应的tasklt_struct指针,该小任务就会被调度以便适当的时候执行:
tasklet_schedule(&my_tasklet);        /*把my_tasklet标记为挂起 */
在小任务被调度以后,只要有机会它就会尽可能早的运行。在它还没有得到运行机会之前,如果一个相同的小任务又被调度了,那么它仍然只会运行一次。
        可以调用tasklet_disable()函数来禁止某个指定的小任务。如果该小任务当前正在执行,这个函数会等到它执行完毕再返回。调用tasklet_enable()函数可以激活一个小任务,如果希望把以DECLARE_TASKLET_DISABLED()创建的小任务激活,也得调用这个函数,如:
tasklet_disable(&my_tasklet);        /*小任务现在被禁止,这个小任务不能运行*/
tasklet_enable(&my_tasklet);        /*  小任务现在被激活*/
也可以调用tasklet_kill()函数从挂起的队列中去掉一个小任务。该函数的参数是一个指向某个小任务的tasklet_struct的长指针。在小任务重新调度它自身的时候,从挂起的队列中移去已调度的小任务会很有用。这个函数首先等待该小任务执行完毕,然后再将它移去。
4.tasklet的简单用法
    下面是tasklet的一个简单应用,以模块的形成加载。

#include <linux/module.h>
#include <linux/init.h>
#include <linux/fs.h>
#include <linux/kdev_t.h>
#include <linux/cdev.h>
#include <linux/kernel.h>
#include <linux/interrupt.h>

static struct  t asklet_struct my_tasklet;

static void tasklet_handler (unsigned long d ata)
{
        printk(KERN_ALERT,"tasklet_handler is running./n");
}

static int __init test_init(void)
{
        tasklet_init(&my_tasklet,tasklet_handler,0);
        tasklet_schedule(&my_tasklet);
        return0;
}

static  void __exit test_exit(void)
{
        tasklet_kill(&tasklet);
        printk(KERN_ALERT,"test_exit is running./n");
}
MODULE_LICENSE("GPL");

module_init(test_init);
module_exit(test_exit);

从这个例子可以看出,所谓的小任务机制是为下半部函数的执行提供了一种执行机制,也就是说,推迟处理的事情是由tasklet_handler实现,何时执行,经由小任务机制封装后交给内核去处理。

 

二、中断处理的工作队列机制

工作队列(work queue)是另外一种将工作推后执行的形式,它和前面讨论的tasklet有所不同。工作队列可以把工作推后,交由一个内核线程去执行,也就是说,这个下半部分可以在进程上下文中执行。这样,通过工作队列执行的代码能占尽进程上下文的所有优势。最重要的就是工作队列允许被重新调度甚至是睡眠。

那么,什么情况下使用工作队列,什么情况下使用tasklet。如果推后执行的任务需要睡眠,那么就选择工作队列。如果推后执行的任务不需要睡眠,那么就选择tasklet。另外,如果需要用一个可以重新调度的实体来执行你的下半部处理,也应该使用工作队列。它是唯一能在进程上下文运行的下半部实现的机制,也只有它才可以睡眠。这意味着在需要获得大量的内存时、在需要获取信号量时,在需要执行阻塞式的I/O操作时,它都会非常有用。如果不需要用一个内核线程来推后执行工作,那么就考虑使用tasklet。


  1. 工作、工作队列和工作者线程

如前所述,我们把推后执行的任务叫做工作(work),描述它的数据结构为work_struct,这些工作以队列结构组织成工作队列(workqueue),其数据结构为workqueue_struct,而工作线程就是负责执行工作队列中的工作。系统默认的工作者线程为events,自己也可以创建自己的工作者线程。

  1. 表示工作的数据结构

工作用<linux/workqueue.h>中定义的work_struct结构表示:

struct work_struct{

unsigned long pending; /* 这个工作正在等待处理吗?*/

struct list_head entry; /* 连接所有工作的链表 */

void (*func) (void *); /* 要执行的函数 */

void *data; /* 传递给函数的参数 */

void *wq_data; /* 内部使用 */

struct timer_list timer; /* 延迟的工作队列所用到的定时器 */

};

这些结构被连接成链表。当一个工作者线程被唤醒时,它会执行它的链表上的所有工作。工作被执行完毕,它就将相应的work_struct对象从链表上移去。当链表上不再有对象的时候,它就会继续休眠。

3. 创建推后的工作

要使用工作队列,首先要做的是创建一些需要推后完成的工作。可以通过DECLARE_WORK在编译时静态地建该结构:

DECLARE_WORK(name, void (*func) (void *), void *data);

这样就会静态地创建一个名为name,待执行函数为func,参数为data的work_struct结构。

同样,也可以在运行时通过指针创建一个工作:

INIT_WORK(struct work_struct *work, woid(*func) (void *), void *data);

这会动态地初始化一个由work指向的工作。

4. 工作队列中待执行的函数

工作队列待执行的函数原型是:

void work_handler(void *data)

这个函数会由一个工作者线程执行,因此,函数会运行在进程上下文中。默认情况下,允许响应中断,并且不持有任何锁。如果需要,函数可以睡眠。需要注意的是,尽管该函数运行在进程上下文中,但它不能访问用户空间,因为内核线程在用户空间没有相关的内存映射。通常在系统调用发生时,内核会代表用户空间的进程运行,此时它才能访问用户空间,也只有在此时它才会映射用户空间的内存。

5. 对工作进行调度

现在工作已经被创建,我们可以调度它了。想要把给定工作的待处理函数提交给缺省的events工作线程,只需调用

schedule_work(&work);

work马上就会被调度,一旦其所在的处理器上的工作者线程被唤醒,它就会被执行。

有时候并不希望工作马上就被执行,而是希望它经过一段延迟以后再执行。在这种情况下,可以调度它在指定的时间执行:

schedule_delayed_work(&work, delay);

这时,&work指向的work_struct直到delay指定的时钟节拍用完以后才会执行。

6. 工作队列的简单应用

#include <linux/module.h>
#include <linux/init.h>
#include <linux/workqueue.h>

static struct workqueue_struct *queue = NULL;
static struct work_struct work;

static void work_handler(struct work_struct *data)
{
        printk
(KERN_ALERT "work handler function./n");
}

static int __init test_init(void)
{
        
queue = create_singlethread_workqueue("helloworld"); /*创建一个单线程的工作队列*/
        
if (!queue)
                
goto err;

        INIT_WORK
(&work, work_handler);
        schedule_work
(&work);

        
return 0;
err
:
        
return -1;
}

static void __exit test_exit(void)
{
        destroy_workqueue
(queue);
}
MODULE_LICENSE
("GPL");
module_init
(test_init);
module_exit
(test_exit);

<think>我们正在讨论Linux内核中断处理中的工作队列Workqueue)作为下半部的实现方法。根据引用资料,特别是引用[1][3][4],我们可以总结如下: 中断处理分为上半部(Top Half)下半部(Bottom Half)。上半部在中断上下文中快速执行,处理紧急、与硬件相关的任务;下半部则延后处理那些可以稍后执行且允许睡眠的任务工作队列Workqueue)是中断下半部的一种实现机制,它在进程上下文中执行,因此可以睡眠或重新调度。这意味着如果我们需要在中断下半部执行可能阻塞的操作(如I/O操作、等待信号量等),工作队列是一个合适的选择。 实现工作队列作为中断下半部的基本步骤包括: 1. 创建工作队列(或使用内核提供的默认工作队列)。 2. 创建工作项(work_struct),并定义其处理函数。 3. 在中断上半部(中断处理函数)中调度工作项,将其加入工作队列。 下面详细说明: ### 1. 创建工作队列工作项 有两种方式使用工作队列: - 使用内核提供的默认工作队列(如`system_wq`),然后直接将工作项提交到默认队列。这种方式简单,但需要注意默认队列是共享的,长时间的任务可能会影响其他使用默认队列的任务。 - 自行创建专用工作队列(使用`alloc_workqueue`函数)。这种方式可以避免与其他任务相互影响,但需要自行管理。 #### 使用默认工作队列 通常,我们使用`INIT_WORK`宏来初始化一个工作项,并指定其处理函数。 ```c #include <linux/workqueue.h> struct work_struct my_work; // 工作处理函数 static void work_handler(struct work_struct *work) { // 处理任务,这里可以睡眠 printk(KERN_INFO "Work is being executed in process context\n"); } // 在模块初始化中初始化工作项 INIT_WORK(&my_work, work_handler); ``` #### 创建专用工作队列 如果任务需要较长执行时间,为了避免影响默认工作队列,可以创建专用工作队列: ```c struct workqueue_struct *my_wq; // 创建专用工作队列 my_wq = alloc_workqueue("my_workqueue", WQ_MEM_RECLAIM, 1); // 最后一个参数是并发级别 // 初始化工作项(与默认队列相同) DECLARE_WORK(my_work, work_handler); // 或者使用INIT_WORK ``` ### 2. 在中断处理函数中调度工作项 在中断处理函数(上半部)中,我们使用`queue_work`或`queue_delayed_work`(如果需要延迟执行)将工作项加入工作队列。 ```c static irqreturn_t irq_handler(int irq, void *dev_id) { // 处理紧急任务(如读状态寄存器、清除中断标记等) // 调度下半部 queue_work(my_wq, &my_work); // 如果是专用工作队列 // 或者使用默认队列:queue_work(system_wq, &my_work); return IRQ_HANDLED; } ``` ### 3. 清理工作 在模块卸载时,需要取消挂起的工作项并销毁工作队列(如果是专用工作队列): ```c // 取消工作项(确保工作项没有在执行) cancel_work_sync(&my_work); // 如果是专用工作队列,销毁它 destroy_workqueue(my_wq); ``` ### 注意事项 - 工作队列处理函数运行在进程上下文,因此可以调用可能引起睡眠的函数(如`mutex_lock`、`msleep`等)。 - 工作队列可以被多个中断处理函数共享,但需要注意并发控制(如使用锁)。 - 默认工作队列(`system_wq`)是共享资源,因此长时间的任务可能会影响其他使用默认工作队列内核任务。对于耗时任务,建议创建专用工作队列。 ### 示例代码 以下是一个简化的示例,展示在中断处理中使用工作队列作为下半部: ```c #include <linux/interrupt.h> #include <linux/workqueue.h> #include <linux/module.h> // 定义工作项结构体专用工作队列 static struct work_struct my_work; static struct workqueue_struct *my_wq; // 工作处理函数 static void work_handler(struct work_struct *work) { printk(KERN_INFO "Workqueue handler: processing in process context\n"); // 这里可以执行耗时或可能睡眠的操作 } // 中断处理函数(上半部) static irqreturn_t irq_handler(int irq, void *dev_id) { printk(KERN_INFO "Top half: interrupt received\n"); // 调度下半部 queue_work(my_wq, &my_work); return IRQ_HANDLED; } // 模块初始化 static int __init my_module_init(void) { int ret; int irq_num = 10; // 假设中断号 void *dev_id = (void *)&my_work; // 设备ID,这里简化 // 创建专用工作队列 my_wq = alloc_workqueue("my_wq", WQ_MEM_RECLAIM, 1); if (!my_wq) return -ENOMEM; // 初始化工作项 INIT_WORK(&my_work, work_handler); // 注册中断处理函数 ret = request_irq(irq_num, irq_handler, IRQF_SHARED, "my_irq", dev_id); if (ret) { destroy_workqueue(my_wq); return ret; } return 0; } // 模块退出 static void __exit my_module_exit(void) { free_irq(10, (void *)&my_work); // 释放中断 cancel_work_sync(&my_work); // 确保工作项完成 destroy_workqueue(my_wq); // 销毁工作队列 } module_init(my_module_init); module_exit(my_module_exit); MODULE_LICENSE("GPL"); ``` 在这个示例中,当发生中断时,上半部(`irq_handler`)会快速处理并调度工作项到专用工作队列`my_wq`。然后,工作项的处理函数`work_handler`会在进程上下文中执行。 ### 引用说明 - 工作队列允许在进程上下文中执行,因此可以睡眠[^3]。 - 中断分为上下两部分的原因是为了缩短中断屏蔽时间,提高系统响应能力[^4]。 - 工作队列作为中断下半部的实现方式之一,适用于需要延后执行且可能睡眠的任务[^1][^3][^4]。 §§相关问题§§ 1. 工作队列中断tasklet有什么区别? 2. 如何在中断下半部处理中防止竞态条件? 3. 工作队列的并发控制机制有哪些? 4. 默认工作队列专用工作队列的选择标准是什么? </think>### 中断处理工作队列Workqueue)作为下半部的实现方法 在Linux内核中,中断处理分为**上半部(Top Half)****下半部(Bottom Half)**。使用工作队列实现中断下半部的步骤如下: #### 1. **创建工作队列工作项** ```c #include <linux/workqueue.h> // 定义工作队列工作struct workqueue_struct *my_wq; struct work_struct my_work; // 工作处理函数 static void work_handler(struct work_struct *work) { // 此处执行延时操作(可睡眠) printk("Workqueue executing in process context\n"); } ``` #### 2. **初始化工作队列** 在模块初始化时创建工作队列并绑定处理函数: ```c static int __init my_init(void) { // 创建专用工作队列 my_wq = alloc_workqueue("my_workqueue", WQ_MEM_RECLAIM, 1); // 初始化工作项 INIT_WORK(&my_work, work_handler); return 0; } ``` (使用默认工作队列可简化为`schedule_work(&my_work)`)[^3] #### 3. **在中断上半部调度工作项** ```c static irqreturn_t irq_top_half(int irq, void *dev_id) { // 1. 快速处理硬件相关操作(如清除中断标志) // 2. 调度下半部 queue_work(my_wq, &my_work); // 将工作项加入队列 return IRQ_HANDLED; } ``` #### 4. **资源清理** 在模块退出时销毁资源: ```c static void __exit my_exit(void) { destroy_workqueue(my_wq); // 销毁工作队列 free_irq(irq_num, NULL); // 释放中断 } ``` --- ### 关键特性与注意事项 1. **执行上下文** 工作队列在**进程上下文**执行,允许睡眠、调度阻塞操作,这与不能睡眠的上半部形成对比[^3][^4]。 2. **并发控制** - 默认工作队列(如`system_wq`)由所有任务共享 - 专用工作队列(通过`alloc_workqueue()`创建)可隔离关键任务[^3] 3. **延迟处理机制** 使用`queue_delayed_work()`可实现延时调度: ```c struct delayed_work d_work; queue_delayed_work(my_wq, &d_work, msecs_to_jiffies(100)); // 延迟100ms ``` 4. **上半部约束** 上半部必须满足: - 执行时间极短(通常<$10\mu s$) - 禁止睡眠或阻塞 - 避免调用可能引起睡眠的内核函数[^4] --- ### 设计原理 工作队列的核心是通过内核线程异步执行任务: 1. 中断上半部快速响应硬件事件 2. 调用`queue_work()`将任务加入队列 3. 内核线程`kworker`从队列取出任务执行 $$\text{中断上半部} \rightarrow \text{Workqueue} \xrightarrow{\text{kworker线程}} \text{安全执行延时操作}$$ 这种机制确保系统响应性,同时允许完成复杂操作[^1][^4]。 ---
评论 3
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值