﻿# Intel 8259A 可编程中断控制器（PIC）—— 工作原理、编程接口与演进

> 现代 x86 中断投递由两级 APIC（Local APIC + I/O APIC）完成（见 [../process/irq-affinity.md](/concepts/process/irq-affinity.md) §二），但理解 **8259A 可编程中断控制器（Programmable Interrupt Controller, PIC）** 是理解中断控制器进化史的第一块基石。它不仅定义了"IRQ 编号"、"优先级链"、"中断向量"、"EOI 应答"等核心概念，而且 **Linux 至今沿用 8259 时代的 IRQ 0-15 编号体系**、IO-APIC 保留 ExtINT 兼容模式、多核启动初期仍靠 8259 投递中断——**8259A 从未真正消失**。本篇参照 [dma-8237.md](/concepts/io/dma-8237.md) 的"祖宗芯片"母题风格，细致拆解 8259A 的硬件架构、全部初始化/操作寄存器逐位分解、主从片级联、完整中断响应时序（两个 INTA 周期）、以及到 APIC 的进化路线。

---

## 零、8259A 在历史上的位置

8259A 是 Intel 在 1980 年代推出（8086/8088 时代配套）的可编程中断控制器。每片 8259A 管理 **8 条 IRQ 线**，PC/AT 用 **两片级联（主片 IRQ2 → 从片 INT）** 提供 **15 个有效中断输入（IRQ0-7 + IRQ8-15）**。

```bash
IBM PC/AT 8259A 级联架构：
主 8259A (端口 0x20/0x21, 中断向量基址 0x08):
  IRQ0 ─── 系统定时器 (PIT 8254, 18.2Hz tick, 现代 APIC Timer 的前身)
  IRQ1 ─── 键盘控制器 (8042)
  IRQ2 ─── 级联到从 8259A  ← 这是关键！主片 IRQ2 不接设备，接的是从片的 INT 输出
  IRQ3 ─── 串口 COM2/COM4
  IRQ4 ─── 串口 COM1/COM3
  IRQ5 ─── 声卡 / 并口 LPT2
  IRQ6 ─── 软盘控制器
  IRQ7 ─── 并口 LPT1
                     主 8259A INT 引脚 → CPU INTR 引脚
                             ↑
                         级联 (INTA 同步)
                             ↓
从 8259A (端口 0xA0/0xA1, 中断向量基址 0x70):
  IRQ8  ─── 实时时钟 (RTC)
  IRQ9  ─── (空闲 / ACPI)
  IRQ10 ─── (空闲)
  IRQ11 ─── (空闲)
  IRQ12 ─── PS/2 鼠标
  IRQ13 ─── 数学协处理器 (x87 FPU)
  IRQ14 ─── 主 IDE 控制器
  IRQ15 ─── 从 IDE 控制器
```

> **关键限制**：8259A 是 8 位 ISA 时代的产物——只有 8 条 IRQ 输入/片、主从级联才凑出 15 条、固定优先级链（IRQ0 > IRQ1 > ... > IRQ7）、不支持多核路由（所有中断只能投给 BSP）、INTA 周期慢（两个总线周期才能拿到向量号）。以上全部是 APIC 要解决的问题。但 8259A 定义的"向量号+EOI 应答+优先级仲裁"三要素至今未变。

---

## 一、硬件架构

### 1.1 内部框图

