目录
中断下半页
软中断
Tasklet
Work Queues
回顾一下
Reference
(偷个懒:这里不将怎么用hhh)
我们现在开始考虑中断的部分了。我们直到处理设备(不管是CPU自己的还是外部设备的)的中断是操作系统的一个重要任务。
我们说中断是异步的——我们不知道何时会来中断:一旦来了,就会打断当前的执行上下文督促操作系统进行处理,同样的,还不可以滞留过久,否则将会影响用户的体验
注意到:我们更青睐于那些可以被中断的中断(Interrruptable Interrupt),也就是那些一旦发生了其他中断我们仍能接受并处理而不是丢失,比起来,那些屏蔽了中断的中断则很有可能会导致中断的丢失
以及我先说的是:中断运行在中断上下文!
中断下半页
我们提升用户体验的办法就是拆分中断处理的流程:接受中断(Interrupt Handlers)和处理中断(Bottom Halves),上半部分要求经可能的防止阻塞,将耗时的操作放到下半部分。一个可以遵循的准则是:
如果某些工作对时间非常敏感,必须马上处理,那么就放在上半部做;
如果某些工作和hardware直接相关,那么就放在上半部做;
如果某些工作要求不能同样的中断抢占,那么放在上半部做(因为上半部是关掉这个中断的);
其他的工作,考虑放到下半部来做。
在kernel比较早的的版本中,下半部是用的BH interface,这些interface在现在已经不用了。当时的实现,是有一个global的静态创建的32个下半部的list,上半部通过设置32bit中的一个,来决定哪个下半部执行,并且在系统中,同一个时间只能有一个下半部执行。这个我们现在不关心。我们关心的是比较新的Task Queue和Softirqs(软中断)和Taskle。
软中断
嗯,具备原子性:也就是说,排除真正的硬中断处理器句柄以外,软处理句柄是优先级最高的,他也只能被硬中断处理器句柄中断(遇到真货了)
通过NR_SOFTIRQS的定义,我们可以看到系统中会存在很多的softirq类型,每个softirq类型是否会被调用,取决于它是否被marked。通常来说,在interrupt handler返回之前,会把需要调用的softirq mark,这个过程称为raising softirq。这样在将来的某个时刻,kernel检查到某个softirq被mark,就会调用它。检查并调用的过程发生在:
-
在hardware interrupt code返回时。(也就是interrupt handler执行结束,要返回时)
-
在ksoftirqd线程中。
-
其他显示指明要检查并调用相应softirq的code中,比如network子系统。
Tasklet
/* Tasklets --- multithreaded analogue of BHs. Main feature differing them of generic softirqs: taskletis running only on one CPU simultaneously. Main feature differing them of BHs: different taskletsmay be run simultaneously on different CPUs. Properties:* If tasklet_schedule() is called, then tasklet is guaranteedto be executed on some cpu at least once after this.* If the tasklet is already scheduled, but its execution is still notstarted, it will be executed only once.* If this tasklet is already running on another CPU (or schedule is calledfrom tasklet itself), it is rescheduled for later.* Tasklet is strictly serialized wrt itself, but notwrt another tasklets. If client needs some intertask synchronization,he makes it with spinlocks.*/ struct tasklet_struct {struct tasklet_struct *next;unsigned long state;/*state是tasklet的状态,有以下几种值:TASKLET_STATE_SCHED表示tasklet已经被调度,准备运行TASKLET_STATE_RUN表示tasklet正在运行。*/atomic_t count; // count是tasklet的reference counter,如果不是0,说明tasklet被disable,不能被运行;如果是0,表示可以运行。void (*func)(unsigned long);unsigned long data; };
tasklet是在softirqs的基础上实现的,但是应用起来更简单。比起softirqs,大多数时候都是使用tasklet。非常简单,只要调用相应的函数即可。
两个tasklet不能同时concurrently;和softirqs一样,tasklet不能sleep。在其他中断运行的时候(因为是下页,其他中断当然可以运行)如果当前的下页和中断有share data,需要考虑同步的问题。
值得注意的是,softirqs是可以自我启动的,为了解决陷入自我循环中,浪费大量的系统资源。一种方式是不去处理这个softirqs的reactivated,直到它的interrupt handler再次出现才执行。另一种方式是,使用一个叫ksoftirqs的kernel thread去处理这些自启动,但是设定其优先级较低,使其在资源不紧张时调用。
被调度的tasklet存储在两个per CPU的数据结构中,这两个数据结构分别是:tasklet_vec和tasklet_hi_vec。这两个数据结构都是list,里面是等待被执行的tasklet,tasklet_vec中对应的是TASKLET_SOFTIRQ,tasklet_hi_vec对应的是HI_SOFTIRQ。前者通过tasklet_schedule来调度,后者通过tasklet_hi_schedule来调度,这两个函数的参数都是tasklet_struct的指针,并且功能类似
我们来看看Tasklets是如何被调度的:
首先检查tasklet的状态是不是TASKLET_STATE_SCHED,如果是那么实际上就是准备运行了直接返回
发起__tasklets_scheduled()
保存中断系统的状态然后禁用本地中断。这将会保证调度不会发生混乱
然后添加到调度的头区
然后发出一个Tasklet_softirq这样的话我们的do_softirq继续完成真正的处理工作
然后恢复状态返回
Work Queues
work queue和我们之前看到的所有bottom half都不一样,因为它是运行在process context中,也就是说它可以sleep,可以等I/O,可以分配memory,可以等待semaphore等等,使用work queue对下半部的实现几乎没有要求,kernel的绝大部分函数都能调用。
work queue也需要kernel来调度,并且调度的时间是不确定的。从这些方便看,使用work queue完全可以使用自己创建的kthread来代替,是的,kernel的实现也是通过per CPU的work thread来实现的,不过从device driver的角度看,直接使用work queue可以免去创建kthread和维护它的麻烦,稍微方便一些
回顾一下
选择哪一种下半部,取决于driver的需求,根据driver功能的不同,或者不同的特点,选择合适的下半部。
softirq,从它本身的性质看,比较适合高度并行化的工作,因为相同type的softirq可以同时运行在多个CPU上,高度并行化的工作可以充分利用softirq的这种性质;如果涉及到共享的数据,那就需要加锁保护,破坏了softirq并行化的机制。
tasklet,基于softirq,但是和softirq又有不同,最大的不同点在于相同的tasklet不支持同时运行在多个CPU上,这样对于device driver而言更加适合,因为tasklet handler往往需要处理数据,串行执行,可以保证driver在handler中不用加锁。
如果下半部只能运行在process context中,那么work queue就是唯一的选择了。work queue相较于softirq和tasklet来说,因为使用了kthread,所以涉及到context switch,导致性能不如softirq和tasklet。
总结来说,driver绝大部分情况只需要在tasklet和work queue之间做出选择:如果下半部需要在将来的某个时刻调用,或者其中会导致sleep,就使用work queue,否则使用tasklet。
Reference
[Bottom Halves and Deferring Work LKD 08] - gapofsky - 博客园 (cnblogs.com)