2012年(11)
分类:
2012-06-01 09:46:12
上一篇博文里,我们谈到了异常处理,现在我们开始研究一下中断处理的情况。中断处理比异常处理复杂得多,这是因为:
第一,中断的发生对于正在运行的进程无关,被调用的中断处理函数叫做中断服务程序,它运行在内核态并处于系统上下文中(使用内核页表、其为内核代码和内核数据结构),所以中断处理程序不允许被阻塞;
第二,由于硬件资源的限制,比如APIC的IRQ引脚数量有限,一根IRQ信号线需要被几个外部设备所共享,因此当一个IRQ信号产生时CPU不可 能会预先知道是哪一个外部设备发出的中断请求,故要求内核必须提供一个能够为共享一根IRQ线上的几个设备提供服务的中断处理程序;
第三,由于执行中断处理程序时不允许被阻塞,所以要求中断处理程序的代码尽可能短,以求快速执行完毕而不影响整个系统的性能。
针对第三点,在这里还要强调一下。当中断发生时,并不是与中断相关的所有操作都具有同等的紧迫性。Linux是这样解决的:把中断发生后需要CPU执行的操作分为“快速中断处理”和“慢速中断处理”两部分。
快速中断处理又再次被划分成两类,一类是那些需要CPU紧急处理的操作——如修改由设备和数据结构同时访问的数据结构、对中断控制器或设备控制器重 新编程等情况——这类处理动作需要在关中断的情况下执行,即CPU的中断标志位IF被禁止,不允许其他中断源所中断;还有一类快速中断处理是一些不是那么 紧急但不能延迟的操作——如键盘上有某个键被按下后,读扫描码这个操作——这类中断处理里程序实在开中断的情况下进行的,因为这类中断操作只修改CPU才 会访问到的数据。
慢速中断处理是指那些不是太紧急同时在一定时间范围内可延迟的中断操作,例如把一个关中断期间拷贝到某缓冲区(位于接口的缓存中)的内容再复制到进 程的地址空间。需要这些数据的进程会“耐心”等待,因此这类操作可以延迟较长时间,由内核在适当的时候调度他们运行。这类慢速处理动作用到的就是著名的 Linux“下半部分”函数。显然,这类操作也是在开中断的情况下执行的。
中断向量
我们先来回顾一下中断的分类。每个中断和异常是由0~255之间的一个数来标识的,我们把这个八位无符号整数叫做向量。非屏蔽中断的向量和异常的向 量是固定的,而可屏蔽中断的向量可以通过对中断控制器的编程来改变。向量0-19号表示20个异常,32-238表示物理号IRQ,其中的128号用作系 统调用,239号用作APIC 时钟中断等。
IBM PC兼容的体系结构要求,一些设备必须被固定地连接到指定的IRQ线。典型的情况是:
- 间隔定时设备必须连到IRQ0线。
- 从8259A PIC必须与IRQ2线相连(尽管现在有了更高级的PIC,Linux还是支持8259A风格的PIC)。
- 必须把外部数学协处理器连接到IRQ 13线(尽管最近的80x86处理器不再使用这样的设备,但Linux仍然支持历史悠久的80386模型)。
那么,除了上述一些典型情况,其他一般的IRQ可配置设备选择一条线则有三种方式:
1、设置一些硬件跳接器(仅适用于旧式设备卡)。
2、安装设备时执行一个实用程序。这样的程序可以让用户选择一个可用的IRQ号,或者探测系统自身以确定一个可用的IRQ号。
3、
在系统启动时执行一个硬件协议。外设宣布它们准备使用哪些中断线,然后协商一个最终的值以尽可能减少冲突。该过程一旦完成,每个中断处理程序都通过访问设
备某个I/O端口的函数,来读取所分配的IRQ。例如,遵循外设部件互连(Peripheral Component Interconnect,
PCI)标准的设备的驱动程序利用一组函数,如pci_read_config_byte()访问设备的配置空间。
内核必须在启用中断前发现IRQ号与I/O设备之间的对应,否则,内核在不知道哪个向量对应哪个设备的情况下,无法处理来自这个设备的信号。IRQ号与I/O设备之间的对应是在初始化每个设备驱动程序时建立的(后面讲设备驱动的时候会提及具体的方法)。
下表就是把IRQ分配给I/O设备的一个例子,这也是大多数80x86体系初始化后的实例:
IRQ |
INT |
硬件设备 |
0 |
32 |
时钟 |
1 |
33 |
键盘 |
2 |
34 |
PIC级联 |
3 |
35 |
第二串口 |
4 |
36 |
第一串口 |
6 |
38 |
软盘 |
8 |
40 |
系统时钟 |
10 |
42 |
网络接口 |
11 |
43 |
USB端口、声卡 |
12 |
44 |
PS/2鼠标 |
13 |
45 |
数学协处理器 |
14 |
46 |
EIDE磁盘控制器的一级链接 |
15 |
47 |
EIDE磁盘控制器的二级链接 |
IRQ数据结构
不管引起中断的电路种类如何,所有的I/O中断处理程序都执行四个相同的基本操作:
1.在内核态堆栈中保存IRQ的值和寄存器的内容。
2.为正在给IRQ线服务的PIC发送一个应答,这将允许PIC进一步发出中断。
3.执行共享这个IRQ的所有设备的中断服务例程(ISR)。
4.跳到ret_from_intr()的地址后终止。
这里重点谈谈第3个操作。大家必须得明确一个概念,一个IRQ不等于一个设备,他们是一个一对多的关系,这一点太重要了。几个设备可以共享一个 IRQ线,这就意味着仅仅中断向量不能说明所有问题。在上面那个例子中,同一个向量43既分配给USB端口,也分配给声卡。不过,在老式PC体系结构(像 ISA)中发现的一些硬件设备,当它们的IRQ与其他设备共享时,就不能可靠地运转。
所以,第三步操作执行的是给定IRQ对应的所有设备的中断服务例程。这是为什么呢?因为内核不可能预先知道是哪个特定的设备产生的本个IRQ,因此,每个ISR都被执行,以验证它的设备是否需要关注,如图所示;如果是,就执行需要执行的所有操作。
还有一种情况——IRQ动态分配。跟多个设备共享一个IRQ不同,一条IRQ线在可能的最后时刻才与一个设备驱动程序相关联;例如,软盘设备的 IRQ线只有在用户访问软盘设备时才被分配。这样,即使几个硬件设备并不共享IRQ线,同一个IRQ向量也可以由这几个设备在不同时刻使用(见本博最后一 部分的讨论)。
每个中断向量都有一个irq_desc_t描述符,其具体字段如下。所有的这些描述符组织在一起形成irq_desc数组。
字段 |
说明 |
handler |
指向PIC对象(hw_irq_controller描述符),它服务于IRQ线 |
handler_data |
指向PIC方法所使用的数据 |
action |
标识当出现IRQ时要调用的中断服务例程。该字段指向IRQ的irqaction描述符链表的第一个元素。在本章后面将描述irqaction描述符。 |
status |
描述IRQ线状态的一组标志 |
depth |
如果IRQ线被激活,则显示0;如果IRQ线被禁止了不止一次,则显示一个正数 |
irq_count |
中断计数器,统计IRQ线上发生中断的次数(仅在诊断时使用) |
irqs_unhandled |
对在IRQ线上发生的无法处理的中断进行计数(仅在诊断时使用) |
lock |
用于串行访问IRQ描述符和PIC的自旋锁 |
irq_desc_t描述符的depth字段和IRQ_DISABLED标志表示IRQ线是否被禁用。每次调用disable_irq()或 disable_irq_nosync()函数,depth字段的值增加,如果depth等于0,函数禁用IRQ线并设置它的IRQ_DISABLED标 志。相反,每当调用enable_irq()函数,depth字段的值减少,如果depth变为0,函数激活IRQ线并清除IRQ_DISABLED标 志。
在系统初始化期间,init_IRQ()函数把每个IRQ主描述符的status字段设置成IRQ_DISABLED。此外,init_IRQ()通过替换由setup_idt()所建立的中断门来更新IDT,上上篇博文的谜底就是在这里解开了。
下面重点来关注一下irq_desc_t描述符的handler字段。由于现代PC体系中,中断控制器已经不再单纯是8259A芯片系列了,还支持 一下其他的PIC电路,因此,Linux用了一个“PIC对象”,由PIC名字和7个PIC标准方法组成。这种面向对象方法的优点是,驱动程序不必关注安 装在系统中的PIC种类。每个驱动程序可见的中断源透明地连接到适当的控制器。定义PIC对象的数据结构叫做hw_interrupt_type(也叫做 hw_irq_controller)。
这里很难理解,我们还是用老方法,举个实例来说明他。假设,我们的系统只是两片8259A级联出来的PIC,则在这种情况下,只有16个 irq_desc_t描述符,其中每个描述符的handler字段指向描述8259A PIC的i8259A_irq_type变量。这个变量被初始化为:
struct hw_interrupt_type i8259A_irq_type = {
.typename = "XT-PIC",
.startup = startup_8259A_irq,
.shutdown = shutdown_8259A_irq,
.enable = enable_8259A_irq,
.disable = disable_8259A_irq,
.ack = mask_and_ack_8259A,
.end = end_8259A_irq,
.set_affinity = NULL
};
这个结构中的第一个字段“XT-PIC”是PIC的名字。接下来就是用于对PIC编程的六个不同的函数指针。前两个函数分别启动和关闭芯片的IRQ 线。但是,在使用8259A芯片的情况下,这两个函数的作用与第三、四个函数是一样的,第三、四个函数是启用和禁用IRQ线。 mask_and_ack_8259A()函数通过把适当的字节发往8259A I/O端口来应答所接收的IRQ。end_8259A_irq()函数在IRQ的中断处理程序终止时被调用。最后一个set_affinity()方法置 为空:它用在多处理器系统中以声明特定IRQ所在CPU的“亲和力”——也就是说,那些CPU被启用来处理特定的IRQ。
下面再来谈谈irq_desc_t描述符的action字段。如前所述,多个设备能共享一个单独的IRQ。因此,内核要维护多个irqaction描述符,其中的每个描述符涉及一个特定的硬件设备和一个特定的中断。包含在这个描述符中的字段如下表所示。
字段 |
说明 |
handler |
指向一个I/O设备的中断服务例程。这是允许多个设备共享同一IRQ的关键字段 |
flags |
描述IRQ与I/O设备之间的关系 |
mask |
未使用 |
name |
I/O设备名(通过读/proc/interrupts文件,在列出所服务的IRQ时也显示设备名) |
dev_id |
I/O设备的私有字段。典型情况下,它标识I/O设备本身(例如,它可能等于其主设备号和次设备号),或者它指向设备驱动程序的数据 |
next |
指向irqaction描述符链表的下一个元素。链表中的元素指向共享同一IRQ的硬件设备 |
irq |
IRQ线 |
dir |
指向与IRQn相关的lproclirgln目录的描述符 |
好啦,中断处理的数据结构介绍完了,下面以一个图来总结并且结束这篇博文: