Linux 中断机制自底向顶理解 (基于 x86 架构平台)

概述

中断的基本分类

    首先,我们必须对 x86 体系架构下中断的分类有一个基本的分类认知:

  • Hardware Interrupt: 我们将有外部硬件设备产生的中断信号称为硬件中断,由于它可以随时发生 (但仍然按照 CPU 时钟周期来产生),因此也被称为 Asynchronous Interrupt;
  • Software Interrupt: 软件中断由 CPU 执行的指令产生,由于它伴随着 CPU 指令的执行而产生,因此也被称为 Synchronous Interrupt。具体可以分为两种情况:
    1. Exception: 当处理器在执行指令的过程中发现异常事件 (e.g. 页表不存在, 除 0 错误等) 的时候被触发,由 CPU 控制单元发出;
    2. Sysenter: 程序可以通过执行一些指令来产生软件中断 (e.g. x86 平台下的 int 指令)

    其中,对于 Exception,Intel 平台下又有如下的分类 intel_document_volume_3a_c6:

  • Fault: 该类异常通常可以被纠正,纠正后程序可以继续运行。当 Fault 异常发生时,处理器会记录下产生 Fault 异常的指令的地址 (i.e. 当前 CSEIP 寄存器的值),作为异常处理程序的返回地址。在运行完异常处理程序后,处理器将返回原先产生 Fault 异常的指令并重新运行;
  • Trap: 该类异常通常是由于处理器运行了 Trapping Instruction 引起的,在运行完异常处理程序后,程序可以被继续运行。值得的注意的是此时异常程序返回后执行的是紧跟在 Trapping Instruction 后面的指令,这与 Fault 异常有所区别;
  • Abort: 该类异常代表着处理器无法精确地获取导致异常的指令的地址,因此产生 Abort 异常后,程序将无法恢复。
我们本文阐述的重点是 Linux 如何处理 Hardware Interrupt (以下简称中断)。

Linux 中断子系统

    Linux 中断子系统的设计结构如 img_code_structure 所示。在硬件层次上,Interrupt Controller (中断控制器) 负责将来自外部设备的中断信号通告给 CPU;CPU 则负责响应中断,运行对应的中断服务程序。不同架构 (e.g. x86, arm64, etc.) 平台有不同的 CPU 和中断控制器实现,Linux 为了实现代码在各种架构平台上的可移植性,在内核中实现了一个 Generic Interrupt Layer (通用中断层),屏蔽了底层硬件的异构性,向上提供了统一的注册/注销中断回调,屏蔽/恢复中断响应等的接口,这些接口的具体实现将由各个底层平台相关的代码进行实现。得益于通用中断层,厂商在开发设备驱动的时候可以忽略设备所在的底层平台,直接调用 Linux 内核提供的通用中断层 API 即可。

    在本文中,我们将看到通用中断层中相关的数据结构和 API,另外我们还将结合 x86 架构分析底层中断从产生到最终运行对应设备驱动的中断回调的全过程。下面,我们自底向顶,逐渐分析 x86 平台下中断的全流程。

中断硬件

    说到中断硬件,我们的主角就是 中断控制器 (Inertrrupt Controller),它是外部设备向 CPU 发起中断请求的代理人。在 x86 架构下,中断控制器经历了 PIC (Programmable Interrupt Controller)APIC (Advanced Programmable Interrupt Controller) 两个阶段,前者应用于早期单处理器时代,后者应用于如今的多处理器时代。

    在启动对上述两种中断控制器架构的分析之前,我们首先比如明确中断控制器具体的功能: 中断控制器接收来自设备的中断电平信号,将电平信号按照一定的规则转化为 CPU 中断向量 (Interrupt Vector) 后通告给 CPU。

    这里 CPU 中断向量,在 x86 架构下实际上指的就是处理器硬件所能识别的硬件中断编号,也即 中断描述符表 (Interrupt Description Table, IDT) 中的编号,我在学习 x86 保护模式汇编的时候写的另一篇文章曾经阐述过 IDT 相关的内容: 中断和异常的处理与抢占式多任务

    在明确了中断控制器的具体功能后,下面我们对 PIC 和 APIC 中断控制器结构进行分析,其中重点分析 APIC。

PIC