```plantuml
@startuml
skinparam shadowing false
skinparam rectangle {
  BackgroundColor<<bus>> #C8E6C9
  BorderColor<<bus>> #388E3C
  BackgroundColor<<ctrl>> #FFF9C4
  BorderColor<<ctrl>> #F9A825
  BackgroundColor<<arb>> #FFCCBC
  BorderColor<<arb>> #E64A19
  BackgroundColor<<reg>> #E3F2FD
  BorderColor<<reg>> #1976D2
}
rectangle "数据总线缓冲器\n(D7~D0)\n双向、三态、8-bit" <<bus>> as DB
rectangle "读/写控制逻辑\nRD#/WR#/A0/CS#\n────────────────────\nCPU→PIC: 写 ICW/OCW\nCPU←PIC: 读 IRR/ISR/IMR" <<ctrl>> as RW
rectangle "中断请求寄存器\nIRR (8-bit)\n───────────\n记录正在请求的 IRQ\n(锁存边沿/电平)" <<reg>> as IRR
rectangle "中断服务寄存器\nISR (8-bit)\n───────────\n记录正在被 CPU\n服务的中断级别" <<reg>> as ISR
rectangle "中断屏蔽寄存器\nIMR (8-bit)\n───────────\n1=屏蔽该位 IRQ\n(OCW1 控制)" <<reg>> as IMR
rectangle "优先级仲裁器\n(Privilege Resolver)\n──────────────────\n比较 IRR 中未屏蔽\n的最高优先级\nvs 当前 ISR 中的\n最低优先级\n决定是否发 INT" <<arb>> as PR
rectangle "控制逻辑\n───\nINTA 应答序列\ntrigger INT 引脚" <<arb>> as CTRL
rectangle "级联缓冲/比较器\n───────────\n主片: 驱动 CAS0~CAS2\n从片: 比较自己的 ID\n与 CAS0~CAS2 是否匹配" <<ctrl>> as CASCADE
IRR -down-> PR : 待决中断
ISR -down-> PR : 正在服务的中断
IMR -right-> PR : 屏蔽哪些中断
PR -down-> CTRL : 选出最高优先级\n(大于当前 ISR)
CTRL -right-> DB : 发送 INT + 向量号
DB -[hidden]down-> CASCADE
IRR -[hidden]down-> CASCADE
CASCADE -[hidden]right-> RW
@enduml
```

### 1.2 关键引脚

| 引脚 | 方向 | 功能 |
|------|------|------|
| **IR0~IR7** | 输入 | 8 条中断请求线，外设以此请求中断 |
| **INT** | 输出 | 连接到 CPU 的 INTR 引脚——8259 告诉 CPU："有中断" |
| **INTA#** | 输入 | CPU 发出的中断应答（两个脉冲周期）——8259 在此周期内把向量号放到数据总线上 |
| **D7~D0** | 双向 | 数据总线——CPU 用它读写寄存器，8259 用它发送向量号 |
| **CS#** | 输入 | 片选——为低时 CPU 才能访问这块 8259 |
| **RD# / WR#** | 输入 | 读/写选通 |
| **A0** | 输入 | 地址线——区分偶端口（ICW1/OCW2/OCW3）和奇端口（ICW2/3/4/OCW1/IMR） |
| **CAS0~CAS2** | 双向 | 级联线——主片输出（告诉从片"轮到你应答"），从片输入（与自身 ID 比较） |
| **SP#/EN#** | 输入/输出 | 主/从选择——硬件拉高=主片，拉低=从片 |

### 1.3 三大核心寄存器

这是理解 8259A 的**最关键一张图**——三个 8-bit 寄存器之间的协作关系：

```plantuml
@startuml
skinparam shadowing false
skinparam rectangle {
  BackgroundColor<<irr>> #FFCDD2
  BorderColor<<irr>> #C62828
  BackgroundColor<<isr>> #C8E6C9
  BorderColor<<isr>> #388E3C
  BackgroundColor<<imr>> #E3F2FD
  BorderColor<<imr>> #1976D2
}
rectangle "外设\n(键盘/磁盘/网卡)" as DEV
rectangle "IRR (8-bit)\n────────\nbit 0 = IRQ0\nbit 1 = IRQ1\n...\nbit 7 = IRQ7" <<irr>> as IRR
rectangle "优先级仲裁器\n──────────\nIRR 中未屏蔽最高优先级\n是否 > ISR 中最低优先级？" as ARB
rectangle "ISR (8-bit)\n────────\nbit 0 = IRQ0 正被服务\nbit 1 = IRQ1 正被服务\n...\nbit 7 = IRQ7 正被服务" <<isr>> as ISR
rectangle "IMR (8-bit)\n────────\nbit 0 = 是否屏蔽 IRQ0\nbit 1 = 是否屏蔽 IRQ1\n..." <<imr>> as IMR
DEV -up-> IRR : ① 设备拉高 IRQ 线
IRR --> ARB : ② 记录请求
IMR --> ARB : ③ 屏蔽哪些
ARB --> ISR : ④ 若优先级足够→发INT→CPU应答→置 ISR 对应位
@enduml
```

**三寄存器在不同阶段的值（以 IRQ1 键盘中断为例）**：

