大部分内容来自:linux内核修炼之道
中断的下半部
简单的说,某些设备调用的中断服务程序,需要大量执行时间,如果这过程中,设备一直等着中断服务程序执行完毕,设备就有可能会丢失数据,下文举了网卡作为例子。所以需要把中断变得更灵活,分为上半部和下半部,上半部和传统的中断服务一样,但只负责处理紧急和必要的功能,如对中断到达的响应,不必等到服务程序执行完毕。而下半部负责处理耗时长的操作。这样,在执行下半部的时候,上半部可以继续工作,保证所有中断都是打开状态,中断控制器不会一直处于Busy的状态。下半部有5种机制,BH/任务队列/软中断/tasklet/工作队列。其中,BH和任务队列是早期机制,灵活性低,已经淘汰。软中断和tasklet在服务程序上下文中执行,所以性能好,但是不应该处理延时长的任务。延时长的,需要休眠的任务,工作对列就是它唯一的选择。软中断性能最好,但是需要考虑加锁机制,tasklet比较安全,对加锁要求低,因此,对于下半部模块代码的保护考虑不好的,就要使用tasklet弥补。
具体描述看下文:
到目前为止,对中断的处理过程似乎已经很完备了:设备根据自己的需要产生中断信号,将其通过中断控制器发送给 CPU,
CPU 检测到该中断信号后停止它当前的工作,然后根据中断向量号获取中断服务程序的地址并执行。
如果设备的请求都很容易满足,那么这样的处理过程确实已经足够简单高效,并且无可挑剔,既节省了内核轮询时耗费的大量资源,又不会影响系统的正常运作。但现实总是那么不尽如人意,内核往往需要完成大量的工作才能满足设备的需要。
比如对于一个网卡中断,网卡的目的是,内核收到中断后将网卡收到的网络数据进行处理,这个处理的完整过程就应该是:从网卡硬件的缓冲区中把网卡收到的网络数据包复制到系统内存中,然后对这个数据包进行 TCP/IP 协议栈的处理,首先是链路层,然后是 IP 层(包括分片、奇偶校验等),再然后是 TCP层(TCP 层的实现相当复杂,会花费比较长的时间对数据包进行一些状态或者内容的分析处理),最后会通过 socket 将数据包传入用户空间。
在将数据传入用户空间之前,所有这些将花费大量时间的处理都会在中断服务程序中完成。在这段时间内,CPU 将不再响应网卡发来的其他中断,网卡将因为自身缓存的不足而丢失数据。
为了解决这种响应一次中断需要完成大量工作的问题,Linux 将对中断的处理划分为两个部分:上半部和下半部。上半部是实际响应中断的程序,也就是传统意义上的中断服务程序,只完成一些比较紧急和必要的功能,比如对中断的到达进行响应确认。
而下半部则是完成其他那些可以延缓处理的部分,在下半部的执行期间,所有的中断都是打开的。这样上半部的执行将非常快,且允许在下半部工作期间,上半部仍能继续服务新的中断。
对于上面网卡中断的例子来说,上半部将只是完成将数据复制到内存的工作,而其他的协议栈处理等不是特别紧急的工作则是放到了下半部去处理。
下半部的实现机制
内核最初(现在废弃不用了)通过 BH(bottom half)机制实现下半部,定义了一个函数指针数组,共有 32 个函数指针,采用数组索引来访问与之相对应的一套函数(也就是 bottom half)。
enum
{
TIMER_BH = 0,
TQUEUE_BH,
DIGI_BH,
SERIAL_BH,
RISCOM8_BH,
⋯⋯
};
显而易见,原始的 BH 机制有个很大的局限,就是个数限制在 32 个以内,因为每个 bottom half 上只能挂接一个函数,随着系统硬件越来越多,这个数目显然是不够用的。另外,同一时间不允许任何两个bottom half 同时运行,即使它们分属不同的 CPU。
因此,内核引入了任务队列(task queue)来取代 BH 机制,每个队列中都包含一个由延缓执行的函数组成的链表。
内核使用 struct tq_struct 描述延缓执行的任务,使用时驱动可以使用 DECLARE_TASK_QUEUE宏定义自己的任务队列,然后定义一个 struct tq_struct 变量,并将其注册到自己定义的任务队列上,之后可以通过手工调用 run_task_queue()函数启动该任务对列中的所有任务,也可以直接将 run_task_queue()的
地址作为 BH 机制中函数指针数组中的一个函数指针,进而让 BH 机制在合适的时候自动执行延缓的任务。
但大多数情况下,驱动都没有必要定义自己的任务队列,因为系统已经预定义了一些任务队列,比如tq_disk,由内存管理模块使用。
但是任务队列的机制仍然不够灵活,并不能满足一些对性能要求比较高的子系统,比如网络。因此内核又引入了软中断(softirq)和 tasklet 的机制,并在 2.6 内核中彻底摒弃了 BH 机制,同时任务队列也被工作队列(work queue)所取代。
1.软中断
这里所说的软中断与系统调用所使用软中断(确切地说应该是软件中断)并不是同一个概念。软中断沿用了最早的 BH 机制的思想,但在这个 BH 机制之上,实现了一个更加庞大和复杂的软中断子系统。
软中断在编译期间静态分配,内核定义了一个软中断数组,最多可以包含 32 个软中断(由 struct softirq_action 描述),每个被注册的软中断都占据该数组中的一项,
但与 BH 机制相比,它们可以在所有的CPU 上同时执行,即使是两个相同类型的软中断,所以使用时需要特别小心。同时,内核还定义了一组枚举变量,预定义了几个软中断的用途。
enum
{
HI_SOFTIRQ=0,
TIMER_SOFTIRQ,
// 优先级高的 tasklets
// 定时器的下半部
NET_TX_SOFTIRQ, // 发送网络数据包
NET_RX_SOFTIRQ, // 接收网络数据包
BLOCK_SOFTIRQ,
TASKLET_SOFTIRQ, // tasklets
SCHED_SOFTIRQ,
#ifdef CONFIG_HIGH_RES_TIMERS
HRTIMER_SOFTIRQ,
#endif
};
一个软中断只能被中断服务程序抢占,而不会去抢占其他软中断。通常中断服务程序会在返回前触发它的软中断以使其在稍后被执行,然后内核在执行完该中断服务程序后,就会马上调用 do_softirq()函数,执行软中断去完成剩余的任务。
2.tasklet
tasklet 是利用软中断实现的一种下半部机制。
与软中断相比,两个不同类型的 tasklet 可以在不同的 CPU上同时执行,但相同类型的 tasklet 却不能够同时执行,因此 tasklet 的锁保护要求较低,这就大大降低了驱
动开发的难度。
在 tasklet 的源码注释中还说明了几点特性,总之归结为一点,就是:
同一个 tasklet 只会在一个 CPU上运行。
tasklet 本身就是软中断,可以是 HI_SOFTIRQ 和 TASKLET_SOFTIRQ 两者之一,它们之间的唯一区别就是 HI_SOFTIRQ 类型的软中断要比 TASKLET_SOFTIRQ 类型的高。当内核调度一个 tasklet 时,就会唤起这两个软中断中的一个,随后该软中断会被特定的函数进行处理,执行所有已调度的 tasklet。
3.工作队列
工作队列是另一种实现下半部的机制,它在 2.6 内核被引入用来取代任务队列。与前面讨论的软中断和 tasklet 机制不同的是,工作队列把延缓的工作交由一个内核线程去执行,因此通过工作队列执行的代码是运行在进程上下文的,允许重新调度和睡眠。
另外,你可以通过工作队列将工作延后到一个明确的时间间隔后再执行,因此它通常用来处理不是很紧急的事件。
请参考工作对列和等待队列,更多地比较其作用范围
http://blog.chinaunix.net/uid-22590270-id-3242503.html
工作队列,workqueue,它允许内核代码来请求在将来某个时间调用一个函数。用来处理不是很紧急事件
的回调方式处理方法.
工作队列的作用就是把工作推后,交由一个内核线程去执行,更直接的说就是如果您写了一个函数,而您
现在不想马上执行它,您想在将来某个时刻去执行它,那您用工作队列准没错
如果需要用一个可以重新调度的实体来执行你的下半部处理,也应该使用工作队列。它是唯一能在进程上
下文运行的下半部实现的机制,也只有它才可以睡眠。这意味着在需要获得大量的内存时、在需要获取信
号量时,在需要执行阻塞式的I/O操作时,它都会非常有用
可供选择的下半部机制
通过上节的讨论,我们知道下半部可以通过 5 种方式实现:BH 机制、任务队列、软中断、tasklet、工作队列。其中,前两种在 2.6 内核中已经看不到了,因此可供我们选择的其实就是后三种。
软中断和 tasklet 运行于中断上下文,工作队列靠内核线程实现,运行于进程上下文。因此,如果需要将任务推迟到进程上下文完成,工作队列是必然的选择,否则软中断和 tasklet 可能要更合适,因为使用工作队列时不可避免要带来到上下文切换的消耗。
相对于软中断而言,tasklet 对锁的要求要低得多。如果要使用下半部的模块代码本身对于保护考虑的不够好,那么选择 tasklet 就比较有必要了。
如果从易用性考虑,要首推工作队列,其次是 tasklet,最后才是软中断,因为它必须要静态的创建。
简单地说,关于下半部的使用,一般的驱动程序开发者需要做出的选择是:需要一个可调度的实体来执行需要推后完成的工作,即有休眠的需要吗?要是有,工作队列就是唯一的选择,否则最好用 tasklet,如果必须专注于性能的提高,那么就考虑软中断吧。