Linux中断和中断处理
众所周知,处理器的速度跟外围的硬件设备的速度往往不在一个数量级上,因此,如果内核采取让处理器发送一个请求,然后专门等待回应的办法,显然差强人意。既然硬件处理的这么慢,那么内核就应该在这期间去处理其他事务,等待硬件真正完成了请求的操作后,再回过头来对它进行处理。
轮询(polling)可能会是一种解决办法。它可以让内核定期对设备的状态进行查询,然后做出相应的处理。不过这种方法可能会让内核做不少不用功。更好的办法是提供一种机制,让硬件在需要的时候想内核发出信号,这就是中断机制。
中断
中断使得硬件得以发出通知给处理器。在你敲击键盘的时候,键盘控制器会发送一个中断,通知操作系统有键按下。中断本质上是一种特殊的电信号,有硬件设备向处理器发出。处理器收到中断后,会马上向操作系统反应此信号的到来,然后就由操作系统负责处理这些新到来的数据。硬件设备生成中断的时候并不考虑处理器的时钟同步---换句话说就是中断随时产生,因此,内核随时可能因为新到来的中断而被打断。
中断处理程序
在相应一个特定中断的时候,内核会执行一个函数,该函数叫作中断处理程序或中断服务例程(interrupt service routine,ISR)。产生中断的每个设备都有一个相应的中断处理程序。例如:由一个函数专门处理来自系统时钟的中断,而另外一个函数专门处理键盘产生的中断。一个设备的中断程序时它设备驱动程序的一部分---设备驱动程序是用于对设备进行管理的内核代码。中断处理程序负责通知硬件设备中断已经被接收。同时中断程序还要处理其他工作,比如要把来自硬件的网络数据包拷贝到内存,对其进行处理后再交给合适的协议栈或应用程序。
上半部与下半部的对比
又想中断处理程序运行的快,又想中断程序完成的工作多,这两个目的显然有所抵触。鉴于两个目的之间存在的此消彼长的矛盾关系,所以我们一般把处理切为两半部分执行。中断时上半部分(top half)---接收到一个中断,它就立即执行,但是做有严格时限的工作,例如对接收的中断进行应答或者复位硬件,这些工作都是所有中断被禁止的情况下完成的。能够被允许稍后完成的工作会推迟到下半部(bottom half)去。此后,在合适的时机,下半部会被中断执行。
中断上下文
当执行一个中断处理程序时,内核处于中断上下文(interrput context)中。进程上下文是一种内核所处的操作模式,此时内核代表进程执行---例如,执行系统调用或运行内核线程。在进程上下文中,可以通过current宏关联当前进程。此外,因为进程是以进程上下文的形势连接到内核中的,因此,进程上下文可以睡眠,也可以调用调度程序。
与之相反,中断上下文和进程并没有什么瓜葛。与current宏也是不相干的。因为没有后备进程,所以中断上下文不可以休眠,否则又怎能再对它重新调度呢?,因此,不能从中断上下文中调用某些函数。如果一个函数睡眠,就不能在你的中断处理程序中使用它---这是对什么样的函数可以在中断成立程序中使用的限制。
中断上下文具有较为严格的时间限制。因为它打断了其他代码。中断上下文的代码应当迅速,简洁,尽量不要使用循环去处理繁重的工作。尽量把工作从中断处理程序中分离出来,放在下半部来执行,因为下半部可以在更合适的时间运行。
中断处理程序之前并不具有自己的栈,他们共享所有进程的内核栈,内核栈的大小是两页,在32位的体系结构上是8KB,在64位体系结构上是16KB。因为在这种设置中,中断处理共享别人的堆栈。2.6早期版本的内核中,增加了一个选项,把栈的大小从两页减小到一页。也就是32位系统只提供4KB的栈。这就减轻了内存的压力,因为系统中每个进程原先都需要两页连续,且不可换出的内核内存。为了应对栈大小的压力,中断处理程序拥有了自己的栈。
中断处理机制的实现
中断处理系统在Linux中的实现是非常依赖体系结构的,实现依赖于处理器、所使用的中断控制器的类型、体系结构的设计以及机器本身。
下图是中断从硬件到内核的路由,设备产生中断,通过总线把电信号发送给中断控制器,如果中断线是激活的(他们是允许被屏蔽的),那么中断控制器就会把中断发往处理器。在大多数体系结构中,这个工作就是通过电信号给处理器的特定管脚发送一个信号。除非在处理器上禁止该中断,否则,处理器会立即停止它正在做的事情,关闭中断系统,然后调到内存中预定义的位置开始执行那里的代码。这个预定义的位置是由内核设置的,是中断处理程序的入口点。
中断控制
Linux内核提供了一组接口用于操作机器上的中断状态。这些接口为我们提供了能够禁止当前处理器的中断系统,或屏蔽掉整个机器的一条中断线的能力,这些例子都是与体系结构相关的。
控制中断系统的原因归根结底是需要提供同步。通过禁止中断,可以确保某个中断处理程序不会抢占当前的代码。此外,禁止中断还可以禁止内核抢占。然而,不管是禁止中断还是禁止内核抢占,都没有提供任何的保护机制来防止来自其他处理器的并发访问。Linux支持多处理器,因此,内核代码一般需要获取某种锁,防止来自其他处理器对共享数据的并发访问。获取这些锁的同时也伴随着禁止本地中断。锁提供保护机制,防止来自其他处理器的并发访问,而禁止中断提供提供保护机制,则是防止来自其他中断程序并发访问。
下半部和退后执行的工作
中断处理程序时内核中很有用的部分,但是由于本身存在的一些局限,所以它只能完成整个中断处理流程的上半部分,这些局限包括:
- 中断处理程序以一部方式执行,并且它有可能会打断其他重要代码(设置包括其他中断处理程序)的执行,因此为了避免被打断的代码停止时间过长,中断处理程序应该执行的越快越好。
- 如果当前有一个中断处理程序正在执行,在最好的情况下,与该中断同级的其他中断会被屏蔽,在最坏情况下,当前处理器上所有其他中断都会被屏蔽。因此禁止中断后硬件与操作系统无法通信,因此中断处理程序执行的愉快越好。
- 由于中断处理程序往往需要对硬件进行操作,所以他们通常有很高的时限要求。
- 中断处理程序不在进程上下文中运行,所以他们不能阻塞,这限制了他们所做的事情。
现在,为什么中断处理程序只能作为整个硬件处理流程一部分的原因就很明显了,操作系统必须有一个快速、异步、简单的机制对硬件作出迅速响应并完成那些时间要求严格的操作。中断处理程序很适合于实现这些功能,可是对于那些其他的、对时间要求相对宽松的任务,就应该推后到中断被激活以后再去运行。
这样,整个中断处理流程就被分为两个部分,或叫两半。第一部分是中断处理程序(上半部),内核通过对它的异步执行完成对硬件中断的即时相应。
下半部
下半部的任务就是执行与中断处理密切相关但中断处理程序本身不执行的工作。在理想情况下,最好是中断处理程序将所有工作都交给下半部执行,因为我们希望给在中断处理程序中完成的工作越少越好,我们期望中断处理程序能够尽快地返回。
但是,中断处理程序注定要完成一部分工作。例如:中断处理程序机会都需要通过操作硬件对中断的到达进行确认,有时它还会从硬件拷贝数据。因为这些工作对时间非常敏感,所以只能靠中断处理程序自己去完成。
剩下的几乎所有其他工作都是下半部执行的目标。例如:如果你在上半部中把数据从硬件拷贝到了内容,那么当然应该在下半部中处理他们。遗憾的是,并不存在严格明确的规定来说明到底什么任务应该在哪个部分中完成--如何做决定完全取决于驱动程序开发者自己的判断。尽管在理论上不存在什么错误,但轻率的实现效果往往不很理想。中断处理程序会异步执行,并且在最好的情况下它也会锁定当前的中断线程。因此将中断处理程序持续执行的时间缩短到最小程度显然非常重要。对于上半部和下半部之间划分工作,尽管不存在某种严格的规则,但还是有一些提示可以借鉴:
- 如果一个任务对时间非常敏感,将其放在中断处理程序中执行。
- 如果一个任务和硬件相关,将其放在中断处理程序中执行。
- 如果一个任务要保证不被其他中断(特别是相同的中断)打断,将其放在中断处理程序中执行。
- 其他所有任务,考虑放置在下半部执行。
和上半部只能通过中断程序实现不同,下半部可以通过多种机制实现:工作队列、软中断、tasklet。
软中断
软中断是执行中断指令产生的,而硬中断是由外设引发的。软中断使用的比较少,而tasklet是下半部更常用的一种形式,tasklet是通过软中断实现的。软中断是在编译期间静态分配的,他不能向tasklet那样能被动态的注册或注销。
目前只有两个子系统直接使用软中断网络和SCSI.
tasklet
tasklet是利用软中断实现的一种下半部机制。选择到底用软中断还是tasklet其实很简单:通常应该用tasklet,它与软中断本质上很相似,行为表现也相近,但是接口更简单,锁保护也要求低。
工作队列
工作队列(work queue)是另外一种将工作退后执行的形式,它和我们前面讨论的形式不同。工作队列把工作推后,交给一个内核线程去执行---这个下半部分总是会在进程上下文中执行。这样,通过工作队列执行的代码能占尽进程上下文的所有优势。最重要的是工作队列允许重新调度和睡眠。
通常在工作队列和软中断/tasklet中做出选择很容易,如果要推后执行的任务需要睡眠,那么就选择工作队列。如果推后执行的任务不需要睡眠,那么就选择软中断或tasklet。
如果需要用一个可以重新调度的实体来执行下半部处理,应该选择使用工作队列,他是唯一能在进程上下文中运行的下半部机制,也只有它可以睡眠,这意味着需要获得大量内存是、需要获取信号量时、需要执行阻塞I/O操作时,它会非常有用。
下半部机制的选择
各种不同的下半部实现机制之间做出选择是很重要的。一般的驱动程序开发者需要做出两个选择,首先是你虚部一个可调度的实体来执行推后的工作或者说需要休眠吗?要是有,工作队列就是唯一选择,否则就是用tasklet。如果专注性能提高就考虑软中断。
推荐阅读