| 时刻 | IRR (bit1) | ISR (bit1) | IMR (bit1) | 发生了什么 |
|------|------------|------------|------------|-----------|
| 空闲 | 0 | 0 | 0 | — |
| 键盘按下 | **1** | 0 | 0 | 8259 锁存 IRQ1 到 IRR |
| CPU 正在处理更高优先级 | 1 | 0 | 0 | 仲裁器等待 |
| CPU 发 INTA | 1→0 | 0→**1** | 0 | IRR 对应位清除，ISR 对应位置位 |
| 中断 handler 写 EOI | 0 | 1→**0** | 0 | ISR 对应位清除，该优先级可以再来 |

---

## 二、编程接口 —— 全部寄存器逐位拆解

### 2.0 端口映射与命令分类

8259A 的寄存器通过 **两组端口 + 读写方向 + 命令字 D4 位** 复用：

| 端口 (主片) | 端口 (从片) | A0 | 读 | 写 |
|-------------|-------------|----|----|----|
| 0x20 | 0xA0 | 0 | IRR 或 ISR（由 OCW3 选择） | **ICW1**（D4=1）/ **OCW2**（D4=0, D3=0）/ **OCW3**（D4=0, D3=1） |
| 0x21 | 0xA1 | 1 | IMR（读 OCW1） | **ICW2/3/4**（初始化序列）/ **OCW1**（IMR） |

> **区分 ICW 还是 OCW**：初始化阶段必须在复位后严格按顺序写入 ICW1→ICW2→ICW3→ICW4，写入同一端口（A0=0）；之后所有写入同一端口的都是 OCW，通过 D4 和 D3 区分。

### 2.1 初始化命令字 ICW1 —— 触发初始化、配置触发方式

| 位 | 名称 | 含义 |
|----|------|------|
| D7-D5 | — | 8086/88 模式下必须为 0（仅 80x 模式下有意义：中断向量基址 A7-A5） |
| **D4** | **必须为 1** | **这是"我是 ICW1 不是 OCW2/3"的标志位** |
| D3 | **LTIM** | Level Triggered Mode：1=电平触发，0=边沿触发 |
| D2 | ADI | 地址间隔：8086 默认=1（4 字节间隔），80x=0（8 字节间隔） |
| D1 | **SNGL** | Single：1=只有一片 8259，0=多片级联（需要 ICW3） |
| D0 | **IC4** | ICW4 Needed：1=需要写 ICW4，0=不需要（8080/85 模式） |

### 2.2 初始化命令字 ICW2 —— 中断向量基址

| 位 | 含义 |
|----|------|
| **D7-D3** | 中断向量号的高 5 位（低 3 位由 IRQ 编号自动填充） |
| D2-D0 | 8086 模式下必须为 0（IRQ0→向量基址+0，IRQ1→基址+1，...） |

> **Linux 经典设置**：ICW2=0x20（主片，向量 0x20-0x27），ICW2=0x28（从片，向量 0x28-0x2F）。注意这与 IA-32 保护模式下 CPU 保留向量 0-31 冲突——**这正是 APIC 解决了的问题之一**。

### 2.3 初始化命令字 ICW3 —— 级联配置

**主片的 ICW3**（位图，标记哪个 IR 接了从片）：

| 位 | 含义 |
|----|------|
| D7-D0 | bit=1 表示对应 IR 输入接了从片。PC/AT：D2=1（IRQ2 级联从片），其余=0 |

**从片的 ICW3**（ID 号，告诉从片"你在主片的哪条 IR 线上"）：

| 位 | 含义 |
|----|------|
| D7-D3 | 必须为 0 |
| **D2-D0** | 从片 ID（PC/AT：=2，即 IRQ2） |

### 2.4 初始化命令字 ICW4 —— 工作模式

| 位 | 名称 | 含义 |
|----|------|------|
| D7-D5 | — | 必须为 0 |
| D4 | **SFNM** | Special Fully Nested Mode：1=特殊全嵌套（主片向从片开放同优先级的多个 IRQ），0=普通全嵌套 |
| D3-D2 | BUF | 缓冲模式：0x=非缓冲，10=缓冲/从片，11=缓冲/主片 |
| D1 | **AEOI** | Auto EOI：1=自动（INTA 第二个周期自动清 ISR），0=手动（中断 handler 结束前须写 EOI 命令） |
| D0 | **µPM** | Microprocessor Mode：1=8086/88 模式，0=8080/85 模式 |

### 2.5 操作命令字 OCW1 —— 设置 IMR（中断屏蔽寄存器）

写入奇端口（0x21/0xA1），最常用的一个寄存器：