本节内容主要参考自引用文献 zxw_interrupt_in_linux

    Intel 8259 wiki_8259 是历史上最经典的 PIC,由 Intel 在上个世纪 80 年代为 8086 处理器开发,其 28 个引脚分布如下所示:

    其中,各个管脚的功能分别是:

  • IR0~IR7 (Interrupt Request 0~7): 用于连接设备;
  • INT: 连接 CPU, 当有中断请求时,拉高该管脚以通知 CPU 中断的到来;
  • INTA: 连接 CPU,CPU 通过该管脚应答中断请求,并通知 PIC 提交中断的 vector 到数据线;

    此外,由于一片 8259 只能连接 8 个设备,对于当时较为先进的 PC 架构来说显得过少,通常会通过 CS (片选) 将两个 8259A 连在一起构成一个可以连接 15 个设备的 PIC。

    正如其名 Programmable Interrupt Controller,8259 是可编程的芯片。ICW (Interrupt Command Word) 寄存器用于初始化 8259 芯片; OCW (Operation Command Word) 寄存器用于控制 8259 芯片。另外,8259 中还有以下三个内部使用的寄存器:

  • IRR: Interrupt Request Register,一共 8 bits,对应于 IR0~IR7 八个中断管脚。当某个管脚的中断请求到来后,若该管脚没有被屏蔽,则 IRR 中对应的 bit 被置位,表示当前 PIC 已经接收到设备的中断请求,但是还未提交给 CPU;
  • ISR: Interrupt Service Register,一共 8 bits,每个 bit 的意义同上。当 IRR 中的某个中断请求被发送给 CPU 后,ISR 中对应的 bit 即被置位,表示中断已经发送给 CPU,但 CPU 仍未处理完;
  • IMR: Interrupt Mask Register,一共 8 bits,每个 bit 的意义同上,用于屏蔽中断。当某个 bit 被置位时,对应的中断管脚将被屏蔽,上面介绍的 IRR 对应的 bit 在中断来临时将不会被置位。

    PIC 与我们下面将要介绍的 APIC 不同,每一个管脚都具有优先级,以 0 号管脚优先级最高,管脚号码越小的设备,具有较高的中断优先级。

    我们上面提到过,中断控制器的功能是把外部设备的中断电平信号,转化为 x86 硬件可以理解的中断向量。通过对 8259 的 ICW 寄存器进行编程,我们可以设定起始的中断向量号。例如我们设置起始的中断向量号为 $16$,则当 IR3 管脚接收到中断请求时,中断控制器输出的中断向量号将为 $16+3=19$。

    通过 8259 的 PIC 发起的中断流程一般如下所示:

  1. 一个或多个 IRx 管脚上产生电平信号,若对应的中断没有被屏蔽,IRR 中相应的 bit 被置位;
  2. PIC 拉高 INT 管脚通知 CPU 中断发生;
  3. CPU 通过 INTA 管脚应答 PIC,表示中断请求收到;
  4. PIC 收到 INTA 应答后,将 IRR 中具有最高优先级的 bit 复位,并置位 ISR 中对应的 bit;
  5. CPU 通过 INTA 管脚第二次发出脉冲,PIC 收到后计算最高优先级中断的中断向量值,并将它提交到数据线上;
  6. PIC 等待 CPU 写 EOI (End Of Interrupt):
    • 收到 EOI 后,ISR 中最高优先级的 bit 被清零;
    • 如果 PIC 处于 AEOI (Auto End Of Interrupt) 模式,在 PIC 收到第二个 INTA 脉冲时,ISR 中最高优先级的 bit 就会被自动清零。

    前面提到,PIC 的各个管脚是具有优先级的,当一个中断正被 CPU 处理时,优先级等于或低于该中断的中断被自动屏蔽。一个比当前中断优先级更高的中断会被马上发送给 CPU, 而不管 CPU 是否已经为当前的中断写过 EOI

    值得注意的是,以上关于优先级的论述,是基于 PIC 默认的 Full Nested 模式说的。这是 PIC 最常用的模式。实际上 PIC 还有优先级轮转 (rotating)、特殊优先级轮转模式。优先级轮转是指 PIC 在服务完一个管脚后将其优先级降低,并升高未服务管脚的优先级,以实现一种类似轮询的模式,这和后面讲到的 LAPIC Arb 机制类似。

APIC

本节内容主要参考自引用文献 intel_document_volume_3a_c10, zxw_interrupt_in_linux

    上面介绍的 PIC 中断架构,最大的问题在于它无法应用在 多处理器 (Multi Processors, MP) 平台中。为了实现对 MP 的支持,x86 应用了 APIC 中断架构。

    APIC 由两部分组成,一个称为 Local APIC (LAPIC),另一个称为 I/O APIC。前者位于 CPU 中,对于 MP 平台来说,每一个 CPU 核心都有一个自己的 LAPIC; 后者通常位于南桥上,像 PIC 一样连接各个产生中断的设备。I/O APIC 负责将接收到的设备中断信号,根据可编程的转发规则,转换为对应的中断向量后,将中断消息发送给部分或者全部处理器核心对应的 Local APIC,由 Local APIC 再触发 CPU 核心对应的中断。

    如 img_p6_apic 所示,在 Intel 的 P6 和 Pentium 系列的 CPU 中,I/O APIC 和 LAPIC 之间,以及 LAPIC 之间通信的连接是单独的 APIC 总线,也即 Intel 编程文档 intel_document_volume_3a_c10 中提到的 3-wire APIC Bus;在后来的 Intel Xeon 和 Pentium 4 系列 CPU 中,如 img_xeon_apic 所示,I/O APIC 和 LAPIC 之间,以及 LAPIC 之间通信的连接不再使用独立的 APIC 总线,而是使用 Front-side Bus (FSB) 进行通信 (p.s. I/O APIC 和 LAPIC 之间的通信需要借助北桥)。

    上图更加清晰地展示了老架构所使用的 3-Wire APIC Bus;后来的 Intel 至强处理器,I/O APIC 直接使用 System Bus 发送中断请求,然后通过北桥中的 Bridge 转发给 LAPIC;LAPIC 之间的通信走的同样也是 System Bus。

