Linux 使用大量不同的硬件来完成许多不同的任务。显示设备驱动显示器, IDE 设备驱动磁盘等等。你可以同步地驱动这些设备,就是你可以发出一个请求执行一些操作(比如把一块内存写到磁盘)然后等待操作结束。这种方式,虽然可以工作,但是非常没有效率,操作系统当它等待每一个操作完成的时候会花费大量时间“忙于什么也不做”( busy doing nothing )。一个好的,更有效的方法是做出了请求然后去作其他更有用的事情,然后当设备完成请求的时候被设备中断。在这种方案下,系统中同一时刻可能有许多设备的请求在同时发生。
让设备中断 CPU 当前的工作必须有一些硬件的支持。大多数,如果不是所有的话,通用目的的处理器比如 Alpha AXP 都使用相似的方法。 CPU 的一些物理管脚的电路只要改变电压(例如从 +5V 到 -5V )就会让 CPU 停止正在做的工作,开始执行处理中断的特殊代码:中断处理代码。这些管脚之一可能连接一个内部适中,每一个 1000 分之一秒就接收一个中断,其他的也许连接到系统的其他设备,比如 SCSI 控制器。
系统通常使用一个中断控制器把设备的中断集合在一起,然后把信号传送到 CPU 的一个单一的中断管脚。这可以节省 CPU 的中断管教,也给设计系统带来了灵活性。中断控制器有掩码和状态寄存器,用于控制这些中断。设置掩码寄存器的位可以允许和禁止中断,状态寄存器返回系统中当前的中断。
一些系统中的中断可能是硬连接的,例如实时时钟的内部时钟可能永久地连接到中断控制器的第 3 管脚。但是,另一些管脚连接什么可能由在特定的 ISA 或者 PCI 槽位插入什么控制卡决定。例如,中断控制器的第 4 管脚可能和 PCI 槽位 0 相连,可能某一天有一个以太网卡,当时后来可能是一块 SCSI 控制卡。每一个系统都有它自己的中断中转机制,操作系统必须足够灵活才能处理。
大多数现代的通用目的微处理器用相同的方式处理中断。发生硬件中断的时候, CPU 停止它正在运行的指令,跳到内存中一个位置运行,这里或者包含中断处理代码或者是跳到中断处理代码的指令。这种代码通常在 CPU 的特殊模式下工作:中断模式,通常,这种模式下其他中断不能产生。这里也有例外:一些 CPU 将中断划分级别,更高级别的中断可以发生。这意味着写第一级的中断处理程序必须非常小心。中断处理程序通常都有自己的堆栈,用来存放 CPU 的执行状态( CPU 所有的通用寄存器和上下文)并处理中断。一些 CPU 有一组只在中断模式下存在的寄存器,中断处理代码可以使用这些寄存器来存储它需要保存的大部分上下文信息。
当处理完中断, CPU 的状态恢复,中断结束。 CPU 会继续做它在中断发生之前做的事情。重要的事中断处理程序必须尽可能地有效,通常操作系统不能经常或者长时间阻塞中断。
7.1 Programmable Interrupt Controllers (可编程中断控制器)
系统设计师可以任意使用他们希望用的中断体系结构,但是 IBM PC 都使用 Intel 82C59A-2 CMOS 可编程中断控制器或者它的衍生物。这种控制器在 PC 最初的时候就使用了。它可通过寄存器编程,这些寄存器在 ISA 地址空间的众所周知的位置。甚至很现代的逻辑芯片组都在 ISA 内存的相同位置保留了等价的寄存器。非 Intel 的系统,例如 Alpha AXP PC 不受这些体系限制,通常使用不同的中断控制器。
图 7.1 显示了两个串联在一起的 8 位控制器:每一个都有一个掩码和一个中断状态寄存器, PIC1 和 PIC2 。掩码寄存器位于地址 0x21 和 0xA1 ,而状态寄存器位于 0x20 和 0xA0 。在掩码寄存器的一个特殊位写 1 允许一种中断,写 0 可以禁止它。所以向位 3 写 1 允许中断 3 ,写 0 会禁止它。不幸的是(也是让人气恼的),中断掩码寄存器只可以写,你无法读回你所写的值。这意味着 Linux 必须为它设置的掩码( mask )寄存器保留一份本地拷贝。它在中断允许和禁止的例程中修改这些保存的掩码,每一次都要把整个掩码写到寄存器中。
当产生中断信号,中断处理程序读取两个中断状态寄存器( ISR )。它把 0x20 的 ISR 看作 16 位的中断寄存器的第 8 位, 0xA0 中的 ISR 看作高 8 位。所以,发生在 0xA0 的 ISR 的第 1 位的中断被看作是中断 9 。 PCI1 的第 2 位不可用,因为它用作串联 PIC2 的中断,任何 PIC2 的中断都会使 PIC1 的第 2 位置位。
7.2 Initializing the Interrupt Handling Data Structures (初始化中断处理数据结构)
当设备驱动程序要求控制系统的中断的时候建立核心的中断处理数据结构。为此,设备驱动程序使用一系列 Linux 核心服务,用来请求一个中断、允许它和禁止它。这些设备驱动程序调用这些例程来登记它们的中断处理例程的地址。
参见 arch/*/kernel/irq.c request_irq() enable_irq() and disable_irq()
PC 体系结构为了方便把一些中断固定下来,所以驱动程序在初始化的时候只需要简单地请求它的中断。软盘设备驱动程序就是这样:它总是请求中断 6 。但是也可能一个设备驱动程序不知道设备会使用什么中断。对于 PCI 设备驱动程序这不是问题,因为它们总是知道它们的中断编号。不幸的是对于 ISA 设备没有什么简单的办法找到它们的中断号码, Linux 允许设备驱动程序探查它们的中断来解决这个问题。
首先,设备驱动程序让设备产生中断,然后系统中所有没有分配的中断都允许了。这意味着设备等待处理的中断现在会通过可编程中断控制器传递。 Linux 读取中断状态寄存器然后把它的内容返回到设备驱动程序。非 0 的结果表示在探查中发生了一或多个中断。驱动程序现在关闭探查,并禁止所有位分配的中断。如果 ISA 设备驱动程序成功地找到了它的 IRQ 号,它就可以想平常一样地请求控制它。
参见 arch/*/kernel/irq.c irq_probe_*()
PCI 系统比 ISA 系统更加动态。 ISA 设备的中断通常用硬件设备上的跳线来设置,对于设备驱动程序是固定的。反过来, PCI 设备的中断是在系统启动的时候由 PCI BIOS 或者 PCI 子系统在 PCI 初始化的时候分配的。每一个 PCI 设备可以使用 4 个中断管脚其中之一: A 、 B 、 C 或 D 。这时设备制造的时候确定的,大多数设备缺省用中断管脚 A 。每一个 PCI 槽位的 PCI 中断线( interrupte line ) A 、 B 、 C 和 D 都转到中断控制器。所以槽位 4 的管脚 A 可能转到了中断控制器的第 6 管脚,槽位 4 的管脚 B 可能转到了中断控制器的管脚 7 ,依此类推。
PCI 中断如何被转发(路由 route )完全是和系统相关的,必须有一些理解这种 PCI 中断路由拓扑的设置代码。在 Intel PC 上,这是启动的时候的系统 BIOS 代码完成的。但是对于没有 BIOS 的系统(例如 Alpha AXP 系统), Linux 进行这种设置。 PCI 设置代码把中断控制器的管脚编号写到每一个设备的 PCI 配置头中。它使用它知道的 PCI 中断路有拓扑和设备的 PCI 槽位以及它正在使用的 PCI 中断管脚来决定中断管脚(或者说 IRQ )编号。设备使用的中断管脚就确定下来并放到 PCI 配置头的一个域。它把这个信息写到中断线( interrupte line )域(这是为此目的保留的)。当设备驱动程序运行的时候,它读取这个信息,并使用它向 Linux 核心请求对中断的控制。
参见 arch/alpha/kernel/bios32.c
系统中可能使用许多 PCI 中断资源。例如,当使用 PCI-PCI 桥的时候。中断来源的数目可能超过系统的可编程中断控制器的管脚数目。这种情况下, PCI 设备可以共享中断:中断控制器上的一个管脚接收来自多于一个 PCI 设备的中断。 Linux 让第一个请求一个中断的源宣称( declare )它是否可以共享,这样来支持中断共享。共享中断结果是 irq_action 向量表中的一个条目可以指向几个 irqaction 的数据结构。当发生了一个共享的中断的时候, Linux 会调用这个源的所有的中断处理程序。所有可以共享中断的设备驱动程序(都应该是 PCI 设备驱动程序)必须预备在没有中断服务的时候被调用。
7.3 Interrupt Handling (中断处理)
Linux 中断处理子系统的一个主要任务是把中断转送到( route )正确的中断处理代码段。这种代码必须了解系统的中断拓扑。例如,如果软驱控制器在中断控制器的管脚 6 发生中断,它必须可以识别出中断是来自软驱,并把它转送到软驱设备驱动程序的中断处理程序代码。 Linux 使用一系列数据结构的指针,包含了处理系统中断的例程的地址。这些例程属于系统中的设备的设备驱动程序,每一个设备驱动程序必须负责在驱动程序初始化的时候请求它想要的中断。图 7.2 显示了 irq_action 是一个指向 irqaction 数据结构的指针的向量表。每一个 irqaction 数据结构都包括了这个中断处理程序的信息,包括中断处理例程的地址。不同体系的中断数目和如何处理是不同的,通常,不同系统之间, Linux 中断处理代码是和体系结构相关的。这意味着 irq_action 向量表的大小依赖于中断源的数目而不同。
当发生中断的时候, Linux 必须首先通过读取系统的可编程中断控制器的状态寄存器确定它的来源。然后把这个来源转换成 irq_action 向量表中的偏移。例如,从软驱控制器来的中断控制器管脚 6 的中断会转为中断处理程序向量表中的第 7 个指针。如果发生的中断没有对应的中断处理程序, Linux 核心会记录下一个错误,否则,它会调用这个中断源的所有的 irqaction 数据结构中的中断处理例程。
当 Linux 核心调用设备驱动程序的中断处理例程的时候,它必须有效地判断为什么被中断,并进行响应。为了找出中断的原因,设备驱动程序会读取中断设备的状态寄存器。设备可能回应:发生了一个错误或者完成了一个请求的操作。例如软驱控制器可能报告它已经把软驱的读磁头定位到了软盘正确的扇区。一旦确定了中断的原因,设备驱动程序可能还需要做更多的工作。如果是这样, Linux 核心有机制允许延迟这个操作稍候进行。这可以避免让 CPU 在中断模式下花费太多时间。详细描述参见设备驱动程序章(第 8 章)