```bash
OCW1 (IMR):
  D7  D6  D5  D4  D3  D2  D1  D0
  │   │   │   │   │   │   │   │
  IR7 IR6 IR5 IR4 IR3 IR2 IR1 IR0
  1=屏蔽（禁止该 IRQ），0=允许
```

`cli/sti` 控制的是 CPU 端（IF 标志，禁止/允许一切可屏蔽中断），IMR 控制的是 8259 端（选择性屏蔽特定 IRQ）。

### 2.6 操作命令字 OCW2 —— 优先级循环 + EOI 命令

写入偶端口（0x20/0xA0）：

| 位 | 含义 |
|----|------|
| **D7** | **R**（Rotation）：1=开启优先级循环 |
| **D6** | **SL**（Specific Level）：1=配合 L2-L0 指定特定中断级别 |
| **D5** | **EOI**（End of Interrupt）：1=发送 EOI 命令 |
| D4 | 必须为 0（与 ICW1 的 D4=1 区分）+ D3 必须为 0（与 OCW3 区分） |
| D2-D0 | **L2-L0**：当 SL=1 时指定 IRQ 级别 |

最常用的组合：

| R | SL | EOI | 组合 | 俗称 | 含义 |
|---|----|-----|------|------|------|
| 0 | 0 | 1 | `0x20` | **Non-Specific EOI** | 对当前 ISR 中最高优先级位清零——最常用 |
| 0 | 1 | 1 | `0x60+IRn` | **Specific EOI** | 指定清零 ISR 中某一位 |

### 2.7 操作命令字 OCW3 —— 读寄存器选择 + 特殊屏蔽/轮询模式

| 位 | 含义 |
|----|------|
| D7 | 必须为 0（ESMM 之前保留） |
| D6 | **ESMM**（Enable Special Mask Mode）：1=允许 D5 控制特殊屏蔽模式 |
| D5 | **SMM**（Special Mask Mode）：1=特殊屏蔽模式（允许低优先级中断打断高优先级 ISR） |
| D4 | 必须为 0 |
| D3 | 必须为 1（这是"我是 OCW3"的标志） |
| **D2** | —（0=无操作, 1=轮询命令） |
| **D1** | **RR**（Read Register）：1=准备读 IRR/ISR |
| **D0** | **RIS**（Read ISR）：读 ISR=1, 读 IRR=0 |

---

## 三、中断优先级的仲裁机制

### 3.1 固定优先级（Fully Nested Mode，默认）

IRQ0 最高 → IRQ7 最低。这是 8259A 上电默认行为和 Linux 最常用模式：

```bash
优先级：IRQ0 > IRQ1 > IRQ2 > IRQ3 > IRQ4 > IRQ5 > IRQ6 > IRQ7
当前 CPU 正在处理 IRQ4 时：
  IRQ3 到达 → 可以打断（IRQ3 优先级 > IRQ4）← 中断嵌套
  IRQ5 到达 → 等待（IRQ5 优先级 < IRQ4）
  IRQ4 到达 → 等待（同优先级不嵌套）
```

**中断嵌套的完整时序**：

```plantuml
@startuml
skinparam shadowing false
participant "CPU\n(正在处理\nIRQ4)" as CPU4
participant "8259A" as PIC
participant "设备\n(IRQ3)" as DEV3
DEV3 -> PIC : ① IRQ3 拉高
PIC -> PIC : ② IRR: bit3=1
PIC -> PIC : ③ 仲裁：IRQ3 > 当前 ISR 最高位(IRQ4)
PIC -> CPU4 : ④ INT 拉高
CPU4 -> CPU4 : ⑤ 保存 IRQ4 上下文(STI 前先保存)
CPU4 -> PIC : ⑥ 发送 INTA(第一个)
PIC -> PIC : ⑦ IRR bit3 清零, ISR bit3 置位
CPU4 -> PIC : ⑧ 发送 INTA(第二个)
PIC -> CPU4 : ⑨ 向量号=0x23
CPU4 -> CPU4 : ⑩ 执行 IRQ3 handler
CPU4 -> PIC : ⑪ handler 末尾写 EOI(0x20)
PIC -> PIC : ⑫ ISR bit3 清零(恢复 IRQ4 为最高)
CPU4 -> CPU4 : ⑬ 恢复 IRQ4 上下文, 继续
note right of PIC : 中断嵌套成立的前提：\nhandler 内部调用了 STI(开中断)\n否则 IRQ3 进不来
@enduml
```