中断信号、中断消息的区别

    在正式展开对 I/O APIC 和 LAPIC 的分析之前,我们需要再次梳理中断信号、中断消息这两者的区别。

    中断信号 是电平信号,是由发送中断的 外部设备/处理器核心 在对应的 中断控制器/中断处理结构 的引脚引起的电平变化,以通告中断的产生。常见的中断信号有两种形式:Level (电平) 触发Edge (边沿) 触发

    在收到中断信号后,中断控制器/中断处理结构 需要通过查表等一系列操作,将中断信号转化为在系统总线上传输的消息,也即 中断消息。因此,在下面的阐述中,我们把实现「信号 $\rightarrow$ 消息转换」功能的 中断控制器/中断处理结构 称为 中断处理前端

    有前端当然就有后端。当前端完成中断消息的转换和发送时,中断消息中封装了关于生成的中断的各种信息,系统中需要有相应的后端结构对这些信息进行进一步处理,最后通告 CPU 运行对应位置的中断服务程序,下面的阐述中我们把实现「中断消息处理」的结构称为 中断处理后端

    如 img_frontend_backend 所示,下面我们将分析在 APIC 系统中三种类型的中断 —— 外部设备中断、处理器间中断和处理器内部中断,它们的详情如 tbl_interrupt_fe_be 所示:

中断类型 中断处理前端 中断处理后端
外部设备中断 I/O APIC LAPIC 的 ISR, IRR, TPR, PPREOI 等寄存器组
处理器间中断 LAPIC 的 ICR 寄存器 LAPIC 的 ISR, IRR, TPR, PPREOI 等寄存器组
处理器内部中断 LAPIC 的 LVT 寄存器组 LAPIC 的 ISR, IRR, TPR, PPREOI 等寄存器组

    可以看到,APIC 系统的中断处理的前后端就藏在了 I/O APIC 和 LAPIC 两个片子中,下面我们分别分析 I/O APIC 和 LAPIC。我们首先从外部设备中断的处理前端 —— I/O APIC 入手进行分析。

I/O APIC

    和 PIC 相比,I/O APIC 最大的作用在于中断分发。当 I/O APIC 收到外部设备的中断信号时,它根据它内部的 Programmable Rediection Table (PRT),它可以格式化出一条中断消息,并发送给某个 CPU 核心下的 LAPIC,由 LAPIC 通知 CPU 进行处理。目前典型的 IOAPIC 具有 24 个中断管脚,每个管脚对应一个 Redirection Table Entry (RTE),RTE 也即 PRT 的表项。与 PIC 不同的是,I/O APIC 的管脚没有优先级,也即连接在 I/O APIC 管脚上的设备是平等的,但这并不意味着 APIC 系统中没有硬件优先级,设备的中断优先级由它对应的中断向量值所决定,APIC 架构将优先级的控制功能放到了 LAPIC 中实现,我们在下一小节中将会看到。

    Intel 发布的第一款 APIC 芯片是 Intel 82489DX,它是一个包含了 I/O APIC 和 LAPIC 的独立的芯片。对于一个老式的双处理器的系统来说,如果使用 82489DX 构建 APIC 架构,则一共需要 3 个 —— 2 个作为 LAPIC,1 个作为 I/O APIC。后来,LAPIC 被做进了处理器中,只有 I/O APIC 需要独立的芯片支持,因此就诞生了后来的 Intel 82093AA 芯片 inteldoc_82093AA,它是专用于 I/O APIC 功能的芯片 ic_evolution

    要理解 I/O APIC 是如何工作的,那么关键就在于 PRT 表。tbl_rte_format 是从 Intel 82093AA 官方文档 inteldoc_82093AA 中查到的关于 PRT 的表项 RTE 的格式说明:

Bit 描述
63:56
[8-bits]

Destination Field,目的字段,R/W (可读写)。根据 Destination Mode 字段 (见下) 的不同,该字段值的意义不同,它有两个意义:

  • Physical Mode (Destination Mode 为 0 时): 其值为 APIC ID,用于标识一个唯一的 APIC;
  • Logical Mode (Destination Mode 为 1 时): 其值根据 LAPIC 的不同配置,代表一组 CPU (详见下文 LAPIC 内容);
55:17
[39-bits]

Reserved,保留未用;

16
[1-bit]

Interrupt Mask,中断屏蔽位,R/W:

  • 置位时,对应的中断管脚将被屏蔽,此时产生的中断将被忽略;
  • 复位时,对应管脚产生的中断将被发送至 LAPIC;
15
[1-bit]

Trigger,触发模式,R/W,指明该管脚的中断由什么方式触发:

  • 1: Level,电平触发;
  • 0: Edge,边沿触发;
14
[1-bit]

