中断
中断是指计算机运行过程中,出现某些意外情况需主机干预时,机器能自动停止正在运行的程序并转入处理新情况的程序,处理完毕后又返回原被暂停的程序继续运行。
中断使得硬件得以发出通知给处理器。中断使得硬件得以发出通知给处理器。例如,在敲击键盘时,键盘控制器(控制键盘的硬件设备)会发送一个中断,通知操作系统有键按下。中断本质上是一种特殊的电信号,由硬件设备发向处理器。处理器接收到中断后,会马上向操作系统反映此信号的到来,然后就由操作系统负责处理这些新到来的数据。硬件设备生成中断的时候并不考虑与处理器的时钟同步——中断随时可以产生。因此,内核随时可能因为新到来的中断而被打断。
从物理学的角度看,中断是一种电信号,由硬件设备生成,并直接送入中断控制器的输入引脚中——中断控制器是个简单的电子芯片,其作用是将多路中断管线,采用复用技术只通过一个和处理器相连接的管线与处理器通信。当接收到中断后,中断控制器会给处理器发送一个电信号。处理器一经检测到此信号,便中断自己的当前工作转而处理中断。此后,处理器会通知操作系统已经产生中断,这样,操作系统就可以对这个中断进行适当地处理了。
不同的设备对应的中断不同,而每个中断都通过一个唯一的数字标志。因此,来自键盘的中断就有别于来自硬盘的中断,从而使得操作系统能够对中断进行区分,并知道哪个硬件设备产生了哪个中断。这样,操作系统才能给不同的中断提供对应的中断处理程序。 这些中断值通常被称为中断请求(IRQ)线。每个IRQ线都会被关联一个数值量——例如,在PC机上,IRQ 0是时钟中断,而IRQ 1是键盘中断。但并非所有的中断号都是这样严格定义的。例如,对于连接在PCI总线上的设备而言,中断是动态分配的。而且其他非PC的体系结构也具有动态分配可用中断的特性。重点在于特定的中断总是与特定的设备相关联,并且内核要知道这些消息。
中断——异步的:由硬件随机产生,在程序执行的任何时候可能出现
异常——同步的:在(特殊的或出错的)指令执行时由CPU控制单元产生
我们用“中断信号”来通称这两种类型的中断
中断分为:
可屏蔽中断(Maskable interrupt) I/O设备发出的所有中断请求(IRQ)都可以产生可屏蔽中断。可屏蔽中断可以处于两种状态:屏蔽的(masked)和非屏蔽的(unmasked)
非屏蔽中断(Nonmaskable interrupt) 只有几个特定的危急事件才引起非屏蔽中断。如硬件故障或是掉电
异常分为:
处理器探测异常 由CPU执行指令时探测到一个反常条件时产生,如溢出、除0错等
编程异常 由编程者发出的特定请求产生,通常由int类指令触发;也叫做“软中断”:例如系统调用
中断信号
中断信号的作用
中断信号提供了一种特殊的方式,使得CPU转去运行正常程序之外的代码
当一个中断信号到达时,CPU必须停止它当前正在做的事,并且切换到一个新的活动
在进程的内核态堆栈保存程序计数器的当前值(即eip和cs寄存器)以便处理完中断的时候能正确返回到中断点
并把与中断信号相关的一个地址放入进程序计数器,从而进入中断的处理
中断信号的处理原则
内核的目标就是让中断尽可能快的处理完,尽其所能把更多的处理向后推迟
允许不同类型中断的嵌套发生,这样能使更多的I/O设备处于忙状态
尽管内核在处理一个中断时可以接受一个新的中断,但在内核代码中还在存在一些临界区,在临界区中,中断必须被禁止
中断向量
每个中断和异常由0~255之间的一个数(8位)来标识,Intel称其为中断向量。
非屏蔽中断的向量和异常的向量是固定的
可屏蔽中断的向量可以通过对中断控制器的编程来改变
中断的产生
每个能够发出中断请求的硬件设备控制器都有一条称为IRQ(Interrupt ReQuest)的输出线。所有的IRQ线都与一个中断控制器的输入引脚相连。中断控制器与CPU的INTR引脚相连
中断控制器执行下列动作
1.监视IRQ线,对引发信号检查
2.如果一个引发信号出现在IRQ线上
- 1.把此信号转换成对应的中断向量
- 2.把这个向量存放在中断控制器的一个I/O端口,从而允许CPU通过数据总线读这个向量
- 3.把引发信号发送到处理器的INTR引脚,即产生一个中断
- 4.等待,直到CPU应答这个信号;收到应答后,清INTR引脚
3.返回到第一步(a)
中断处理程序
在响应一个特定中断的时候,内核会执行一个函数,该函数叫做中断处理程序或中断服务例程。产生中断的每个设备都有一个相应的中断处理程序。中断处理程序与其他内核函数的真正区别在于,中断处理程序是被内核调用来响应中端的,而他们运行在我们称之为中断上下文的特殊上下文中,中断上下文也被成为原子上下文,该上下文执行的代码不可阻塞。
中断可能随时发生,因此中断处理程序也就随时可能执行。所以必须保证中断处理程序能够快速执行,这样才能保证尽可能快地恢复中断代码的执行。
上半部和下半部的对比
由于中断处理程序既需要运行的快,又需要完成的工作量多,所以将中断处理切分为两部分。中断处理程序是上半部,一旦接收到一个中断,他就立即开始执行,但只做有严格时限的工作,例如对接收的中断进行应答或复位硬件能够被允许稍后执行的工作会推迟到下半部去。此后,在合适的时机,下半部会被中断执行。比如对接收的中断进行应答。
注册中断处理程序
中断处理程序是管理硬件的驱动程序的组成部分。每一个设备都有相关的驱动程序,如果设备使用中断,那么相应的驱动程序就注册了一个中断处理程序。
驱动程序可以通过request_irp()函数注册一个中断处理程序,该函数被定义在linux/interrupt.h文件中,并且激活给定的中断线,以处理中断。
int request_irq(unsigned int irq,irq_handler_t handler,
unsigned long flags,const char *name,void *dev)
第一个参数表示要分配的中断号。对于大多数其他设备来说,中断号要么可以通过探测获取,要么可以通过编程动态确定。
第二个参数handler是一个指针,指向处理这个中断的实际中断处理程序。只要操作系统一接收到中断,该函数就被调用。 handler函数的原型
typedef irqreturn_t (*irq_handler_t)(int, void *);
第三个参数flags可以为0,也可能是下列一个或多个标志的位掩码。定义在linux/interrupt.h文件中。下面列举一下几个比较重要的标志:
IRQF_DISABLE - 该标志被设置后,意味着内核在处理中断处理程序本身期间,要禁止所有的其他中断。如果不设置,中断处理程序可以与除本身以外的其他任何中断同时运行。
IRQF_SAMPLE_RANDOM - 此标志表明这个设备产生的中断对内核熵池(entroy pool)有贡献。内核熵池负责提供从各种随机事件导出真正的随机数。如果指定了该标志,那么来自该设备的中断间隔时间就会作为熵填充到熵池。 I
RQF_TIMER - 该标志是特别为系统定时器的中断处理准备的。
IRQF_SHARED - 该标志表明可以在多个中断处理程序之间共享中断线。在同一个中断线上注册的每个处理程序必须指定这个标志,否则,在每条线上只能有一个处理程序。
第四个参数name是与中断相关的设备的ASCII文本表示 。
第五个参数dev用于共享中断线。当一个中断处理程序需要释放的时候,dev将提供唯一的标志信息(cookie),以便从共享中断线的诸多中断处理程序中删除指定的那一个。
request_irq()成功执行会返回0,如果返回非0值,表示有错误发生。其中最常见的错误是-EBUSY,他表示给定的中断线已经在使用。
注意request_irq()函数可能会睡眠,因此,不能在中断上下文或其他不允许阻塞的代码调用该函数。
释放中断处理程序
卸载中断处理程序时,需要注销相应的中断程序程序,并释放中断线。上述动作需要调用:
void free_irq(unsigned int irq,void *dev)
如果指定的中断线不是共享的,那么该函数删除处理程序的同时,禁用这条中断线.如果中断线是共享的,则仅仅删除dev所对应的中断处理程序,而中断线本身只有在删除了最后一个处理程序时才会被禁用。
中断上下文
当执行一个中断处理程序或下半部时,内核处于中断上下文中。让我们先回忆一下进程上下文。进程上下文是一种内核所处的模式,此时内核代表进程执行——例如,执行系统调用或运行内核线程。在进程上下文中,可以通过current宏关联当前进程。中断上下文和进程并没有什么瓜葛。与current宏也是不相干的(尽管它会指向被中断的进程)。中断上下文不可以睡眠。如果一个函数睡眠,就不能在中断处理程序中使用——这是对什么样的函数可以在中断处理程序中使用的限制。 中断上下文有较为严格的时间限制,因为它打断了其他代码。中断上下文中的代码应当迅速简洁,尽量不要使用循环去处理繁重的工作。请永远牢记:中断处理程序打断了其他的代码(甚至可能是打断了在其他中断线的另一个中断处理程序)。尽量把工作从中断处理程序中分离出来,放在下半部来执行,因为下半部可以在更合适的时间运行。写中断处理程序时不必关心栈如何设置,或者内核栈的大小是多少。总而言之,尽量节约内核栈空间。
中断处理机制的实现
下半部
下半部的任务就是执行与中断处理密切相关但中断处理程序本身不执行的工作。在理想情况下,最好是中断处理程序将所有工作都交给下半部执行,因为我们希望给在中断处理程序中完成的工作越少越好,我们期望中断处理程序能够尽快地返回。
但是中断处理程序注定要完成一部分工作。例如,中断处理程序几乎都需要通过操作硬件对中断的到达进行确认,有时它还会从硬件拷贝数据。因为这些工作对时间多非常敏感,所以只能靠中断处理程序自己完成。具体的处理工作往往应该在下半部去执行。
为什么要用下半部
理解为什么要让工作推后执行以及在什么时候推后执行非常关键。我们希望尽量减少中断处理程序中需要完成的工作量,因为在它运行的时候当前的中断线在所有处理器上都会被屏蔽。更糟糕的是如果一个处理程序是SA_ INTERRUPT类型,它执行的时候会禁止所有本地中断。而缩短中断被屏蔽的时间对系统的响应能力和性能都至关重要。解决的方法就是把一些工作放到以后去做。
但具体放到以后的什么时候去做呢?在这里,以后仅仅用来强调不是马上而已,理解这一点相当重要。下半部并不需要指明一个确切时间,只要把这些任务推迟一点,让它们在系统不太繁忙并且中断恢复后执行就可以了。通常下半部在中断处理程序一返回就会马上运行。下半部执行的关键在于当它们运行的时候,允许响应所有的中断。
不仅仅是Linux,许多操作系统也把处理硬件中断的过程分为两个部分。上半部分简单快速,执行的时候禁止一些或者全部中断。下半部分稍后执行,而且执行期间可以响应所有的中断。这种设计可使系统处干中断屏蔽状态的时间尽可能的短,以此来提高系统的响应能力。