### 3.2 循环优先级（Rotating Priority）

- **自动循环**：每次 EOI 后，刚被服务的 IRQ 变成最低优先级，其下一个 IRQ 升为最高
- **指定循环**：写 OCW2（R=1, SL=1, L2-L0=指定）将指定某级为最低

> Linux 不使用旋转优先级——它由内核的 softirq+workqueue 机制解决公平性问题。但理解它是理解"为什么 XT-PIC 和 APIC 都选择固定优先级"的前提。

### 3.3 特殊屏蔽模式（Special Mask Mode）

常规模式下，只要 ISR 有任何位置位，同优先级或更低优先级的中断都被阻塞。写入 OCW3（ESMM=1, SMM=1）后，只比较 **当前 IMR**，不考虑 ISR：

```bash
用途：DMA 中断 handler 想允许低优先级定时器中断打断自己，又不希望通过 EOI 释放 ISR 锁
```

---

## 四、完整中断响应时序 —— 两个 INTA 周期详解

这是 8259A 最核心的硬件行为。从设备拉高 IRQ 到 CPU 拿到向量号，8259A 与 CPU 之间有严格的握手协议：

```plantuml
@startuml
skinparam shadowing false
participant "外设" as DEV
participant "8259A" as PIC
participant "CPU" as CPU
== 请求阶段 ==
DEV -> PIC : ① 拉高 IRQx 线
PIC -> PIC : ② IRR 对应 bit 置 1
PIC -> PIC : ③ 优先级仲裁器：
  比较 IRR 中未屏蔽的最高优先级
  vs 当前 ISR 中的最高优先级
PIC -> CPU : ④ INT 引脚拉高（若优先级够）
== CPU 判决阶段 ==
CPU -> CPU : ⑤ 当前指令执行完毕
CPU -> CPU : ⑥ 检查 IF=1?（是否开中断）
CPU -> CPU : ⑦ (若条件满足) 进入中断响应序列
== 中断应答阶段（INTA 总线周期） ==
CPU -> PIC : ⑧ 第一个 INTA# 脉冲：
  - 8259A 内部：(a) 冻结优先级仲裁（防止更高优先级中断同时来）
  - 8259A 内部：(b) 清除 IRR 对应 bit
  - 8259A 内部：(c) 设置 ISR 对应 bit
  - 主片检查：若本次 IRQ 来自从片（IRQ2 级联）：
    → 驱动 CAS0~CAS2 输出从片 ID
    → 从片比较 CAS 与自身 ID，匹配则准备应答
CPU -> PIC : ⑨ 第二个 INTA# 脉冲：
  PIC -> CPU : 8259A 将 8-bit 向量号放到 D7~D0：
    向量号 = ICW2[7:3] + IRQ 编号[2:0]
    例：ICW2=0x20, IRQ1→向量=0x21
== CPU 处理阶段 ==
CPU -> CPU : ⑩ 硬件自动压栈：RFLAGS, CS, RIP
CPU -> CPU : ⑪ 清 IF（关中断，禁止嵌套）
CPU -> CPU : ⑫ 从 IDT[向量号] 加载 handler 地址
CPU -> CPU : ⑬ 跳转到中断 handler
CPU -> CPU : ⑭ handler 执行(处理外设数据等)
== 结束阶段 ==
CPU -> PIC : ⑮ handler 末尾写 EOI(OCW2=0x20)
PIC -> PIC : ⑯ 清除 ISR 对应 bit（优先级"解锁"）
PIC -> PIC : ⑰ 若还有待决中断在 IRR → 回到步骤③
CPU -> CPU : ⑱ IRET 返回（恢复 RFLAGS, CS, RIP）
note right of PIC : 若 AEOI=1（ICW4 设置）：\n步骤⑨ 第二个 INTA 自动清 ISR\n省略步骤⑮⑯
@enduml
```

> **关键设计点**：

> 1. **INTA 分两个周期**：第一个周期冻结仲裁+记录在案，第二个周期才出数据——这保证向量号对应的就是刚才仲裁出来的那个 IRQ

> 2. **AEOI 的陷阱**：自动 EOI 让 handler 不必写 EOI，但也意味着 handler 还没执行完，同优先级中断就可以再次进入——可能导致**中断重入（reentrancy）**。Linux（在 PIC 模式下）使用手动 EOI，handler 末尾才写。