Remote IRR,远程 IRR,RO (只读)。只对 Level 触发的中断有效,当该中断是 Edge 触发时,该值代表的意义位定义。当对应引脚的中断是 Level 触发的中断时,该值的含义如下:

  • 当 LAPIC 接收了该中断时,该 bit 被置位;
  • 当 LAPIC 写 EOI 时,该 bit 被复位;
13
[1-bit]

Interrupt Input Pin Polarity (INTPOL),远程 IRR,中断管脚的极性,R/W。指定该管脚的有效电平是高电平还是低电平:

  • 0: 高电平;
  • 1: 低电平;
12
[1-bit]

Delivery Status,传送状态,RO:

  • 0: IDEL,当前引脚上没有接收到中断;
  • 1: Send Pending,I/O APIC 已经接收到该中断,但是由于某种原因该中断还未发给 LAPIC,其原因例如当前 I/O APIC 没有竞争到总线;
11
[1-bit]

Destination Mode,目的地模式,R/W:

  • 0: Physical Mode,解释见 Destination Field;
  • 0: Logic Mode,解释见 Destination Field;
10:8
[3-bits]

Delivery Mode,传送模式,R/W。用于指定该中断以何种方式发送给目的 LAPIC,各种方式需要和相应的触发方式配合,可选的模式如下:

  • Fixed: 000b,发送的中断消息使用 Vector 字段 (见下) 所制定的中断向量,发送给 Destination Field 列出的所有 CPU,Level 和 Edge 触发的中断均支持;
  • Lowest Priority: 001b,发送的中断消息使用 Vector 字段 (见下) 所制定的中断向量,发送给 Destination Field 列出的所有 CPU 中,优先级最低的 CPU (CPU 的优先级见 LAPIC 相关内容),Level 和 Edge 触发的中断均支持;
  • NMI: 100b,None-Maskable Interrupt,不可屏蔽中断,发送给 Destination Field 列出的所有 CPU,Vector 字段 (见下) 的值将被忽略。NMI 是 Edge 触发的,上面 Trigger Mode 字段中的值对 NMI 无影响,但建议配置成 Edge;
  • INIT: 101b,发送给 Destination Field 列出的所有 CPU,LAPIC 收到后执行 INIT 中断,INIT 是 Edge 触发的,上面 Trigger Mode 字段中的值对 INIT 无影响,但建议配置成 Edge;
  • ExtINT: 111b,发送给 Destination Field 列出的所有 CPU,CPU 收到该中断后,认为这是一个 PIC 发送的中断请求,并回应 INTA 信号 (该 INTA 脚连接到的是与该管脚相连的 PIC 上,而非 I/O APIC 上)。ExtINT 用于 PIC 接在 APIC 上的情况,详见后面的 Virtual Wire Mode;
7:0
[8-bit]

Interrupt Vector,中断向量,R/W,指定该引脚产生的中断对应的中断向量,范围从 0x10h (16) 到 0xFEh (254),x86 架构的前 16 个中断向量被系统预留。

    当 I/O APIC 某个管脚接收到中断信号后,会根据该管脚对应的 RTE,格式化出一条中断消息,发送给某个 (或多个) CPU 的 LAPIC。从上表我们可以看出,该消息包含了一个中断的所有信息。

🤔 杂谈: Remote IRR 的意义

    这里我们简单讨论一下 Remote IRR 字段的意义,以及为什么它仅对 Level 触发的中断有效。

    Remote IRR 实际应该叫“Monitor Remote IRR”,用于监控对应中断管脚的状态。如 img_remote_irr_1 所示,它与 I/O APIC 中断管脚 INTIN# 以异或的逻辑驱动 I/O APIC 的消息发送单元,异或结果为 1 时发送消息。此时,I/O APIC 发送消息分两种:level-assertlevel-deassert:

  • Remote IRR 为 0,INTIN# 为 1 时,发送 level-assert 消息,LAPIC 收到后将 IRR 置位;
  • Remote IRR 为 1,INTIN# 为 0 时,发送 level-deassert 消息,LAPIC 收到后将 IRR 复位;

    关于 LAPIC 的 IRR 寄存器详见下面介绍 LAPIC 部分的内容。

    Remote IRR 如此设计是为了保证: 在 Level 触发中断共享的情况下,CPU 服务完所有中断。

    如 img_remote_irr_2 所示,举个例子,有两个 PCI 设备 (Devive A 和 Devive B) 共享一个中断管脚 INTIN1,该引脚的中断触发模式为 Level。我们的场景是:Devive A 先把管脚拉至有效电平 (INTIN1 有效电平为高电平),Devive B 在一段时间后也同样动作 (此时 Devive A 仍然在有效电平)。过程分析如下:

  1. 当 Devive A 拉高引脚电平时,也即 INTIN1 由 0 跳变到 1 时,该引脚对应的 Remote IRR 此时为 0,因此 异或结果为 1,I/O APIC 发送 level-assert 消息通知中断发生,并且 I/O APIC 会置位 Remote IRR;
  2. 在 LAPIC 接收到消息后,LAPIC 将 IRR 中对应的 bit 置位;
  3. 当 CPU 处理完 Devive A 的中断后,会发送 EOI 到 I/O APIC。此时 Remote IRR 被复位;
  4. 由于 Devive B 的中断还没处理,因此引脚 INTIN1 仍然为高电平,这导致 Remote IRRINTIN1 的异或结果又为 1,故又一条 level-assert 中断消息产生;
  5. 在所有中断处理完后,INTIN1 被复位,在 LAPIC 向 I/O APIC 写 EOI 之前,Remote IRR 仍为 1,这导致 Remote IRRINTIN1 的异或结果又为 1,故发送 level-deassert 消息,表明当前 Level 触发的中断引脚上已经没有中断请求了,LAPIC 复位 IRR 中对应的 bit;
  6. 在 LAPIC 为 Device B 的中断写 EOI 时 ,Remote IRR 被复位。此时 Remote IRRINTIN 1 均为 0 (低电平),因此 I/O APIC 不发送任何消息。

    而对于 Edge 触发的中断,由于中断管脚不会一直处于有效电平,因此也就不需要 Remote IRR 来辅助。

