778 lines
14 KiB
Markdown
778 lines
14 KiB
Markdown
你可以把**中断控制器**理解成:
|
||
👉 **CPU 和所有外部设备之间的“中断调度中心”**,专门负责把“谁在打断 CPU”这件事变得可控、有序。
|
||
|
||
我不绕概念,直接从“它解决什么问题 → 怎么工作 → 现代结构”讲清楚。
|
||
|
||
---
|
||
|
||
# 一、为什么必须有中断控制器(没有会怎样)
|
||
|
||
如果没有中断控制器,系统会变成:
|
||
|
||
```text id="f5b0kz"
|
||
键盘 ——┐
|
||
网卡 ——┼——→ CPU(直接连)
|
||
磁盘 ——┘
|
||
```
|
||
|
||
问题:
|
||
|
||
```text id="b6n4ul"
|
||
1)CPU根本不知道是谁发的中断
|
||
2)多个设备同时中断 → 冲突
|
||
3)优先级无法控制
|
||
4)无法分配给不同CPU核心
|
||
```
|
||
|
||
👉 所以必须有一个“中间层”:
|
||
|
||
```text id="1r9nql"
|
||
设备 → 中断控制器 → CPU
|
||
```
|
||
|
||
---
|
||
|
||
# 二、中断控制器到底做什么(核心三件事)
|
||
|
||
---
|
||
|
||
## 1)接收中断请求(汇聚)
|
||
|
||
```text id="gg3qmh"
|
||
多个设备 → 中断控制器
|
||
```
|
||
|
||
它像一个“汇线器”:
|
||
|
||
* 键盘发一个中断
|
||
* 网卡发一个中断
|
||
* 磁盘发一个中断
|
||
|
||
👉 都先到它这里
|
||
|
||
---
|
||
|
||
## 2)仲裁(谁先处理)
|
||
|
||
如果同时来多个:
|
||
|
||
```text id="7x9q3m"
|
||
网卡 + 键盘 同时中断
|
||
```
|
||
|
||
中断控制器决定:
|
||
|
||
```text id="2sl9t9"
|
||
谁优先(优先级)
|
||
谁延后(排队)
|
||
```
|
||
|
||
---
|
||
|
||
## 3)分发(发给哪个 CPU)
|
||
|
||
现代是多核:
|
||
|
||
```text id="e9q0j3"
|
||
CPU0 / CPU1 / CPU2 / CPU3
|
||
```
|
||
|
||
中断控制器可以:
|
||
|
||
```text id="3cf9xk"
|
||
把网卡中断发给 CPU2
|
||
把键盘中断发给 CPU0
|
||
```
|
||
|
||
👉 这就是“中断亲和性(IRQ affinity)”
|
||
|
||
---
|
||
|
||
# 三、现代中断控制器:APIC
|
||
|
||
这是 x86 现在的主流结构。
|
||
|
||
---
|
||
|
||
## 它分成两部分(非常关键)
|
||
|
||
---
|
||
|
||
### 1)IOAPIC(外部中断入口)
|
||
|
||
```text id="k5x42n"
|
||
设备 → IOAPIC
|
||
```
|
||
|
||
作用:
|
||
|
||
```text id="0vt2no"
|
||
收集来自设备的中断(IRQ / MSI)
|
||
```
|
||
|
||
---
|
||
|
||
### 2)Local APIC(每个 CPU 一个)
|
||
|
||
```text id="y4x8vx"
|
||
每个 CPU core 内部都有一个
|
||
```
|
||
|
||
作用:
|
||
|
||
```text id="x6j5x9"
|
||
接收中断
|
||
决定是否打断当前CPU执行
|
||
```
|
||
|
||
---
|
||
|
||
## 整体结构
|
||
|
||
```text id="wqjskv"
|
||
设备 → IOAPIC → Local APIC(某个CPU) → CPU核心
|
||
```
|
||
|
||
---
|
||
|
||
# 四、现代中断是怎么“传”的(重点)
|
||
|
||
---
|
||
|
||
## 方式 1:传统 IRQ(老)
|
||
|
||
```text id="r7w7lz"
|
||
设备 → 电线(IRQ) → IOAPIC
|
||
```
|
||
|
||
---
|
||
|
||
## 方式 2:MSI(现代主流)
|
||
|
||
设备做的是:
|
||
|
||
```text id="h9u8k3"
|
||
发一次 PCIe 写操作
|
||
```
|
||
|
||
写到 APIC 的某个寄存器:
|
||
|
||
```text id="0n0a0s"
|
||
设备 → PCIe → Local APIC
|
||
```
|
||
|
||
👉 这次写 = “中断信号”
|
||
|
||
---
|
||
|
||
# 五、CPU 眼里中断控制器是什么
|
||
|
||
从 CPU 角度:
|
||
|
||
```text id="rtr0yb"
|
||
Local APIC:
|
||
有一个“中断队列”
|
||
```
|
||
|
||
当有中断:
|
||
|
||
```text id="f4l6l6"
|
||
APIC 告诉 CPU:
|
||
“有中断 vector = 33”
|
||
```
|
||
|
||
CPU才会:
|
||
|
||
```text id="sdyh6x"
|
||
查 IDT → 跳 handler
|
||
```
|
||
|
||
👉 所以:
|
||
|
||
```text id="3gkp1k"
|
||
中断控制器 ≠ 执行中断
|
||
中断控制器 = 告诉CPU“该执行哪个中断”
|
||
```
|
||
|
||
---
|
||
|
||
# 六、一个完整例子(键盘)
|
||
|
||
```text id="9u8v3k"
|
||
1)键盘产生 scan code
|
||
|
||
2)控制器发中断
|
||
|
||
3)IOAPIC 接收
|
||
|
||
4)IOAPIC:
|
||
- 查配置
|
||
- 决定 vector = 33
|
||
- 选择 CPU0
|
||
|
||
5)Local APIC(CPU0)收到
|
||
|
||
6)CPU0:
|
||
- 当前指令结束
|
||
- 进入中断处理
|
||
|
||
7)查 IDT[33] → keyboard handler
|
||
```
|
||
|
||
---
|
||
|
||
# 七、再给你一个“工程级理解”
|
||
|
||
你可以把中断控制器当成:
|
||
|
||
```text id="2m2v3n"
|
||
“中断调度器”
|
||
```
|
||
|
||
类似 CPU 调度器:
|
||
|
||
| CPU调度器 | 中断控制器 |
|
||
| ------ | -------- |
|
||
| 管线程 | 管中断 |
|
||
| 决定谁运行 | 决定谁打断CPU |
|
||
| 分配CPU | 分配给哪个CPU |
|
||
|
||
---
|
||
|
||
# 八、你现在应该形成的模型
|
||
|
||
```text id="gql5w7"
|
||
设备:
|
||
产生事件
|
||
|
||
DMA:
|
||
搬数据
|
||
|
||
中断控制器:
|
||
调度中断
|
||
|
||
CPU:
|
||
执行中断处理
|
||
```
|
||
|
||
---
|
||
|
||
# 九、最后一句“压缩理解”
|
||
|
||
```text id="6u9yzf"
|
||
中断控制器 = 把“混乱的硬件中断”变成“有序、可调度的CPU事件”
|
||
```
|
||
|
||
---
|
||
|
||
如果你继续往下走,我建议下一步:
|
||
|
||
👉 **“中断上半部 / 下半部(softirq / tasklet)是怎么拆分工作的”**
|
||
|
||
这一步会让你理解:
|
||
|
||
* 为什么中断不能干太多事
|
||
* Linux 是怎么把中断变成“异步任务系统”的
|
||
|
||
|
||
---
|
||
|
||
不是一个东西,而且在现代系统里它们是**明确分层、分工协作**的两类硬件。你可以把它们想成两级:
|
||
|
||
```text
|
||
设备(键盘/网卡/磁盘)
|
||
↓
|
||
设备控制器(device controller / host controller)
|
||
↓ (发中断请求:IRQ 或 MSI)
|
||
中断控制器(interrupt controller, APIC)
|
||
↓
|
||
CPU
|
||
```
|
||
|
||
下面把三件事讲清:**谁是谁、信号怎么走、怎么“分配/配置”**。
|
||
|
||
---
|
||
|
||
# 一、设备控制器 vs 中断控制器:本质区别
|
||
|
||
## 1)设备控制器(Device Controller)
|
||
|
||
* 位置:贴近设备(有时在设备里,有时在主板/芯片组里)
|
||
* 作用:**把具体设备“变成可编程接口”**
|
||
|
||
* 提供寄存器(MMIO)给 CPU 配置
|
||
* 管 DMA(把数据搬到内存)
|
||
* 维护队列/缓冲(ring、FIFO 等)
|
||
* **在需要时“发出中断请求”**
|
||
|
||
例子:
|
||
|
||
* USB 键盘 → USB Host Controller(xHCI)
|
||
* 网卡 → NIC 控制器(在网卡芯片里)
|
||
* 磁盘 → SATA/AHCI 或 NVMe 控制器
|
||
|
||
👉 它关心的是:**设备数据 & 何时通知 CPU**
|
||
|
||
---
|
||
|
||
## 2)中断控制器(Interrupt Controller)
|
||
|
||
* 位置:CPU/主板一侧(每核一个本地控制器 + 全局入口)
|
||
* 作用:**把“谁在请求中断”这件事变成可调度事件**
|
||
|
||
* 接收中断请求(来自很多设备/控制器)
|
||
* 仲裁优先级
|
||
* 选择目标 CPU 核心
|
||
* 告诉 CPU:用哪个“中断向量(vector)”
|
||
|
||
现代 x86 是 APIC 体系:
|
||
|
||
* IOAPIC:外部入口(接收 IRQ,或配合转发)
|
||
* Local APIC:每个 CPU 核一个(最终递送给该核)
|
||
|
||
👉 它关心的是:**哪个中断先、送到哪个核、对应哪个 vector**
|
||
|
||
---
|
||
|
||
## 一句话区分
|
||
|
||
```text
|
||
设备控制器:产生事件 +(可选)发中断
|
||
中断控制器:调度这些中断给 CPU
|
||
```
|
||
|
||
---
|
||
|
||
# 二、信号是怎么从设备“走到 CPU”的
|
||
|
||
分两种路径:**老式 IRQ 线** 和 **现代 MSI(主流)**。
|
||
|
||
---
|
||
|
||
## 路径 A:传统 IRQ(电信号)
|
||
|
||
```text
|
||
设备控制器 ──IRQ线──> IOAPIC ──> Local APIC(某核) ──> CPU
|
||
```
|
||
|
||
流程要点:
|
||
|
||
1. 设备控制器把某根 IRQ 线拉高
|
||
2. IOAPIC 收到,查自己的“重定向表”(决定 vector/目标核)
|
||
3. 转发给某个 CPU 的 Local APIC
|
||
4. CPU 在指令边界响应,中断进入内核
|
||
|
||
---
|
||
|
||
## 路径 B:MSI / MSI-X(现代主流)
|
||
|
||
**没有“中断线”,而是一次“特殊的内存写”**:
|
||
|
||
```text
|
||
设备控制器 ──(PCIe 写)──> Local APIC(目标核) ──> CPU
|
||
```
|
||
|
||
要点:
|
||
|
||
1. 驱动在初始化时告诉设备:“中断请写到这个地址/数据”(APIC 的 MSI 门铃)
|
||
2. 设备控制器在合适时机发起一笔 PCIe 写(doorbell)
|
||
3. 这笔写被 APIC 解释为“中断到达(带某个 vector)”
|
||
4. CPU 响应并跳转到对应 handler
|
||
|
||
👉 这也是为什么 MSI 能:
|
||
|
||
* 不用物理中断线(可扩展)
|
||
* 直接**指定目标 CPU**(天然支持多核分发)
|
||
|
||
---
|
||
|
||
# 三、“怎么分配的”:谁决定 vector、优先级、目标 CPU?
|
||
|
||
这里有三层配置,主要由**操作系统驱动**在启动/设备初始化时完成。
|
||
|
||
---
|
||
|
||
## 1)中断向量(vector)分配
|
||
|
||
* OS 维护 IDT(中断向量表,0–255)
|
||
* 给每个设备/队列分配一个或多个 vector
|
||
* 驱动注册 handler(ISR)
|
||
|
||
```text
|
||
vector → IDT[vector] → handler地址
|
||
```
|
||
|
||
---
|
||
|
||
## 2)路由/目标 CPU(affinity)
|
||
|
||
* 在 APIC/IOAPIC 的配置里(或 MSI 的消息里)
|
||
* 指定:
|
||
|
||
* 这个中断送到哪个 CPU(或一组 CPU)
|
||
|
||
常见策略:
|
||
|
||
* 键盘 → CPU0(低频、简单)
|
||
* 网卡多队列 → 分散到 CPU0..CPU7(并行处理)
|
||
|
||
---
|
||
|
||
## 3)优先级与屏蔽(masking)
|
||
|
||
* 中断控制器有优先级/屏蔽位
|
||
* OS 可以:
|
||
|
||
* 临时屏蔽某个中断
|
||
* 调整优先级(谁可以打断谁)
|
||
|
||
---
|
||
|
||
# 四、把三者串起来(一次真实事件)
|
||
|
||
以“网卡收包(带 DMA + MSI)”为例:
|
||
|
||
```text
|
||
1)驱动初始化:
|
||
- 分配 RX ring(内存)
|
||
- 配置 DMA(给设备 IOVA/PA)
|
||
- 配置 MSI:vector=80,目标=CPU2
|
||
|
||
2)网卡收到数据包
|
||
3)网卡控制器 DMA → 把包写入内存 buffer
|
||
4)根据策略(可能合并多个包):
|
||
网卡控制器 发 MSI(PCIe 写到 APIC)
|
||
|
||
5)Local APIC(CPU2)接收:
|
||
产生中断 vector=80
|
||
|
||
6)CPU2 在指令边界:
|
||
- 保存现场
|
||
- 查 IDT[80]
|
||
- 跳到网卡 ISR
|
||
|
||
7)驱动:
|
||
- 从 ring buffer 取一批包(NAPI 轮询)
|
||
- 交给协议栈
|
||
```
|
||
|
||
---
|
||
|
||
# 五、常见误区一把清
|
||
|
||
1)**“中断控制器和设备控制器是一个东西吗?”**
|
||
→ 不是。前者是**调度/路由中断**,后者是**管理设备并发起中断**。
|
||
|
||
2)**“中断控制器接收设备控制器的信号吗?”**
|
||
→ 是的。
|
||
|
||
* 传统:接收 IRQ 线
|
||
* 现代:接收 MSI(经 PCIe 的写事务,被 APIC 解释为中断)
|
||
|
||
3)**“PCIe 在这中间干嘛?”**
|
||
→ 只是**传输通道/协议**:承载 DMA 数据和 MSI“门铃写”。
|
||
|
||
---
|
||
|
||
# 六、压缩成三句记忆
|
||
|
||
```text
|
||
设备控制器:管理设备 + 何时发中断
|
||
中断控制器:把中断变成“发给哪个CPU的哪个vector”
|
||
CPU:按 vector 查 IDT,执行 handler
|
||
```
|
||
|
||
如果你想再往下一层,我可以把 **“一个中断里为什么要拆成上半部/下半部(hardirq/softirq/NAPI)”** 的内核执行模型连同关键函数路径一起走一遍。
|
||
|
||
---
|
||
|
||
你现在这个直觉**方向是对的**,但混了两层东西:
|
||
👉 **“谁决定 CPU 运行什么”** vs **“谁能打断 CPU”**
|
||
|
||
我帮你把它压成一个**非常干净的统一模型**,把“线程”和“中断”放在同一张图里,但区分清楚它们的角色。
|
||
|
||
---
|
||
|
||
# 一、先给你一句最本质的话
|
||
|
||
```text
|
||
线程:CPU“主动选择”运行的任务(调度器决定)
|
||
中断:CPU“被动被打断”的事件(硬件触发)
|
||
```
|
||
|
||
👉 这两者是**正交的两套机制**,但会交织在一起。
|
||
|
||
---
|
||
|
||
# 二、统一模型(你要在脑子里建立这个)
|
||
|
||
CPU执行的东西其实只有三种:
|
||
|
||
```text
|
||
1)普通执行流(线程)
|
||
2)中断处理(interrupt)
|
||
3)异常/系统调用(exception/syscall)
|
||
```
|
||
|
||
你可以画成:
|
||
|
||
```text
|
||
┌───────────────┐
|
||
│ 线程 A │
|
||
└──────┬────────┘
|
||
│
|
||
(被打断)
|
||
↓
|
||
┌───────────────┐
|
||
│ 中断处理 │
|
||
└──────┬────────┘
|
||
│
|
||
(返回)
|
||
↓
|
||
┌───────────────┐
|
||
│ 线程 A / B │
|
||
└───────────────┘
|
||
```
|
||
|
||
---
|
||
|
||
# 三、线程 vs 中断:谁在“控制 CPU”
|
||
|
||
---
|
||
|
||
## 1)线程(你写的程序)
|
||
|
||
来源:
|
||
|
||
```text
|
||
进程 / 线程(用户程序 or 内核线程)
|
||
```
|
||
|
||
特点:
|
||
|
||
```text
|
||
✔ 由操作系统调度(scheduler)
|
||
✔ 通过时间片切换
|
||
✔ 是“长期执行的任务”
|
||
```
|
||
|
||
比如:
|
||
|
||
```text
|
||
你在跑:
|
||
- 浏览器
|
||
- shell
|
||
- 你的C++程序
|
||
```
|
||
|
||
---
|
||
|
||
## 2)中断(硬件触发)
|
||
|
||
来源:
|
||
|
||
```text
|
||
网卡 / 键盘 / 磁盘 / 定时器
|
||
```
|
||
|
||
特点:
|
||
|
||
```text
|
||
✔ 异步发生
|
||
✔ 可以随时打断线程
|
||
✔ 执行时间很短(必须短)
|
||
```
|
||
|
||
---
|
||
|
||
# 四、你那句话的“正确版本”
|
||
|
||
你说:
|
||
|
||
> 中断让CPU调整运行什么内容
|
||
|
||
✔ 可以这么理解,但要改成:
|
||
|
||
```text
|
||
中断 ≠ 决定运行什么
|
||
中断 = 打断当前执行,让CPU先处理紧急事件
|
||
```
|
||
|
||
真正“决定运行什么”的是:
|
||
|
||
```text
|
||
调度器(scheduler)
|
||
```
|
||
|
||
---
|
||
|
||
# 五、一个关键细节(很多人卡这里)
|
||
|
||
👉 中断本身**不会直接切换线程**
|
||
|
||
---
|
||
|
||
## 实际流程是这样的:
|
||
|
||
```text
|
||
线程A 正在运行
|
||
↓
|
||
(网卡中断)
|
||
↓
|
||
CPU 进入中断处理
|
||
↓
|
||
中断处理函数执行
|
||
↓
|
||
可能触发调度(schedule)
|
||
↓
|
||
切换到线程B
|
||
```
|
||
|
||
👉 重点:
|
||
|
||
```text
|
||
中断只是“触发点”
|
||
线程切换是“调度器决定”
|
||
```
|
||
|
||
---
|
||
|
||
# 六、三者关系(最重要的一张图)
|
||
|
||
```text
|
||
硬件(网卡/键盘)
|
||
↓
|
||
中断
|
||
↓
|
||
中断处理函数
|
||
↓
|
||
(可能唤醒线程)
|
||
↓
|
||
调度器(scheduler)
|
||
↓
|
||
选择线程运行
|
||
```
|
||
|
||
---
|
||
|
||
# 七、一个真实例子(你更容易理解)
|
||
|
||
你写了一个:
|
||
|
||
```c
|
||
read(socket, buf, n);
|
||
```
|
||
|
||
---
|
||
|
||
## 实际发生:
|
||
|
||
```text
|
||
1)线程A(你的程序)调用 read
|
||
2)没有数据 → 线程A 被阻塞(sleep)
|
||
|
||
3)网卡收到数据
|
||
4)DMA 写入内存
|
||
5)网卡发中断
|
||
|
||
6)CPU处理中断:
|
||
→ 把数据放入socket buffer
|
||
→ 标记:线程A可以运行了
|
||
|
||
7)调度器:
|
||
→ 把线程A加入就绪队列
|
||
|
||
8)某个时间点:
|
||
→ CPU 切回线程A
|
||
```
|
||
|
||
---
|
||
|
||
👉 核心逻辑:
|
||
|
||
```text
|
||
中断:负责“数据到了”
|
||
调度器:负责“谁来处理数据”
|
||
线程:真正处理数据
|
||
```
|
||
|
||
---
|
||
|
||
# 八、再帮你把概念压缩成三层
|
||
|
||
---
|
||
|
||
## 1)执行层(CPU在干嘛)
|
||
|
||
```text
|
||
线程 / 中断 / 异常
|
||
```
|
||
|
||
---
|
||
|
||
## 2)控制层(谁决定切换)
|
||
|
||
```text
|
||
中断:打断
|
||
调度器:选择
|
||
```
|
||
|
||
---
|
||
|
||
## 3)数据层(为什么切换)
|
||
|
||
```text
|
||
IO完成 / 定时器 / 系统调用
|
||
```
|
||
|
||
---
|
||
|
||
# 九、一个非常重要的纠正
|
||
|
||
你现在的模型是:
|
||
|
||
```text
|
||
中断 = CPU调度方式
|
||
```
|
||
|
||
❌ 不准确
|
||
|
||
正确模型:
|
||
|
||
```text
|
||
中断 = CPU的“异步事件入口”
|
||
调度 = CPU的“任务选择机制”
|
||
```
|
||
|
||
---
|
||
|
||
# 十、给你一个最简终极理解(建议背)
|
||
|
||
```text
|
||
CPU一直在跑“线程”
|
||
中断只是偶尔插进来的一段“紧急代码”
|
||
调度器才决定接下来跑哪个线程
|
||
```
|
||
|
||
---
|
||
|
||
# 十一、你现在已经到了一个关键分界点
|
||
|
||
接下来你如果再深入一层,会进入:
|
||
|
||
* 为什么中断不能做太多事(→ 下半部)
|
||
* Linux 的 softirq / tasklet / workqueue
|
||
* 抢占(preempt) vs 中断
|
||
|
||
---
|
||
|
||
如果你愿意,我可以下一步帮你做一个**“一条时间线(纳秒级)”**:
|
||
|
||
👉 从“网卡收包 → 中断 → softirq → 唤醒线程 → 用户 read 返回”
|
||
|
||
这个会把你现在所有概念彻底锁死,不再混。
|
||
|
||
|
||
---
|
||
|