---

## 五、主从级联机制

### 5.1 电气连接

```bash
CPU INTR ←── 主片 INT
                ↑
            主片 IRQ2 ←── 从片 INT
                             ↑
                         从片 IRQ8~15 ←── 外设
INTA"广播"：CPU 的 INTA# 脉冲同时到达主片和从片
CAS 路由：主片在第一个 INTA 周期驱动 CAS0~2，告知从片该谁回答
```

### 5.2 级联下的中断响应序列

```bash
设备 IRQ10 请求中断：
  ① 从片 IRR bit2=1
  ② 从片仲裁通过 → 从片 INT 拉高
  ③ 主片 IRQ2 引脚输入 → 主片 IRR bit2=1
  ④ 主片仲裁通过 → 主片 INT 拉高
  ⑤ CPU 发 INTA 第一个脉冲：
     主片：IRR bit2 清, ISR bit2 置, 发现是级联 IRQ → CAS0~2=010(从片ID=2)
     从片：收到 CAS=010, 匹配 ID → IRR bit2 清, ISR bit2 置
  ⑥ CPU 发 INTA 第二个脉冲：
     从片：因为 CAS 匹配，从片驱动 D7~D0 输出向量号(0x72)
     主片：因为检测到是级联, 主片不驱动总线
  ⑦ CPU 拿到向量号 0x72 → 执行 IRQ10 handler
  ⑧ handler 末尾写 EOI：
     先写从片(0xA0): 从片 ISR bit2 清 → 从片 INT 拉低 → 主片 IRQ2 恢复电平
     再写主片(0x20): 主片 ISR bit2 清
```

> **为什么必须先写从片 EOI 再写主片？** 因为从片的 ISR bit2 置位导致从片 INT 线持续保持高电平，从而主片 IRQ2 依然是"请求中"状态。只有先清从片 ISR，从片 INT 才能拉低，主片 IRQ2 才能真正"完成"——此时主片 EOI 才有意义。反过来写（先主片、后从片）会导致主片 ISR 清掉后立即收到一个假的 IRQ2 请求。

### 5.3 特殊全嵌套模式（Special Fully Nested Mode, SFNM）

默认全嵌套模式下，主片 ISR bit2 置位后，**从片所有 IRQ8~15 都被主片挡住**（主片只看到 IRQ2，不知从片内部还有优先级差别）。SFNM（ICW4 D4=1）解决此问题：

```bash
默认：从片 IRQ10 正在服务 → 主片 ISR bit2=1 → IRQ9 想进来
      → 主片仲裁：IRQ9 在主片看就是 IRQ2，优先级=IRQ2 → ISR 已有 IRQ2 → 不通过
      ❌ IRQ9 被错误地阻塞
SFNM：主片 ISR bit2=1 时，主片不比较 IRQ2 的优先级——只要从片自己仲裁通过（IRQ9>IRQ10），
      主片就不挡，INT 照样发给 CPU
      ✔ IRQ9 可以打断 IRQ10
```

---

## 六、Linux 如何对待 8259A

### 6.1 启动阶段：PIC 模式

现代 x86 Linux 启动时的中断控制器切换过程：

```bash
BIOS / 固件
  │ 默认 8259A PIC 模式（BIOS 已初始化 ICW1-4）
  │ 所有中断（键盘/定时器/磁盘）由 8259A 投递
  ▼
Bootloader (GRUB/syslinux)
  │ 仍然是 PIC 模式
  ▼
Linux 内核早期启动 (arch/x86/boot/)
  │ 还是 PIC 模式——kernel 的前几条指令就是被 PIC 的 timer IRQ0 打断的
  ▼
setup_local_APIC() + apic_intr_mode_init()
  │ 探测 ACPI MADT 表 → 发现 IO-APIC → 决定切换到 APIC 模式
  │ 也有可能因 BIOS bug / 硬件不支持 → 退回到 PIC 模式 (XTPIC)
  ▼
最终运行模式:
  ├── 通常 → 对称 I/O 模式 (Symmetric I/O Mode)：所有中断走 IO-APIC
  ├── 有时 → 8259A Virtual Wire 模式：8259A 只转发时钟中断到 Local APIC LINT0
  └── 罕见 → XT-PIC 兼容模式：完全回退到 8259A
```

### 6.2 `/proc/interrupts` 的 IRQ 编号

