这是很久以前写的文档,第一次放到网络上来。
1. 中断概述与中断控制器
中断就是打断CPU执行指令序列的事件,这事件可能产生于CPU之内也可能产生于外设。中断可分为同步中断和异步中断两类。同步中断又称为异常,它由CPU控制单元产生,如:缺页异常、除0异常;异步中断是由外设产生,如:各类IO中断。有时候,将异步中断直接称为中断。
x86CPU为实现中断机制提供了两根引脚:NMI 和 INTR。其中 NMI 是不可屏蔽中断,它通常用于电源掉电和物理存储器奇偶校验;INTR是可屏蔽中断,可以通过设置中断屏蔽位来进行中断屏蔽,它主要用于接受外部硬件的中断信号。
中断信号不是由外部硬件直接发送给CPU引脚的,而是通过中断控制器来传达。在早期的系统中,CPU通过INTF引脚连到一个8259A可编程中断控制器(PIC)上。8259A有8个引脚,因而可以同时连接8个外设;同时8259A可以两个级联在一起,形成主辅两级(其中主8259A直接与CPU相连,从8259A连到主8259A的一个引脚上),这样可以提供15个引脚。为了提高对称多处理器系统(SMP)的并发性,出现了高级可编程中断控制器(APIC)。该系统中每个CPU都有一个本地APIC,于本地APIC相连的是IO APIC。当中断信号从外设到达IO APIC时,IO APIC可以公平地将中断信号传给各个本地APIC。每个APIC由24个引脚,系统可以同时有8个IO APIC。
目前大部分单处理器系统都包含一个 IO APIC 芯片,它可以通过以下两种方式来对这种芯片进行配置:
1): 作为一种标准的 8259A 工作方式。本地 APIC 被禁止,外部 I/O APIC 连接到 CPU,两条 LINT0 和 LINT1 分别连接到 INTR 和 NMI 引脚。
2): 作为一种标准外部 IO APIC。本地 APIC 被激活,且所有的外部中断都通过 I/O APIC 接收。
2. Linux下中断分类
前面介绍过中断可以分为异常与异步中断;而根据异常时CPU状态和异常处理方式和返回行为的不同,异常又可以可分为故障(fault)、陷阱(trap)、终止(abort)三类。
类别 |
原因 |
异步/同步 |
返回行为 |
中断 |
来自I/O设备的信号 |
异步 |
总是返回到下一条指令 |
陷阱 |
有意的异常 |
同步 |
总是返回到下一条指令 |
故障 |
潜在可恢复的错误 |
同步 |
返回到当前指令 |
终止 |
不可恢复的错误 |
同步 |
不会返回 |
Linux系统定义了19种异常,比较有名的有:Divide error、Debug、 Page fault等。
3. Linux中断处理机制
Linux系统为了管理各个中断定义了中断描述表(IDT),它保存各个中断或异常向量和对应的中断处理程序入口地址。在系统启动时初始化IDT。IDT中的记录分为三类,分别称为:任务门描述符、中断门描述符、陷阱门描述符。Linux用中断门处理中断,用陷阱门处理异常。与中断不同,异常并不会引起进程切换。但它们都会引起一个内核控制路径,即代表当前进程在内核态执行单独的指令序列。内核控制路径是可以嵌套的,即中断处理程序可以被中断。
通常内核为了快速地处理异常,会在异常到来时向引起异常的进程发送一个信号通知一个反常条件的出现。然后直到该进程接收到这个信号才开始处理这个异常。一般异常处理程序的结构是:
1.在内核堆栈保存大多数寄存器的内容
2.异常处理
3.返回恢复寄存器内容ret_from_exception()
与异常处理不同在中断信号到来时,内核不能向异常处理一样向进程发送信号,因为中断可能会导致进程的切换。一个挂起很长时间的进程,等待的中断信号到来后,一个完全无关的进程可能正在运行,给当前进程发送信号是毫无意义的。为了让内核更快地处理中断,Linux将中断处理分成top half与bottom half 两部分。top half 是内核必须马上处理的,包括读中断信号、设置中断寄存器状态等;bottom half则实现各个中断的处理逻辑,中断的主要操作、大部分处理时间都是在这里完成。bottom half的处理是可以推延的,因而可以让CPU很快地响应完中断。根据bottom half 的处理方式不同,Linux提供了softirq, tasklet, work queue等处理机制。不管采用何种方式,中断处理程序的总体结构是:
1.在内核堆栈保存IRQ值和寄存器的内容
2.为正在给IRQ服务的PIC一个应答,允许PIC进一步发出中断
3.执行共享这个IRQ的所有设备的中断服务例程(ISR)
4.返回恢复寄存器内容ret_from_intr()
前面提到中断控制器上IRQ线总是是有限的,为了满足众多中断请求和便于扩展的需求,Linux提供IRQ线的动态分配的方法为多个设备共享同一条IRQ线。动态分配共享IRQ线的方法如下:
1.在激活一个准备利用IRQ线的设备之前,其驱动程序会调用request_irq()创建一个新的irqaction描述符。
intrequest_irq(unsigned int irq,
irqreturn_t(*handler)(int, void *, struct pt_regs *),
unsigned longirqflags,
const char*devname,
void *dev_id)
2.然后调用setup_irf()将这个描述符插入到合适的IRQ链表。
3.当设备操作结束时,驱动程序调用free_irq()释放这个描述符。
void free_irq(unsignedint irq, void *dev_id)
如前所述, softirq是内核提高中断响应的一种技术,Linux提供open_softirq()来初始化软中断、raise_softirq()来激活软中断、raise_softirq_irqoff()清除软中断。
tasklet是在softirq的基础上可以在运行时初始化的一种延时中断处理技术,它的调用比softirq简单多,是IO驱动程序实现可延时函数的首选方法。
与softirq和taslet的可延时函数运行在中断上下文中不同,work queue中的函数是运行在进程上下文的。因而执行可阻塞的函数就必须使用work queue。work queue函数是通过一种叫做work thread的内核线程来执行的,因而具有可阻塞的特性。
参考资料
- Rebert Love,《Linux Kernel Development,2rd Edition》,机械工业出版社,2006。
- Daniel P. Bovet,Marco Cesati,《Understanding the Linux Kernel,3rd Edition》,东南大学出版社,2006。
- 苏春艳、杨小华,《Linux内核中断内幕》