Local APIC

    在收到 I/O APIC 的中断消息后,LAPIC 会将该中断交给 CPU 进行处理。和 I/O APIC 相比,LAPIC 具有更多的寄存器以及更加复杂的机制。

LAPIC 可以接受的中断信号/消息

    除了来自 I/O APIC 到达 LAPIC 的中断消息,实际上,LAPIC 还可以接收 本地中断信号 和来自 其它处理器的中断消息img_lapic_struct 给出了 LAPIC 的输入和输出情况。

    根据 Intel 官方编程手册 intel_document_volume_3a_c10,LAPIC 可以接收以下几种类型的中断消息 (p.s. tbl_lapic_input_content 是对 tbl_interrupt_fe_be 的详细补充):

    下面我们对 LAPIC 中的各个部件的功能进行分析。

中断类型 描述

本地中断源

CPU Core 本地连接的 I/O 设备产生的中断信号 APIC 架构允许 I/O 设备通过和处理器的 LINT[1:0] 引脚相连来产生本地中断信号,也可以在这两个引脚上连接 8259 等中断控制芯片来控制外部设备的中断
APIC 计时器产生的中断信号 本地的 APIC 计时器在计数器到达设置的值后将会触发中断信号
Performance monitoring counter 产生的中断 Intel P6,奔腾和至强处理器可以被设置为当 performance-monitoring 计数器溢出的时候,向处理器发送中断信号
温度传感器产生的中断信号
APIC Internal Error 产生的中断信号 当对 Local APIC 的操作产生错误 (e.g. 访问一个不存在的寄存器) 时产生的中断信号

外部中断源

外部连接的 I/O 设备的 中断消息 外部连接的 I/O 设备会把它们产生的中断信号发送给 I/O APIC,后者会把中断信号转换为 中断消息,通过 System Bus/APIC Bus 发送给选定 CPU Core 对应的 Local APIC
Interrupt-processor Interrupts (IPIs) 的 中断消息 Intel 处理器允许 CPU Core 在 System Bus 上将 中断消息 发送给其它 CPU Core(s)
本地中断处理前端: LVT

    针对本地 中断信号,LAPIC 使用的是 LVT (Local Vector Table) 进行处理,具体如 img_lvt 所示。在收到对应的本地中断信号后,LAPIC 会在 LVT 中查询相关的后续动作,包括通告 CPU Core 的中断类型 (e.g. NMI, SMI, Fixed, etc.),中断向量号等内容,这部分就类似于 I/O APIC 中的 PRT 表的功能。

处理器间中断处理前端: ICR

    针对处理器间中断,CPU Core 可以通过向其本地的 ICR (Interrupt Command Register) 写入相应的信息,来实现中断消息的发送。ICR 的具体格式如下所示:

处理后端: IRR 和 ISR 寄存器

    首先我们对 LAPIC 的 IRRISR 寄存器进行介绍。

    img_irr_isr 展示了 LAPIC 的 IRRISR 寄存器的格式。与 PIC 中的 IRRISR 不同的是,LAPIC 的 IRRISR 均为 256-bits 的寄存器,对应 x86 平台上的 256 个中断向量,其中 0~15 为 x86 架构所预留。

    IRR 的功能和 PIC 的 IRR 功能类似,代表 LAPIC 已接收中断,但还未交 CPU 处理。

    ISR 的功能和 PIC 的 ISR 功能类似,代表 CPU 已开始处理中断,但还未完成。与 PIC 有所不同的 是,当 CPU 正在处理某中断时,同类型中断如果发生,相应的 IRR bit 会再次被置位 (p.s. PIC 模式下,同类型的中断被屏蔽);如果某中断被 pending 在 IRR 中,同类型的中断发生, 则 ISR 中相应的 bit 会被置位。这说明在 APIC 系统中,同一类型中断最多可以被计数两次。超过两次时,不同架构处理不一样。对于 Pentium 系列 CPU 和 P6 架构,中断消息会被 LAPIC 拒绝;对于 Pentium4 和 Xeon 系列,新来的中断消息对应的中断向量会和 IRR 中对应的 bit 重叠。

    当然,我们上面讨论的内容都是基于 I/O APIC 中 RTE 表项的 Delivery Mode 为 Fixed 或者 Lowest Priority 时,也即 Vector 字段有效时所作出的。

    上图还有个 TMR 寄存器,即 Trigger Mode Register,用于表示当前正在处理的中断 (i.e. ISR 中记录的中断) 的触发模式。1 为 Level,0 为 Edge。对于 Level 触发的中断,当中断结束,系统软件写 EOI 时,会被广播到所有 I/O APIC,消息中含有中断的向量值,I/O APIC 收到后检查自己的 PRT 表,把相应 RTE 的 Remote IRR 位清零。

    对于 LAPIC 的 IRR,Intel 给出的 x86 编程手册 intel_document_volume_3a_c10 中指出:“当 CPU 准备处理中断时,IRR 中最高优先级的 bit 被清零,ISR 中对应 bit 被置位”。实际上,根据 Multi-Processor Computer System With Interrupt Controllers Providing Remote Reading us_patent_multi_processor_interrrupt 一文中指出的 APIC 的实现,只有对于 Edge 触发的中断,ISR 对应 bit 被置位时,IRR 相应 bit 才会被复位。对于 Level 触发的中断,IRR 中的 bit 会被保留到中断结束时,系统软件写 EOI,LAPIC 收到 I/O APIC 发出的 level-deassert 消息后才清零。

    这里有一个小细节是,这里说同类型中断发生两次,会同时 Pending 在 ISRIRR 的对应 bit 中。问题是,前面我们介绍的 Remote IRR 的异或逻辑保证了在写 EOI 前,新的 Level 引脚的中断消息并不会被 I/O APIC 发送给 Local APIC。是的,这里要补充一点: 这种 pending 两次的机制,只对 Edge 触发中断有效。

处理后端: EOI 寄存器

    与 PIC 一样,LAPIC 同样需要系统软件写 EOI 来知会中断处理的完成,不同的是,LAPIC 中的 EOI 是一个 32-bits 寄存器:

    系统软件在中断处理完成时,需要对 EOI 寄存器写 0 来通告底层硬件当前中断处理完成。

TPR 和 PPR 寄存器

    Intel 处理器对中断向量进行了优先级的划分。上文说到 Intel 处理器一共支持 256 个中断,也即 8-bits。中断向量的 [7:4] 称为中断的 Interrupt-priority Class,是中断分级的大类; [3:0] 即中断在其 Interrupt-priority Class 中的相对位置,是中断分级的子类。值得注意的是,由于 Intel 把 0~31 号中断划分给 Intel 处理器自用,因此 Interrupt-priority Class 的合法取值范围为 2(0010)~15(1111),取值越大,优先级越高。

    一般来说,我们直接食用的是 Interrupt-priority Class 作为一个中断向量的优先级,因此一个优先级中一共就包含了 16 个中断向量。

    在 Local APIC 中,有两个与中断优先级相关的寄存器 —— TPR (Task-Priority Register) 和 PPR (Processor-Priority Register)。

    TPR 用于被操作系统软件设置,其包含的值代表着当前操作系统允许接受的中断向量值的下限,低于该值的中断将被操作系统暂时封锁不予处理。因此 TPR 是一个可写的寄存器。如 tpr 所示,TPR 寄存器中采用了和中断向量值一样的分级机制,分为 [7:4] 的 Task-Priority Class 和 [3:0] 的 Task-Priority Sub-Class。

    PPR 的值代表着当前处理器允许被中断的中断向量的下限,其是基于 TPRISRV 的值产生的,其中 ISRV 的值指的是 ISR 中被置位的优先级最高的中断对应的中断向量,PPR 是一个只读寄存器,具体的值产生方法如下:

  • PPR[7:4]: 等于 TPR[7:4]ISRV[7:4] 中更大的那一个;
  • PPR[3:0] 产生规则如下:
    • 如果 TPR[7:4] > ISRV[7:4],那么 PPR[3:0] = TPR[3:0];
    • 如果 TPR[7:4] < ISRV[7:4],那么 PPR[3:0] = 0;
    • 如果 TPR[7:4] = ISRV[7:4],那么 PPR[3:0] 视处理器不同而不同;

    只有当中断向量值代表的优先级大于 TPR 中存储的值时,LAPIC 才有可能会将其交给 CPU 进行处理,否则会屏蔽它们,这里的屏蔽指的是 LAPIC 会接受它们,将他们 pending 到 IRR 中,但不会交给 CPU 进行处理。

    只有当中断向量值代表的优先级大于 PPR 中存储的值时,对应的中断消息才会被提交给 CPU Core 进行中断并且对中断予以处理。

    当然,我们上面的说明针对的是 I/O APIC 中 Delivery Mode 字段为 Fixed 或者 Lowest Prority 的 RTE 所对应的中断,也即 Vector 字段有效的中断。

    我们上面在介绍 I/O APIC 时曾经说过,当 Delivery Mode 字段为 Lowest Prority 时,中断消息会被发送给 Destination Field 中指定的处理器中,优先级最低的一个进行处理,这里的优先级实际上指的就是各个处理器的 LAPIC 的 TRP 的值。我们下面举一个 Pentium4 和 Xeon 系列优先级仲裁的例子,说明谁是 Lowest Priority。假设有 CPU1、CPU2、CPU3 三个 CPU,相应的 TPR 值为:TPR1=5TPR2=6TPR3=10,I/O APIC 以 Lowest Priority 模式发送一条中断消息,该中断对应的优先级级别为 $3$,则 CPU1 具有最低优先级,接收该中断。此时,该中断被 Pending 到 IRR 中,但不会交给 CPU 处理,因为其优先级级别低于 TPR 值。