即使运行在 APIC 模式下，Linux `/proc/interrupts` 的 IRQ 编号仍然沿袭 8259A 时代：

```bash
           CPU0       CPU1
  0:         42          0   IO-APIC-edge      timer    ← 继承 8259 IRQ0
  1:          0          0   IO-APIC-edge      i8042    ← 继承 8259 IRQ1
  8:          1          0   IO-APIC-edge      rtc0     ← 继承 8259 IRQ8
 12:          4          0   IO-APIC-edge      i8042    ← 继承 8259 IRQ12
 23:          0          0   IO-APIC-fasteoi   ehci_hcd ← IO-APIC 自己的引脚 23
 24:     520000     480000   PCI-MSI-edge      eth0-TxRx-0 ← MSI 中断，IRQ 号是动态分配的
传统 IRQ 0-15：固定给 8259A 的遗留设备
IO-APIC IRQ（通常 16+）：IO-APIC 重定向表的引脚
PCI MSI/MSI-X IRQ（动态分配）：设备直接写 Local APIC，不经过 IRQ 线
```

### 6.3 多核启动时的 8259A

多核系统启动初期，BSP（Boot Strap Processor）先跑，此时：

- 所有中断（包括 AP 的 SIPI）都由 8259A 投递
- BSP 初始化 Local APIC + IO-APIC
- 逐步把 IRQ 从 8259A 迁移到 IO-APIC
- 最终 mask 掉 8259A（通过 IMR=0xFF），所有中断走 IO-APIC

---

## 七、8259A（PIC）到 APIC 的进化路线

### 7.1 为什么需要 APIC？8259A 的六宗罪

| 问题 | 8259A | APIC 解决方案 |
|------|-------|--------------|
| **多核支持** | 所有中断只能投给 BSP——4 核机器只有 1 核处理所有中断（CPU0 `%soft` 100%） | IO-APIC 重定向表的 Destination Field 可以指定任意 CPU（物理/逻辑模式），Local APIC 每核一个独立接收 |
| **中断线数量** | 最多 15 根（主从级联）——现代服务器有几十上百个 PCIe 设备 | IO-APIC 有 24 个引脚，MSI/MSI-X 不占引脚（用 PCIe 写消息），理论上每个设备可有 2048 个 MSI-X 向量 |
| **中断优先级** | 固定静态链 IRQ0>IRQ1>...>IRQ7——无法动态调整 | Local APIC 的 TPR（Task Priority Register）支持动态优先级阈值；Linux 用向量号分布（高向量→高优先级） |
| **投递延迟** | 两个 INTA 总线周期（ISA 时代约 1μs） | MSI/MSI-X 用 PCIe MWr（Memory Write），一个 TLP 搞定，延迟约 100ns |
| **向量号冲突** | 与 CPU 保留向量 0-31 重叠（ICW2 传统设置 0x20） | IO-APIC 的向量号字段 8-bit（0-255），与 CPU 异常向量不重叠 |
| **亲和性** | 无——所有中断只能到同一 CPU | 支持手动（`/proc/irq/N/smp_affinity`）、自动负载均衡（irqbalance） |

### 7.2 进化路线图

```plantuml
@startuml
skinparam shadowing false
rectangle "8259A PIC\n(1980s)\n───────────\n· 8 IRQ/片\n· 主从级联=15IRQ\n· 固定优先级链\n· 只投单核\n· INTA 握手" as PIC
rectangle "APIC\n(1990s Pentium)\n───────────\n· Local APIC(每核)\n· I/O APIC(芯片组)\n· 3 线 APIC Bus\n· 静态目标 CPU" as APIC_V1
rectangle "xAPIC\n(Pentium 4+)\n───────────\n· APIC 寄存器→MMIO\n· 不用专用总线\n· 逻辑/物理目标模式" as XAPIC
rectangle "x2APIC\n(Nehalem+)\n───────────\n· 32-bit APIC ID\n· 支持 >255 核\n· MSR 访问替代 MMIO\n· 集群/物理模式" as X2APIC
rectangle "MSI/MSI-X\n(PCI 2.2+)\n───────────\n· PCIe TLP 直接写\n  Local APIC\n· 不占 IRQ 线\n· 2048 向量/设备\n· 零额外延迟" as MSI
PIC -right-> APIC_V1 : "多核来了，一片 PIC 不够用"
APIC_V1 -right-> XAPIC : "专用总线太慢，改用 MMIO"
XAPIC -right-> X2APIC : "核太多，8-bit ID 不够；VM Exit 太贵"
XAPIC -down-> MSI : 不经过 IO-APIC，更快
X2APIC -[hidden]right-> MSI
@enduml
```

