第8章 下半部和推后执行的工作
内核为处理中断而提供的中断处理程序机制。中断处理程序是内核中很有用的部分。但是,由于本身存在一些局限,所以它只能完成整个中断处理流程的上半部分。这些局限包括:
中断处理程序以异步方式执行,并且有可能会打断其他重要代码的执行。因此,为了避免被打断的代码停止时间过长,中断处理程序应该执行越快越好。
如果当前有一个中断处理程序正在执行,在最好的情况下(如果设置了IRQF_DISABLED),与该中断同级的其他中断会被屏蔽,在最坏的情况下(如果设置了IRQF_DISABLED),当前处理器上所有其他中断都会被屏蔽。因为禁止中断后硬件与操作系统无法通信,因此,中断处理程序执行得越快越好。
由于中断处理程序需要对硬件进行操作,所以它们通常有很高的时限要求。
中断处理程序不在进程上下文中运行,所以它们不能阻塞。
中断处理程序只能作为整个硬件中断处理流程的一部分。操作系统必须有一个快速、异步、简单的机制负责对硬件做出迅速响应并完成时间要求很严格的操作。中断处理程序适合于实现这些功能,可是,对于那些其他的、对时间要求相对宽松的任务,就应该推后到中断被**以后再去运行。
这样,整个中断处理流程就被分为了两个部分。第一个部分是中断处理程序(上半部),内核通过对它的异步执行完成对硬件中断的即时响应。下面研究中断处理流程中的另外那一部分,下半部。
8.1 下半部
下半部的任务就是执行与中断处理密切相关但中断处理程序本身不执行的工作。在理想的情况下,最好是中断处理程序将所有工作都交给下半部分执行,因为希望在中断处理程序中完成的工作越少越好。期望中断处理程序能够尽可能快地返回。
但是,中断处理程序要完成一部分工作。例如,中断处理程序几乎都需要通过操作硬件对中断的到达进行确认,有时它还会从硬件拷贝数据。因为这些工作对时间非常敏感,所以只能靠中断处理程序自己去完成。
剩下的几乎所有其他工作都是下半部执行的目标。例如,如果在上半部中把数据从硬件拷贝到了内存,那么当然应该在下半部中处理它们。并不存在严格明确的规定来说明到底什么任务应该在哪个部分中完成——如何做决定完全取决于驱动程序开发者自己的判断。尽管在理论上不存在什么错误,但轻率的实现效果往往不很理想。记住,中断处理程序会异步执行,并且在最好的情况下它也会锁定当前的中断线。因此将中断处理程序持续执行的时间缩短到最小程度显得非常重要。对于在上半部和下半部之间划分工作,尽管不存在某种严格的规则,但还是有一些提示可供借鉴:
如果一个任务对时间非常敏感,将其放在中断处理程序中运行。
如果一个任务和硬件有关,将其放在中断处理程序中运行。
如果一个任务要保证不被其他中断打断,将其放在中断处理程序中运行。
其他所有任务,考虑放置在下半部执行。
通常,中断处理程序要执行得越快越好。
8.1.1 为什么要用下半部
理解为什么要让工作推后执行以及在什么时候推后执行非常关键。希望尽量减少中断处理程序中需要完成的工作量,因为它在运行时,当前的中断线在所有处理器上都会被屏蔽。更糟糕的是,如果一个处理程序是IRQF_DISABLED类型,它执行时会禁止所有本地中断。而缩短中断被屏蔽的时间对系统的响应能力和性能都至关重要。再加上中断处理程序要与其他程序异步执行,所以很明显,必须尽力缩短中断处理程序的执行。解决的方法就是把一些工作放到以后去做。
但具体放到以后什么时候去做呢?理解这一点相当重要。下半部并不需要指明一个确切时间,只要把这些任务推迟一点,在系统不太繁忙并且中断恢复后执行就可以了。通常下半部在中断处理程序一返回就会马上执行。下半部执行的关键在于当它们运行的时候,运行响应所有的中断。
不仅仅是Linux,许多操作系统也把处理硬件中断的过程分为两个部分。上半部分简单快速,执行的时候禁止一些或者全部中断。下半部分稍后执行,而且执行期间可以响应所有的中断。这种设计可使系统处于中断屏蔽状态的时间尽可能的短,依次来提高系统的响应能力。
8.1.2 下半部的环境
和上半部只能通过中断处理程序实现不同,下半部可通过多种机制实现。这些用来实现下半部的机制分别由不同的接口和子系统组成。实现中断处理程序的方法只有一种,实现一个下半部会有许多不同的方法。
本章讨论2.6版本的内核中的下半部机制是如何设计和实现的。
1、下半部的起源
最早的Linux只提供“bottom half”这种机制用于实现下半部。这种机制也被称为“BH”,BH接口也非常简单。它提供了一个静态创建、由32个bottom halves组成的链表。上半部通过一个32位整数中的一位来标识出哪个bottom half可以执行。每个BH都在全局范围内进行同步。即使分属于不同的处理器,也不允许两个bottom half同时执行。这种机制使用方便却不够灵活,简单却有性能瓶颈。
2、任务队列
内核开发者引入任务队列机制来实现工作的推后执行,并用它来代替BH机制。内核为此定义了一组队列,其中每个队列都包含一个由等待调用的函数组成链表。根据其所处队列的位置,这些函数会在某个时刻执行。驱动程序可以把它们自己的下半部注册到合适的队列上去。这种机制表现得还不错,但仍不够灵活,无法代替整个BH接口。对于一些性能要求较高的子系统,例如网络部分,它也不能胜任。
3、软中断和tasklet
在2.3这个开发版本中,内核开发者引入了软中断和tasklet。如果无须考虑和过去开发的驱动程序兼容的话,软中断和tasklet可以完全代替BH接口。软中断是一组静态定义的下半部接口,有32个,可以在所有处理器上同时执行——即使两个类型相同也可以。tasklet是一种基于软中断实现的灵活性强、动态创建的下半部实现机制。两个不同类型的tasklet可以在不同的处理器上同时执行,但类型相同的tasklet不能同时执行。tasklet其实是一种在性能和易用性之间寻求平衡的产物。对于大部分下半部处理来说,用tasklet就足够了,像网络这样对性能要求非常高的情况下才需要使用软中断。使用软中断需要特别小心,因为两个相同的软中断有可能同时被执行。软中断还必须在编译期间就进行静态注册。相反,tasklet可以通过代码进行动态注册。
在2.6版本的内核中,内核提供了三种不同形式的下半部实现机制:软中断、tasklet和工作队列。
内核定时器
另外一个可以用于将工作推后执行的机制是内核定时器。内核定时器把操作推迟到某个确定的时间段之后执行。但是当必须保证在一个确定的时间段过去以后再运行时,应该使用内核定时器。
4、混乱的下半部概念
当前,有三种机制可以用来实现将工作推后执行:软中断、tasklet和工作队列。tasklet通过软中断实现,而工作队列与它们完全不同。下半部机制的演化例程。