LAPIC 中断处理后端流程

    现在我们对 LAPIC 中的中断处理后端的流程进行归纳。

    img_local_apic_be_flow 重点给出了 LAPIC 的中断处理后端部分的流程。实际上,根据架构的不同,它们略微有一些区别:

    对于 Pentium4、Xeon 系列,在收到 中断消息 时:

  1. 通过中断消息的 Destination Field 字段,确定该中断是否是发送给自己的;
  2. 如果该中断的 Delivery Mode 为 NMI、SMI、INIT、ExtINT、SIPI,则直接交由 CPU 处理;
  3. 如果不为 2 中所列的中断,则置位 IRR 中相应的 bit;
  4. 当中断被 pending 到 IRRISR 中后,根据 TPRPPR 寄存器,判断当前最高优先级的中断是否能发送给 CPU 处理;
  5. 软件写 EOI 通知中断处理完成。如果中断为 Level 触发,该 EOI 会通过总线消息广播到所有 I/O APIC。NMI、SMI、INIT、ExtINT、SIPI 类型中断无需写 EOI

    对于 Pentium 系列和 P6 架构,在收到 中断消息 时:

  1. 确定该中断是否由自己接收。如果是一个 IPI,且 Delivery ModeLowest priority,则 LAPIC 与其它 LAPIC 一起仲裁该 IPI 由谁接收;
  2. 若该中断由自己接收,且类型为 NMI、SMI、INIT、ExtINIT、INIT-deassert、或 MP 协议中的 IPI 中断 (BIPI、FIPI、SIPI),则直接交由 CPU 处理;
  3. 如果不为 2 中所列的中断,将中断 pending 到 IRRISR,若已经有相同的的中断 pending 到 IRRISR 上,则拒绝该中断消息,并通知 I/O APIC “retry”;
  4. [同 Pentium4、Xeon] 当中断被 pending 到 IRRISR 中后,根据 TPRPPR 寄存器,判断当前最高优先级的中断是否能发送给 CPU 处理;
  5. [同 Pentium4、Xeon] 软件写 EOI 通知中断处理完成。如果中断为 Level 触发,该 EOI 会通过总线消息广播到所有 I/O APIC。NMI、SMI、INIT、ExtINT、SIPI 类型中断无需写 EOI

I/O APIC 发出的消息是如何找到 LAPIC 的?

    上面两个流程的第一步都是 LAPIC 确定是否由自己接收中断。前面我们提到,I/O APIC RTE 中的 Destination Field 用于指定由哪个 LAPIC 接收,并且分为 PhysicalLogical 两种模式。对于 LAPIC,两种模式有着不同的意义。

Physical Mode

    在该模式下,RTE 中的 Destination Field 表示的是具体的 LAPIC ID,下面我们对 LAPIC 的 ID 进行讨论。

    对于 LAPIC 来说,系统在 RESET 后,都会分配一个唯一的 ID 用作标识。我们可以通过 LAPIC ID 寄存器得到它。下图所示为其格式:

    在系统上电初始化的时候,系统会自动地向每一个 LAPIC 分配一个全局唯一的 APIC ID,各个 LAPIC 所被分配的 ID 也被操作系统和 BIOS 用于作为各个处理器核心的 ID。

    系统软件可以在 EAX 寄存器中填入 $1$ 的情况下,运行 CPUID 指令,软件将可以从 EBX[31:24] 中获取到当前运行的处理器核心所对应的 LAPIC 的 ID。对于某些 CPU 来说,其允许软件对 LAPIC 的 ID 进行修改,但是 Intel 的官方手册建议不要这样做。不论怎么修改,运行 CPUID 指令获取的始终是 LAPIC 被系统初始化赋予的 LAPIC ID。

I/O APIC 的 ID

    APIC ID 分为 LAPIC ID 和 I/O APIC ID。前者唯一的标识系统中某个 LAPIC,后者唯一标识某个 I/O APIC。

    与 LAPIC ID 在系统上电时被自动分配不同,I/O APIC ID 在系统上电时被统一清零,由操作系统或者 BIOS 负责验证 I/O APIC ID 是否唯一,并且在冲突时进行重新分配。分配的原则视系统中断架构不同而定:

  • 对于使用系统总线进行中断消息通告的 Pentium4 和 Xeon 处理器,I/O APIC 之间分配的 ID 不重复即可,I/O APIC 的 ID 可以与 LAPIC 的 ID 相同。原因在于在此种系统中,LAPIC 需要使用 LAPIC ID 参与前端总线竞争,而 I/O APIC 却不用,因为是北桥代理它竞争总线,因此 I/O APIC ID 只用于区分多个 I/O APIC,它和 LAPIC ID 不在一个上下文;
  • 对于使用 APIC Bus 的老式处理器,I/O APIC ID 要用于竞争总线,不能和 LAPIC ID 冲突,因此操作系统/BIOS 为 I/O APIC 分配 ID 的原则是从系统中所有 LAPIC ID 后最小的数字开始分配。