### 7.3 8259A 留下的遗产

即使现代系统完全不用 8259A 了，它的设计遗产无处不在：

| 遗产 | 现代体现 |
|------|---------|
| **IRQ 编号 0-15** | Linux 内核保留前 16 个 IRQ 号给 legacy 设备，日志和 `/proc/interrupts` 至今沿用 |
| **中断向量号 = 基址 + 偏移** | IDT 的向量号机制，MSI-X 的 Message Data 也只是换了个地方写向量号 |
| **EOI（End of Interrupt）** | Local APIC 的 EOI 寄存器（偏移 0xB0），内涵一模一样——告诉中断控制器"这个中断处理完了" |
| **IMR（中断屏蔽）** | Local APIC 的 LVT 条目有 Mask 位（bit 16），和 IMR 是同一种思想 |
| **优先级仲裁** | Local APIC 的 TPR/PPR 机制本质上就是 8259 优先级链的向量化版本 |
| **Virtual Wire 模式** | APIC 的 LINT0 可配置为 ExtINT 模式，模拟 8259 的 INT 行为——用于兼容不支持 APIC 的 OS |

---

## 八、观测与实验

### 8.1 判断当前系统是否还在用 8259A

```bash
# 1. 查看 dmesg 中的 PIC→APIC 切换信息
dmesg | grep -i "pic\|8259\|apic\|xtpic"
# 2. 查看当前中断控制器模式
cat /proc/interrupts | head -20
# 如果显示 "XT-PIC" 说明还在用 8259（极为罕见，通常是虚拟机或非常老的机器）
# 如果显示 "IO-APIC" / "PCI-MSI" 说明已迁移到 APIC 模式
# 3. 查看内核 cmdline——是否强制关闭了 APIC
cat /proc/cmdline
# 如果包含 "noapic" 或 "nolapic" → 可能回退到了 PIC 模式
# 如果包含 "pic=noirq" → 强制禁止 8259A 参与中断投递
```

### 8.2 在 QEMU 中观察纯 8259A 环境

```bash
# 创建一台禁用 APIC 的虚拟机
qemu-system-x86_64 \
  -cpu host,-apic \                # 禁用 CPU 内 APIC
  -machine kernel-irqchip=off \    # 强制使用 8259 用户态模拟
  -m 512M \
  -kernel /boot/vmlinuz-xxx \
  ...
# 机器启动后 /proc/interrupts 应显示 XT-PIC 而不是 IO-APIC
```

---

## 九、一句话总结

> **8259A 是所有现代中断控制器的"祖宗"——它定义了"IRQ 编号+向量号+EOI 应答+优先级仲裁"四要素，至今活在 Linux 的 IRQ 命名体系和 Local APIC EOI 寄存器里。每片管 8 条 IRQ、两片级联凑出 15 条、通过 INTA 两个周期握手出向量号、ICW1~4 四步初始化、OCW1~3 三步操作——这些看似简陋的设计，奠定了中断控制器 40 年的编程范式。理解 8259A 的硬件机制（IRR/ISR/IMR 三寄存器协作 + INTA 握手 + 级联 EOI 顺序），才能理解 APIC 为什么要加 Local APIC（多核分发）/ MSI-X（跳过 IO-APIC 直接写）/ x2APIC（32-bit ID 支持超大系统）——以及为什么 `/proc/interrupts` 的第 0 号永远叫 timer。更广泛的 APIC 中断投递机制见 [../process/irq-affinity.md](/concepts/process/irq-affinity.md)，中断内核处理流程见 [../process/interrupts.md](/concepts/process/interrupts.md)。**

---

> 配套：中断控制器进化见 [../process/irq-affinity.md](/concepts/process/irq-affinity.md) §二（APIC 中断投递系统），中断完整分类与流程见 [../process/interrupts.md](/concepts/process/interrupts.md)，APIC 寄存器速查见 [../process/x86-64-registers.md](/concepts/process/x86-64-registers.md) §七，DMA 控制器（同为 PC/AT 时代核心外设芯片）见 [dma-8237.md](/concepts/io/dma-8237.md)，系统总线如何连接外设见 [pcie.md](/concepts/io/pcie/pcie.md)。

