1.中断
中断本质上是一种特殊的电信号,由硬件设备发向处理器。处理器接收到中断后,会马上向OS反映此信号的到来,然后就由OS负责处理这些新到来的数据。硬件设备生成中断的时候并不考虑与处理器的时钟同步--换句话说就是中断随时可以产生。因此,内核随时可能因为新到来的中断而被打断。
不同的设备对应的中断不同,而每个中断都通过一个唯一的数字标志。这些中断值通常被称为中断请求(IRQ)线。每个IRQ线都会被关联一个数值量。
2.中断处理程序
在响应一个特定中断的时候,内核会执行一个函数,该函数叫做中断处理程序或中断服务例程。产生中断的每个设备都有一个相应的中断处理程序。一个设备的中断处理程序是它设备驱动程序的一部分--设备驱动程序是用于对设备进行管理的内核代码。
中断处理程序需要负责通知硬件设备中断已经被接收。但是中断处理程序往往还要完成大量其他的工作。
注意:一个中断对应一个中断处理程序,而不是一个中断线对应一个中断处理程序。一个中断线可以有多个中断来共享,一个中断处理程序也可以对应多个中断线(比如型号相同的多张硬盘)。
3.上半部与下半部的对比
又想中断处理程序运行得快,又想中断处理程序完成的工作量多,这两个目的显然有所抵触。鉴于两个目的之间存在此消彼长的矛盾关系,所以我们一般把中断处理切为两个部分。中断程序是上半部(top half)--接收到一个中断,它就立即开始执行,但只做有严格时限的工作。能够被允许稍后完成的工作会被推迟到下半部(bottom half)去。此后,在合适的时机,下半部就会被开中断执行。
举例:当网卡接收来自网络的数据包时,需要通知内核数据包到了。网卡需要立即完成这件事,否则缓存容易溢出。内核通过执行网卡已注册的中断处理程序来做出应答。中断开始执行,通知硬件,拷贝最新的网络数组包到内存,然后读取网卡更多的数据包。处理和操作数据包的其他工作在随后的下半部中进行。
4.注册中断处理程序。
request_irq()可以用于注册一个中断处理程序,并且激活给定的中断线。request_irq()函数可能会睡眠,因此,不能在中断上下文或其他不允许阻塞的代码中调用该函数。其会引起阻塞的原因是调用了kmalloc()来请求分配内存,而kmalloc()是可以睡眠的。
free_irq()用于卸载驱动程序,需要注销相应的中断处理程序,并释放中断线。
5.编写中断处理程序
调用intr_handler()进行中断处理程序声明。
5.1. 重入和中断处理程序
linux中的中断处理程序是无须重入的。当一个给定的中断处理程序正在执行时,相应的中断线在所有处理器上都会被屏蔽掉,以防止在同一中断线上接收另一个新的中断。通常情况下,所有其他的中断都是打开的,所以这些不同中断线上的其他中断都能被处理,但当前中断线总是被禁止的。由此可以看出,同一个中断处理程序绝对不会被同时调用以处理嵌套。
5.2 共享的中断处理程序。
如果多个中断处理程序共享一根中断线的话,那么,在内核接收到一个中断后,它将一次调用在该中断线上注册的每一个处理程序。因此,一个初例程序必须知道它是否该为这个中断负责。如果与它相关的设备并没有产生中断,那么处理程序应该立即退出。这需要硬件设备提供状态寄存器(或类似机制),以便中断处理程序进行检查。
6 中断上下文
当执行一个中断处理程序时,内核处理中断上下文中。回忆一下,进程上下文是一种内核所处的操作模式,此时内核代表进程执行--例如,执行系统调用或运行内核线程。在进程上下文中,可以通过current宏关联当前进程。此外,因为进程是以进程上下文的形式连接到内核中的,因此,进程上下文可以睡眠,也可以调用调度程序。
与之相反,中断上下文和进程并没有什么瓜葛。因为没有后备进程,所以中断上下文不可以睡眠。如果一个函数睡眠,就不能再中断处理程序中使用它--这是对什么样的函数可以在中断处理程序中使用的限制。
中断处理程序打断了其他的代码(甚至可能是打断了在其他中断线上的另一中断处理程序)。正是因为这种异步执行的特征,所以所有的中断处理程序必须尽可能的迅速,简洁。尽量把工作从中断处理程序中分离出来,放在下半部来执行,因为下半部可以在更适合的时间运行。
老版本的内核中,中断处理程序并不具备自己的栈,而是共享所有中断进程的内核栈。2.6版本中,把进程栈的大小从两页减到一页,也就是在32位的系统上只提供4KB的栈。这就减轻了内存的压力。为了应对栈大小的减少,中断处理程序拥有了自己的栈,每个处理器一个,大小为一页。这个栈就称为中断栈。
7.中断处理机制的实现
中断从硬件到内核的路由。
linux中/proc/interrupts文件存放的是系统中与中断相关的统计信息。
8.中断控制
linux内核提供了一组接口用于操作机器上的中断状态。这些接口为我们提供了能够禁止当前处理器的中断系统,或屏蔽掉整个机器的一条中断线的能力,这些例程都是与体系结构有关的。
一般来说,控制中断系统的原因归根结底在于需要提供同步。通过禁止中断,可以确保某个中断处理程序不会抢占当前的代码。此外,禁止中断还可以禁止内核抢占。然后,不管是禁止中断还是禁止内核抢占,都没有提供任何保护机制来防止来自其他处理器的并发访问。Linux支持多处理器,因此,内核代码一般都需要获取某种锁,防止来自其他处理器对共享数据的并发访问。获取这些锁的同时也伴着禁止本地中断。锁提供保护机制,防止来自其他处理器的并发访问,而禁止中断提供保护机制,则是防止来自其他中断处理程序的并发访问。
8.1 禁止和激活中断
代码:
local_irq_disable();
/*禁止中断*/
local_irq_enable();
在x86下,前者对应cli,后者对应sti。cli和sti分别是对clear和set允许中断标志的汇编调用。换句话说,在发出中断的处理器上,它们禁止和激活中断的传递。
然后,在禁止中断之前保存中断系统的状态会更加安全一些。相反,在准备激活中断时,只需要把中断恢复到它们原来的状态:
unsigned long flags;
local_irq_save(flags); /*禁止中断*/
/* ... */
local_irq_restore(flags); /*中断被恢复到它们原来的状态*/
以前的内核中提供了一种“能够禁止系统中所有处理器上的中断”方法。而且,如果另一个处理器调用这个方法,那么它就不得不等待,知道中断重新被激活才能继续执行。这个函数就是cli(),相应的激活中断函数为sti()。在2.5版本之后,这些接口被取消了,相反,所有的中断同步现在必须结合使用本地中断控制和自旋锁。这就意味着,为了确保对共享数据的互斥访问,以前代码仅仅需要通过全局禁止中断达到互斥,而现在则需要多做些工作了。取消全局cli()有不少优点。首先,强制驱动程序编写者实现真正的加锁。要知道具有特定目的细粒度锁比全局锁要快许多,而且也完全吻合cli()的使用初衷。其次,这也使得很多代码更具流线型,避免了代码的成簇布局。
8.2 禁止指定中断线
为了屏蔽掉一条中断线,Linux提供了四个接口:
void disable_irq(unsigned int irq);
void disable_irq_nosync(unsigned int irq);
void enable_irq(unsigned int irq);
void synchronize_irq(unsigned int irq);
前两个函数禁止中断控制器上指定的中断线,即禁止给定中断向系统中所有处理器的传递。另外,函数只有在当前正在执行的所有处理程序完毕后,disable_irq()才能返回。函数disable_irq_nosync()不会等待当前中断处理程序执行完毕。
函数synchronize_irq()等待一个特定的中断处理程序的退出。
对这些函数的调用可以嵌套。但要记住在一条指定的中断线上,对disable_irq()或disable_irq_nosync()的每次调用,都需要相应的调用一次enable_irq()。
所有这三个函数可以从中断或进程上下文调用,而且不会睡眠。
8.3 中断系统的状态
另外有几个函数用于查询中断系统的状态,分别是:
irqs_disable():如果本地处理器上的中断系统被禁止,则它返回非0,否则返回0。
另外还有两个宏用于检查内核的当前上下文的接口:in_interrupt()和in_irq()。
本文所提到的中断控制方法的列表:
参考:《Linux内核设计与实现》
问:Linux内核中进程上下文与中断上下文的区别
Linux内核工作在进程上下文或中断上下文。提供系统调用服务的内核代码代表发起系统调用的应用程序运行在进程上下文;另一方面,中断处理程序,异步运行在中断上下文。中断上下文和特定的金城武管。
进程上下文主要是异常处理程序和内核线程,是可抢占的。
中断上下文是内核因为中断信号而导致的中断处理或软中断(不是软件中断),它是不能抢占的。
问:系统调用与异常,软件中断,外部中断的区别
中断包括以下三个部分:
- 异常:由计算机硬件异常或故障引起的中断。
- 软件中断(也称软中断,但不同于中断之后下半部的软中断):由程序中执行引起了中断的指令而引起中断。
- 外部中断:由外部设备请求引起的中断。
其中系统调用属于第二个:软件中断。
问:中断处理程序可否被抢占
是可以的,低优先级的中断处理程序执行时,另一个高优先级中断响应,且没有被屏蔽掉,则让出内核给高优先级中断处理程序先执行。