Logical Mode

    在该模式下,中断消息中的 Destination Field 包含的不是 LAPIC ID,而是被称为 MDA (Message Destination Address,消息目的地地址) 的信息。此时,LAPIC 需要两个额外的寄存器来判断自己是否为中断消息的目的地。它们是 LDRDFR

    LDR 的格式如 img_ldr 所示,LDR 全称是 Logical Destination Register,逻辑目的地寄存器。该寄存器包含一个 8-bits 的逻辑 APIC ID (注意区分,它和 LAPIC ID 不是一个东西),在 Logical 模式下用于和 MDA 匹配。

    LDR 的格式由 DFR 指定,DFR 如 img_dfr 所示:

    DFR,Destination Format Register,目的地格式寄存器。该寄存器包含一个 4-bits 的 Model 字段,用于指定 LDR 中的 Logical APIC ID 用何种方式与 MDA 匹配。通过这两个寄存器的配合,Logical 模式又被分为了 Flat 与 Cluster 两种模式:

  • Flat 模式: DFRModel 值为 1111b,此时,LAPIC 将 MDALDR 的 Logical APIC ID 做按位与,如结果不为 0 则接收中断。Logical APIC ID 中每个 bit 代表一个 LAPIC,故 8 bits 最多代表 8 个 CPU;
  • Cluster 模式: DFRModel 值为 0000b。Cluster 模式又分为两种模式:Flat Cluster 模式和 Hierarchical Cluster 模式:
    • Flat Cluster 模式: 该模式只支持 P6 架构和 Pentium 系列 CPU,并假定所有 APIC 通过 APIC BUS 进行通信。该模式将 MDA 编码为两个部分,高 4 bits 为簇号,低 4 bits 标识 LAPIC 在该簇内的 ID (每个bit代表一个LAPIC,故一个簇最多有4 个 LAPIC)。与之对应,LDR 的 Logical APIC ID 也被编码成同样两个部分。

      工作在该模式时,LAPIC 先将 MDA 的高 4 bits 和 Logical APIC ID 的高 4 bits 比 较,以确定自己是否是中断的目的簇。若是,将 MDA 的低 4 bits 与 Logical APIC ID 的低 4 bits 按位与,若值不为 0 则接收中断,否则拒绝。

      通过这种方法,高 4 bits 的簇号可以表示 15 个簇,低 4 bits 的 ID 可以代表簇内的 4 个 CPU,最多可以支持 60 个 CPU。但由于 APIC BUS 的限制,具体的说是 APIC Arb ID(APIC 仲裁 ID)的限制,该模式最多只支持 15 个 CPU。

    • Hierarchical Cluster 模式: 支持 P6 架构和 Pentium 系列,以及 Xeon、Pentium4 系 列。该模式通过为每个簇引入一个 “簇管理器”,将 Flat Cluster 模式中平等的簇构成一个具有等级结构的分级网络,并最多支持 60 个 CPU。

    对于 Flat Cluster 模式,我们下面给出一个例子 (此例中,中断为 Fix delivery mode。关于 Lowest Prority 的例子见 subsubsubsection_tpr_ppr)。假设有三个 CPU 的 Logical 模式配置为: CPU1 的 LDR 值为 0000 0001b,CPU2 的 LDR 值为 0001 0010b,CPU3 的 LDR 值为 0000 0100b,并且此时 DFRModel 值为 0000b,也即 Flat Cluster 模式,则此时 CPU1、CPU3 为一簇,CPU2 为另一簇。若 I/O APIC 发出一条中断消息,其 Destination Mode 为 1,Destination Field 值为 0000 00001b。三个 LAPIC 收到该消息后,CPU1、CPU3 通过 Destination Field 的高 4 bits 判断出该消息目的地为本簇,再将自身 Logical APIC ID 的低 4 bits 与 Destination Field 低 4 bits 按位与,最终 CPU1 接收该中断消息,CPU2、CPU3 丢弃。

PCI(e) 设备中断

    TODO

通用中断层

    本章我们将对通用中断层的相关数据结构以及接口进行分析。正如我们上面所说,通用中断层屏蔽了底层与架构相关的代码和厂商开发的设备驱动代码之间的联系,向上提供了顶层中断相关接口供厂商设备驱动调用,向下提供了若干与中断控制器相关的函数接口供具体底层平台予以实现。因此,理解通用中断层里面的工作原理,将有助于我们理解 Linux 内核处理中断的整个流程。

相关数据结构

    通用中断层中的重要数据结构如 img_generic_data_structure 所示,下面我们分别对它们进行分析。