本篇专门讲述中断以及围绕其产生的一系列问题。
我们说中断处理程序运行在中断上下文中,当然包括软中断在内的各种中断下半部也是。它们的共同特点是,内核工作于中断上下文时,不代表某一进程;这与以异常处理和系统调用为代表的进程上下文区分开来。但是,中断上下文和进程上下文都位于内核态。
这两种上下文的重要差异决定了一些关键注意事项,例如因为不代表进程执行,没有属于自己的PCB(但有属于自己的内核栈),中断上下文内禁止阻塞,所以开发者要对调用的函数是否可能阻塞有清楚的认识。又如,中断上下文内仍可以使用current宏,但它代表的是被打断的进程,由中断的异步性我们知道该进程和当前处理的中断没有必然联系。
禁止睡眠,也意味着禁止调度,因为调度伴随着时钟中断和抢占,被抢占的线程即进入等待队列,开始睡眠。不过既然说中断上下文有自己的内核栈了(而不是借用被打断进程的内核栈),那岂不是可以在必要的断点保存之后就允许响应中断(即嵌套中断)?理论上是可以的,实际上Linux也做过(即调用中断处理程序时不设IRQF_DISABLED标志),但这个功能已经完全废弃,甚至像BKL一样彻底消失在历史的长河中。具体而言,是因为多队列的网卡(几乎)同时向同一个CPU发起中断时会爆栈,详见https://git.kernel.org/pub/scm/linux/kernel/git/torvalds/linux.git/commit/?id=e58aa3d2d0cc。不过即便是在允许嵌套中断的过去,中断处理程序也不需要设计成可重入的,因为中断处理程序执行时,对应的中断线会在所有处理器上禁止。
同时,中断上下文也禁止使用可能导致睡眠的互斥体和信号量——位于中断上下文的执行流没有再被调度的机会,最终将产生不一致性。要是在中断处理程序中(或者任何已经关中断的情况下)直接睡眠的后果就更可怕了,因为关中断就是全关,意味着时钟中断也无法响应,CPU会直接挂死在调度的进程上。但是自旋锁、读写锁等当然是可以用的。不过既然有需要锁保护的资源,那就有该资源会被谁竞争的问题,假如进程上下文也需要竞争该资源,那么进程上下文为它上锁时一定要关闭中断,否则如果此时中断到来争用这把锁,死锁就产生了。对于中断下半部,有类似的结论。
然而,在进程上下文中关中断时,仍可以用信号量等可睡眠锁机制,因为内核的信号量实现保证在开始调度之前启用中断。
虽然硬中断执行期间关闭中断,但下半部允许,而且应当支持中断。软中断允许被打断,但不能嵌套执行,因为完全没必要嵌套,下半部本来就没有那么着急;此外,也不能被调度。但考虑到可扩展性,在多处理器上同一软中断是可以并行执行的,因此有必要将其设计得可重入,不过可以通过使用单处理器数据来避免显式加锁。一般情况下,还是尽量考虑使用tasklet作为下半部机制吧,它基于软中断并在软中断处理程序中被调用,但保证了在多处理器系统上不会同时运行(当然,更全面地说,是不会并发运行)。至于工作队列,它由内核线程实现,所以位于进程上下文(不过没有对应的用户空间映射),不仅能被中断,还能被调度。
到现在我们已经理解,可中断和可调度是两码事。中断是异步产生的,不意味着一定会产生调度(但产生调度的话一定是抢占式调度);调度可能在时钟中断时发生,也可能是在睡眠、阻塞或主动让出处理器时发生。进程上下文一般情况下两者都可;软中断和tasklet一般情况下可中断但不可调度;中断处理程序在Linux上已经整个不可中断不可调度。一般关中断就意味着线程不能被抢占,而线程发生非抢占调度前也没为此做太多准备(例如定时唤醒机制,简单地设一个定时器然后就yield了),即不能被抢占的线程也就不能进行非抢占调度,所以不太可能有不可中断但可调度的程序。(注意可中断和可调度均是笔者为便于理解而述,不意味着有这样的专有名词存在,也不意味着笔者的理解正确)
在编写可中断的内核代码时,需要问一些问题:该代码是否会在自身处理器上并发访问?是否会在多处理器上同时执行?特别地,中断处理程序是一个潜在的竞争源,如果和它共用了资源,就得上锁,而且还不是一般的上锁,要先关中断再锁。编写不可中断的内核代码时,虽然不会在自身所在的处理器并发执行了,但仍需要考虑是否会在多处理器同时执行;以及,保持清醒,千万不能打盹,否则就会长眠不起。
用锁也有不少值得考虑的事情。例如,使用自旋锁的时候不能获取互斥体或down信号量,更是万万忌讳直接拱手送出处理器。首先这种操作是极其不礼貌的,这就好比占着茅坑不拉屎,外面的人憋着不能拉,你倒是把门反锁上在里面睡起觉来了,这期间别人什么事情都做不了(还会占用处理器);更重要的是,它们都破坏了内核关抢占的保护作用,让别的代码(也有可能是另外一个自己)在同一处理器上运行,不仅违背了自旋锁速战速决的初衷,而且分分钟会弄出死锁——同时带走一个处理器。此外虽然内核在进程使用自旋锁时关抢占,但中断还是会如期发生的,我们还需要担心和中断处理程序发生死锁的可能性,即必要时关中断。
参考资料: