大家好,我是王鸽,这篇文章介绍是中断上半部和下半部,主要还是下半部,因为上半部之前的文章有介绍过。前言
CPU 消耗型:此类进程就是一直占用 CPU 计算,CPU 利用率很高
IO 消耗型:此类进程会涉及到 IO,需要和用户交互,比如键盘输入,占用 CPU 不是很高,只需要 CPU 的一部分计算,大多数时间是在等待 IO
CPU 消耗型进程需要高的吞吐率,IO 消耗型进程需要强的响应性,这两点都是调度器需要考虑的。
但是设备的中断会打断内核中进程的正常调度和运行,所以系统对更高吞吐率的追求势必要求中断服务程序尽可能地短小精悍。
软中断(softirq)和硬件中断(硬中断)
在Linux中,软中断(softirq)和硬件中断(硬中断)是两种不同的中断机制,
硬中断主要是由与系统相连的外设(如网卡、硬盘等)自动产生的,用于通知操作系统关于系统外设状态的变化。当中断发生时,CPU会暂停当前的执行流程,转而处理中断请求。
软中断则是执行中断指令产生的,无需外部施加中断请求信号。软中断通常用于处理那些需要尽快完成但又不属于硬中断处理范畴的异步事件,如定时器事件、网络数据包的接收和处理等。软中断的中断号由指令直接指出,无需使用中断控制器。
中断处理
在Linux中,中断处理被分为两个主要部分:顶半部(上半部,Top Half)和下半部(Bottom Half)。这种设计是为了解决中断处理程序执行时间过长和中断丢失的问题,同时确保关键任务能够迅速得到响应。顶半部(上半部,Top Half):
顶半部的主要任务是快速处理中断,并暂时关闭中断请求。它主要处理与硬件紧密相关或时间敏感的事情,比如对硬件的读取寄存器中的中断状态并清除中断标志。当一个中断发生时,顶半部会进行相应的硬件操作,并将中断例程的下半部挂到该设备的下半部执行队列中。由于顶半部的执行速度很快,它可以服务更多的中断请求。下半部(Bottom Half):
下半部用于延迟处理顶半部未完成的工作。与顶半部不同,下半部是可以被中断的,这意味着它可以在执行过程中被新的中断打断。下半部几乎执行了中断处理程序的所有任务,但它并不是非常紧急的,通常比较耗时。因此,它的执行时机由系统自行安排,并不在中断服务上下文中执行。所谓中断上下文,就是IRQ context + softirq context+NMI context。Linux中实现下半部目前使用下面三种方法
1.软中断
2.tasklet
3.工作队列。
这些机制允许下半部处理程序在适当的时机以异步的方式执行,从而避免了长时间占用CPU资源。Linux中断上半部的实现旨在快速响应中断并执行与硬件紧密相关的操作,为下半部处理更复杂的中断事件提供基础。通过优化上半部的执行速度和原子性,Linux系统能够更高效地处理中断,确保系统的稳定性和实时性。如何选择中断
对于一个中断,如何划分出上下两部分呢?哪些处理放在上半步,哪些放在下半部?如果一个任务要保证不被其他中断打断,将其放在上半部。中断下半部实现
在Linux中,中断下半部(Bottom Half)的实现主要是为了处理与中断处理密切相关但不适合在中断上半部执行的工作。这些工作通常比较耗时,或者可以容忍一定的延迟,因此不需要像上半部那样立即完成。Linux中提供了多种机制来实现中断下半部,主要包括软中断(softirq)、tasklet和工作队列(work queue)。这些机制都允许系统在适当的时机异步地执行中断下半部,从而避免了长时间占用CPU资源。软中断(Softirq): 软中断是用软件方式模拟硬件中断的概念,实现宏观上的异步执行效果。它们是在编译期间静态分配的,不能被动态地注册或删除。软中断的优先级仅次于硬件中断,但在执行过程中可能会被其他硬件中断打断。由于软中断的处理是在所有处理器上同时进行的,因此在并发环境下需要加锁保护。
Tasklet: Tasklet是基于软中断实现的,具有更高的灵活性。它可以被动态地创建和销毁。Tasklet的执行也是异步的,并且它允许在多个处理器之间共享执行。但同样地,由于tasklet的优先级低于硬件中断,因此在执行过程中也可能被硬件中断打断。在 SMP 上,调用 tasklet 是会检测 TASKLET_STATE_SCHED 标志,如果同类型在运行,就退出函数
工作队列(Work Queue): 工作队列是另一种实现中断下半部的机制。它利用内核线程来执行下半部的工作。当中断发生时,上半部将下半部的工作放入工作队列,在工作队列机制中,将推后的工作交给一个称之为工作者线程(worker thread)的内核线程去完成(单核下一般会交给默认的线程events/0)。因此,在该机制中,当内核在执行中断的剩余工作时就处在进程上下文(process context)中。也就是说由工作队列所执行的中断代码会表现出进程的一些特性,最典型的就是可以重新调度甚至睡眠。这使得工作队列成为处理耗时任务或可延迟任务的理想选择。
而 tasklet是基于软中断实现的,基本上和软中断相同。但有一点不一样,如果在多处理器的情况下,内核不能保证软中断在哪个处理器上运行(听起来像废话),所以,软中断之间需要考虑共享资源的保护。而在tasklet,内核可以保证,两个同类型(TASKLET_SOFTIRQ和HI_SOFTIRQ)的tasklet不能同时执行,那就说明,同类型tasklet之间,可以不考虑同类型tasklet之间的并发情况。而工作队列完全不同,它是靠内核线程实现的。软中断、tasklet和工作队列的区别与联系
名词 | 描述 |
|---|
软中断 | 1、软中断是在编译期间静态分配的。2、最多可以有32个软中断。3、软中断不会抢占另外一个软中断,唯一可以抢占软中断的是中断处理程序。4、可以并发运行在多个CPU上(即使同一类型的也可以)。所以软中断必须设计为可重入的函数(允许多个CPU同时操作),因此也需要使用自旋锁来保护其数据结构。5、目前只有两个子系直接使用软中断:网络和SCSI。6、执行时间有:从硬件中断代码返回时、在ksoftirqd内核线程中和某些显示检查并执行软中断的代码中。 |
tasklet | 1、tasklet是使用两类软中断实现的:HI_SOFTIRQ和TASKLET_SOFTIRQ。2、可以动态增加减少,没有数量限制。3、同一类tasklet不能并发执行。4、不同类型可以并发执行。5、大部分情况使用tasklet。 |
工作队列 | 1、由内核线程去执行,换句话说总在进程上下文执行。 2、可以睡眠,阻塞。 |
对于软中断,linux kernel也是用一个softirq number唯一标识一个softirq,内核目前实现了10中软中断,定义在linux/interrupt.h中。
enum{ HI_SOFTIRQ=0, //用于高优先级的tasklet TIMER_SOFTIRQ, //for software timer的(所谓software timer就是说该timer是基于系统tick的) NET_TX_SOFTIRQ, //用于网卡数据收发的 NET_RX_SOFTIRQ, //用于网卡数据收发的 BLOCK_SOFTIRQ, //是用于block device的 IRQ_POLL_SOFTIRQ, //用于block device的 TASKLET_SOFTIRQ, //用于普通的tasklet SCHED_SOFTIRQ, //用于多CPU之间的负载均衡的 HRTIMER_SOFTIRQ, //用于高精度timer的 RCU_SOFTIRQ, /* Preferable RCU should always be the last softirq */ //是处理RCU的 NR_SOFTIRQS};
实现方法
软中断
在Linux内核中,软中断是通过一个称为softirq_vec的向量表来管理的。每个软中断类型都对应向量表中的一个条目,该条目包含了软中断的处理函数和其他相关信息。软中断和硬中断是不同的。硬中断是由硬件设备触发的,并且具有更高的优先级。它们通常在中断服务例程(ISR)中处理,这些例程运行在特定的中断上下文中。而软中断则是由内核触发的,用于处理那些不需要立即响应但也不能被推迟太久的任务。软中断执行效率最快,但是需要考虑几点:
同一个软中断支持在不同的 cpu 上并发执行,这样子需要考虑 SMP 下的并发。
软中断不支持动态的定义,只能将软中断静态地编译到内核镜像中,softirq是静态定义的,也就是说系统中有一个定义softirq描述符的数组,
Linux软中断的初始化是一个由内核自动处理的过程,但对于需要自定义softirq的开发者来说需要了解软中断初始化过程。softirq是在编译期间静态分配的,它不像tasklet那样可以动态的分配和删除。
另外构成软中断机制的核心元素包括:
1、 软中断状态寄存器soft interrupt state(irq_stat)
2、 软中断向量表(softirq_vec)
3、 软中断守护daemon
示例:顶半部触发软中断
// 顶半部中触发NET_RX_SOFTIRQraise_softirq(softirq向量表初始化
系统支持多少个软中断,静态定义的数组就会有多少个entry,每个软中断类型(如TIMER_SOFTIRQ、NET_TX_SOFTIRQ等)在向量表中都有一个固定的位置。
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); // /* 开启常规tasklet */ open_softirq(HI_SOFTIRQ, tasklet_hi_action); /* 开启高优先级tasklet */}
注册softirq处理函数
需要定义一个触发 softirq 时的执行函数 open_softirq
externvoidopen_softirq(int nr, void (*action)(struct softirq_action *));/kernel/softirq.cvoid open_softirq(int nr, void (*action)(struct softirq_action *)){ softirq_vec[nr].action = action;}
第一个参数 nr 是之前的 enum 结构中定义的枚举值,第二个参数 action 是一个函数指针,指向需要执行的函数。
在tasklet 中添加
open_softirq(TASKLET_SOFTIRQ, tasklet_action);
这也是为什么说:tasklet 的实现是基于软中断的。
初始化softirq统计数据
内核还维护了一些用于跟踪softirq活动的统计数据。这些统计数据在初始化过程中被清零,以便后续能够准确地记录softirq的触发次数和处理情况。
软中断的名字在如下的数组中定义:
const char * const softirq_to_name[NR_SOFTIRQS] = {"HI", "TIMER", "NET_TX", "NET_RX", "BLOCK", "IRQ_POLL","TASKLET", "SCHED", "HRTIMER", "RCU"};
这个跟linux/interrupt.h中10个软中断匹配。
softirq的统计数据通常是在softirq向量表初始化时自动清零的。softirq的统计数据用于跟踪每种类型的softirq被触发的次数以及它们的处理情况,这对于系统调试和性能分析非常有用。
softirq的统计数据通常定义在softirq_stats数组中,每个softirq类型对应一个统计项。在内核初始化过程中,这个数组会被清零。这个过程通常发生在内核启动的早期阶段,比如在start_kernel()函数的某个子函数中。
简化例子
#include<linux/interrupt.h>/* softirq统计数据数组 */static unsigned int softirq_stats[NR_SOFTIRQS] ____cacheline_aligned;/* 内核初始化函数中的某个部分 */void __init some_early_init_function(void){ /* 清零softirq统计数据 */ memset(softirq_stats, 0, sizeof(softirq_stats)); /* 其他初始化代码... */}
softirq_stats数组被定义为一个全局变量,用于存储softirq的统计数据。NR_SOFTIRQS是一个宏,定义了系统中softirq类型的数量。在some_early_init_function函数中,使用memset函数将softirq_stats数组的内容全部设置为0,从而实现了统计数据的清零。
启动softirq处理
执行一个 softirq ,需要使用的接口是 raise_softirq,调用raise_softirq这个接口函数来触发本地CPU上的softirq。
voidraise_softirq(unsignedint nr){ unsigned long flags; local_irq_save(flags); raise_softirq_irqoff(nr); local_irq_restore(flags);}inlinevoidraise_softirq_irqoff(unsignedint nr){ __raise_softirq_irqoff(nr);/** If we're in an interrupt or softirq, we're done* (this also catches softirq-disabled code). We will* actually run the softirq once we return from* the irq or softirq.** Otherwise we wake up ksoftirqd to make sure we* schedule the softirq soon.*/ if (!in_interrupt()) wakeup_softirqd();}staticvoidwakeup_softirqd(void){/* Interrupts are disabled: no need to stop preemption */ struct task_struct *tsk = __this_cpu_read(ksoftirqd); if (tsk && tsk->state != TASK_RUNNING) wake_up_process(tsk);//in_interrupt判断现在是否在中断上下文中,或者软中断是否被禁止,//如果都不成立,否则,我们必须要调用wakeup_softirqd函数用来唤醒本CPU上的softirqd这个内核线程。}
在中断处理程序中触发软中断是最常见的形式,一个硬件中断处理完成之后。下面的函数在处理完硬件中断之后退出中断处理函数,在irq_exit中会触发软件中断的处理。
中断处理模型:
软中断的分析具体到代码还是比较复杂的,需要再写一篇文章来说明。
不论是哪种调用方式,最终都会触发到软中断的核心处理函数do_softirq,它处理当前CPU上的所有软中断。内核将软中断设计尽量与平台无关,
举一个例子
例子假设你已经定义了一个自定义的softirq类型MY_CUSTOM_SOFTIRQ。实际上,Linux内核为常见的softirq类型(如TIMER_SOFTIRQ、NET_TX_SOFTIRQ等)已经定义了值,因此你需要选择一个未被使用的值作为你的自定义softirq类型。
#include<linux/interrupt.h>#include<linux/module.h>/* 自定义的softirq处理函数 */staticvoidmy_softirq_handler(struct softirq_action *action){/* 在这里处理自定义的softirq任务 */}/* 模块初始化函数 */staticint __init my_module_init(void){/* 注册自定义的softirq处理函数 */open_softirq(MY_CUSTOM_SOFTIRQ, my_softirq_handler);/* 其他初始化代码... */return 0;}/* 模块退出函数 */staticvoid __exit my_module_exit(void){/* 注销自定义的softirq处理函数 */close_softirq(MY_CUSTOM_SOFTIRQ);/* 其他清理代码... */}/* 模块声明 */module_init(my_module_init);module_exit(my_module_exit);MODULE_LICENSE("GPL");
此外,还需要注意的是,注册和注销softirq处理函数通常应该在模块的初始化和退出函数中完成,以确保softirq的正确管理。在模块卸载时,必须注销所有注册的softirq处理函数,以避免潜在的内存泄漏和其他问题。
内核中建议
/* PLEASE, avoid to allocate new softirqs, if you need not _really_ highfrequency threaded job scheduling. For almost all the purposestasklets are more than enough. F.e. all serial device BHs etal. should be converted to tasklets, not to softirqs.*/
如果你的工作不是非常高频地被执行,请尽量不要定义 softirq,使用 tasklet 来代替处理。
tasklet
tasklet是内核提供的一种轻量级的任务处理机制,它基于软中断(softirq)实现。tasklet通常用于需要延迟执行的任务,其执行上下文是软中断上下文。
一个使用tasklet的中断程序首先会通过执行中断处理程序来快速完成上半部分的工作,接着通过调用tasklet使得下半部分的工作得以完成。在中断处理程序中,除了完成对中断的响应等工作,还要调用tasklet。
在较新的 Linux 内核版本中,tasklet已经被work_struct和工作队列(workqueue)机制所取代,因为工作队列提供了更多的灵活性和功能。
struct tasklet_struct {struct tasklet_struct *next; //链表中的下一个taskletunsigned long state; //tasklet的状态atomic_t count; //引用计数器void(*func) (unsigned long data); //tasklet处理函数 --参数就下面的 dataunsigned long data; //给tasklet处理函数的参数};
count : 引用计数器,值为0,tasklet 才可以调度(调度结果就是 func被执行)
struct tasklet_struct结构体,它包含了 tasklet 的状态和函数指针等信息。
tasklet定义分有静态定义和动态定义两种方式
动态定义:
struct tasklet_struct my_tasklet;
静态定义:
DECLARE_TASKLET(my_tasklet, my_tasklet_function, data);
比如:
voidmy_tasklet_function(unsignedlong data);char my_tasklet_data[]="my_tasklet_function was called.";/*静态定义并初始化一个tasklet 内核微线程*/DECLARE_TASKLET(my_tasklet,my_tasklet_function,(unsigned long)my_tasklet_data);
初始化和调度一个tasklet,下面是一个基本的步骤概述:
定义tasklet结构体
在内核模块或驱动中,你需要定义一个struct tasklet_struct类型的变量。
初始化tasklet
使用tasklet_init()函数来初始化tasklet。这个函数接受三个参数:tasklet结构体的指针、处理函数的指针以及一个数据参数(通常设置为0)。
编写tasklet处理函数
你需要编写一个处理函数,这个函数将在tasklet被调度时执行。它应该接受一个unsigned long类型的参数(这是你在初始化tasklet时传递的数据参数)。
调度tasklet
当需要执行tasklet时,使用tasklet_schedule()函数来调度它。这会导致tasklet被添加到软中断队列中,等待执行。tasklet一般是用于处理中断的下半部的,所以一般在中断的上半部调度tasklet工作函数。
void tasklet_init(struct tasklet_struct *t,void (*func)(unsigned long), unsigned long data)t: struct tasklet_struct 结构指针func:小任务函数data:传递给工作函数的实际参数示例:char p[]="hello tasklet!"struct tasklet_struct t;void t_func(unsigned long data){ char *p = (char*) data; printk("%s",p);}tasklet_init(&t,t_func, (unsigned long )p);
伪代码:
#include<linux/interrupt.h>#include<linux/tasklet.h>/* tasklet的处理函数 */ staticvoidmy_tasklet_func(unsignedlong data){ // 在这里处理tasklet的任务 // data参数是初始化tasklet时传递的,这里我们没有使用它 udelay(2000); //不是休眠,而软件延时} /* tasklet的声明 */ static struct tasklet_struct my_tasklet; /* 在某个初始化函数中 */ staticint __init my_module_init(void){ /* 初始化tasklet */ tasklet_init(&my_tasklet, my_tasklet_func, 0); /* 其他初始化代码... */ return 0; } /* 在需要调度tasklet的地方 */ voidsome_function(void){ /* 调度tasklet */ tasklet_schedule(&my_tasklet); } /* 模块退出函数 */ staticvoid __exit my_module_exit(void){ /* 在模块退出时,确保tasklet不再被调度 */ tasklet_kill(&my_tasklet); //模块初始化函数初始化了tasklet,则相应地是在模块卸载函数调用tasklet_kill函数来销毁tasklet任务。} /* 模块声明 */ module_init(my_module_init); module_exit(my_module_exit); MODULE_LICENSE("GPL");
my_module_init()函数是模块的初始化函数,它调用tasklet_init()来初始化my_tasklet。some_function()是一个假设的函数,它会在某个时刻调用tasklet_schedule()来调度tasklet的执行。最后,在模块退出时,my_module_exit()函数调用tasklet_kill()来确保tasklet不再被调度。
请注意,tasklet的执行是在软中断上下文中进行的,这意味着它不能阻塞,也不能执行耗时的操作。此外,tasklet的执行是原子的,即在同一时间只有一个tasklet实例在执行。因此,如果你的tasklet需要访问共享资源,你需要确保适当地保护这些资源以防止竞态条件。
工作队列
工作队列 ( workqueue )是将操作(或回调)延期异步执行的一种机制。工作队列可以把工作推后,交由一个内核线程去执行,并且工作队列是执行在线程上下文中,因此工作执行过程中可以被重新调度、抢占、睡眠。
工作项(work item)是工作队列中的元素,是一个回调函数和多个回调函数输入参数的集合,有时也会有额外的属性成员,总之通过一个结构体即可记录和描述一个工作项。说白了 工作队列就是内核中延后工作的一种方式,延后工作在无数场景都可以反复调度使用。
初始化工作项
a. 动态注册
//kernel/include/linux/workqueue.hINIT_WORK(_work, _func)#defineINIT_WORK(_work, _func) \do { \__INIT_WORK((_work), (_func), 0); \} while (0)
b. 静态注册
//kernel/include/linux/workqueue.h#defineDECLARE_WORK(n, f) \struct work_struct n = __WORK_INITIALIZER(n, f)
DECLARE_WORK(my_queue_work,my_queue_func,&data); //编译时创建名为my_queue_work的结构体变量并把函数入口地址和参数地址赋给它;
使用静态注册可以省略定义_work,且DECLARE_WORK需要放在代码头部预处理。
运行指定工作项
a. 自定义队列运行queue_work
/* @wq: workqueue to use* @work: work to queue* Returns %false if @work was already on a queue, %true otherwise.*/queue_work(struct workqueue_struct *wq, struct work_struct *work)
b. 系统工作队列运行schedule_work
staticinlineboolschedule_work(struct work_struct *work){ return queue_work(system_wq, work);}
queue_work 把任务提交到自定义创建的队列[my workqueue/0]中执行queue_delayed_work 把任务提交到自定义创建的队列[my workqueue/0]中,(延时一定的时间)执行schedule_work 把任务提交到内核默认提供的工作队列[events/0]中执行schedule_delayed_work 把任务提交到内核默认提供的工作队列[events/0]中,(延时一定的时间)执行比如:schedule_delayed_work(&my_queue_work,tick); //延时tick个滴答后再提交工作
一般情况下,需要指定情况多次重复调用工作项,选择定时器+queue_work。如果是指定情况下调用一次,则使用schedule_work,利用系统的工作队列执行需要的工作项。
工作队列的使用流程
工作队列是系统延时调度的一个自定义函数, 一般用来处理中断中底半等耗时操作
1、定义struct work_struct irq_queue;
2、初始化INIT_WORK(&irq_queue,do_irq_queuework);
3、调用方法:schedule_work(&irq_queue);
调用完毕后系统会释放此函数,所以如果想再次执行的话,就再次调用schedule_work()即可。
工作队列的使用又分两种情况,一种是利用系统共享的工作队列来添加自己的工作,这种情况处理函数不能消耗太多时间,这样会影响共享队列中其他任务的处理;另外一种是创建自己的工作队列并添加工作。
一种是利用系统共享的工作队
第一步:声明或编写一个工作处理函数
void my_queue_func();
第二步:创建一个工作结构体变量,并将处理函数和参数的入口地址赋给这个工作结构体变量
DECLARE_WORK(my_queue_work,my_queue_func,&data); //编译时创建名为my_queue_work的结构体变量并把函数入口地址和参数地址赋给它;----这种是静态编译的时候加入
或者INIT_WORK(&my_queue_work,my_queue_func,&data); //初始化已经创建的my_queue_work,其实就是往这个结构体变量中添加处理函数的入口地址和data的地址,通常在驱动的open函数中完成。
第三步:将工作结构体变量添加入系统的共享工作队列
schedule_work(&my_queue_work); //添加入队列的工作完成后会自动从队列中删除
或schedule_delayed_work(&my_queue_work,tick); //延时tick个滴答后再提交工作
第四步:删除共享工作队列
cancel_work_sync(&my_queue_work);
一种是创建自己的工作队列并添加工作
第一步:声明工作处理函数和一个指向工作队列的指针
void my_queue_func();
struct workqueue_struct *p_queue;
第二步:创建自己的工作队列和工作结构体变量(通常在open函数中完成)
p_queue=create_workqueue("my_queue"); //创建一个名为my_queue的工作队列并把工作队列的入口地址赋给声明的指针
struct work_struct my_queue_work;
INIT_WORK(&my_queue_work, my_queue_func, &data); //创建一个工作结构体变量并初始化,和第一种情况的方法一样
第三步:将工作添加入自己创建的工作队列等待执行
queue_work(p_queue, &my_queue_work);
//作用与schedule_work()类似,不同的是将工作添加入p_queue指针指向的工作队列而不是系统共享的工作队列
第四步:删除自己的工作队列
destroy_workqueue(p_queue); //一般是在close函数中删除
这种在触摸屏 按键驱动使用的比较多 选择定时器+queue_work 比如drivers/input/keyboard/gpio_keys.c
struct gpio_button_data { const struct gpio_keys_button *button; struct input_dev *input; struct timer_list timer; struct work_struct work; //工作队列 unsigned int timer_debounce; /* in msecs */ unsigned int irq; spinlock_t lock; bool disabled; bool key_pressed;};
static int gpio_keys_probe(struct platform_device *pdev)
|----error = gpio_keys_setup_key(pdev, input, bdata, button);
|---INIT_WORK(&bdata->work, gpio_keys_gpio_work_func); //动态注册 具体的work
|---static void gpio_keys_gpio_work_func(struct work_struct *work){
|----struct gpio_button_data *bdata =container_of(work, struct gpio_button_data, work);
|---gpio_keys_gpio_report_event(bdata);
|---setup_timer(&bdata->timer, gpio_keys_gpio_timer, (unsigned long)bdata); //定时器
|--static void gpio_keys_gpio_timer(unsigned long _data){
|----struct gpio_button_data *bdata = (struct gpio_button_data *)_data;
schedule_work(&bdata->work);//调用系统的工作队列运行工作项
|---static int gpio_keys_remove(struct platform_device *pdev)
|---gpio_remove_key(&ddata->data[i]);
|---cancel_work_sync(&bdata->work); //停止工作队列
那么,什么情况下使用工作队列,什么情况下使用tasklet。
如果推后执行的任务需要睡眠,那么就选择工作队列。如果推后执行的任务不需要睡眠,那么就选择tasklet。
另外,如果需要用一个可以重新调度的实体来执行你的下半部处理,也应该使用工作队列。它是唯一能在进程上下文运行的下半部实现的机制,也只有它才可以睡眠。这意味着在需要获得大量的内存时、在需要获取信号量时,在需要执行阻塞式的I/O操作时,它都会非常有用。如果不需要用一个内核线程来推后执行工作,那么就考虑使用tasklet。
总结
soft_irq用在对底半执行时间要求比较紧急或者非常重要的场合,在中断上下文执行。tasklet和work queue在普通的driver里用的相对较多,主要区别是tasklet是在中断上下文执行,而workqueue是在process上下文,因此可以执行可能sleep的操作。
| | | |
|---|
| raise_softirq | | |
| tasklet_schedule | | |
| queue_work | | |