Files
Operating-System/IO_Interrupt/Interrupt.md
2026-03-22 17:40:44 +08:00

778 lines
14 KiB
Markdown
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
你可以把**中断控制器**理解成:
👉 **CPU 和所有外部设备之间的“中断调度中心”**,专门负责把“谁在打断 CPU”这件事变得可控、有序。
我不绕概念,直接从“它解决什么问题 → 怎么工作 → 现代结构”讲清楚。
---
# 一、为什么必须有中断控制器(没有会怎样)
如果没有中断控制器,系统会变成:
```text id="f5b0kz"
键盘 ——┐
网卡 ——┼——→ CPU直接连
磁盘 ——┘
```
问题:
```text id="b6n4ul"
1CPU根本不知道是谁发的中断
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 现在的主流结构。
---
## 它分成两部分(非常关键)
---
### 1IOAPIC外部中断入口
```text id="k5x42n"
设备 → IOAPIC
```
作用:
```text id="0vt2no"
收集来自设备的中断IRQ / MSI
```
---
### 2Local 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
```
---
## 方式 2MSI现代主流
设备做的是:
```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控制器发中断
3IOAPIC 接收
4IOAPIC
- 查配置
- 决定 vector = 33
- 选择 CPU0
5Local APICCPU0收到
6CPU0
- 当前指令结束
- 进入中断处理
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 ControllerxHCI
* 网卡 → 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 在指令边界响应,中断进入内核
---
## 路径 BMSI / 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中断向量表0255
* 给每个设备/队列分配一个或多个 vector
* 驱动注册 handlerISR
```text
vector → IDT[vector] → handler地址
```
---
## 2路由/目标 CPUaffinity
* 在 APIC/IOAPIC 的配置里(或 MSI 的消息里)
* 指定:
* 这个中断送到哪个 CPU或一组 CPU
常见策略:
* 键盘 → CPU0低频、简单
* 网卡多队列 → 分散到 CPU0..CPU7并行处理
---
## 3优先级与屏蔽masking
* 中断控制器有优先级/屏蔽位
* OS 可以:
* 临时屏蔽某个中断
* 调整优先级(谁可以打断谁)
---
# 四、把三者串起来(一次真实事件)
以“网卡收包(带 DMA + MSI”为例
```text
1驱动初始化
- 分配 RX ring内存
- 配置 DMA给设备 IOVA/PA
- 配置 MSIvector=80目标=CPU2
2网卡收到数据包
3网卡控制器 DMA → 把包写入内存 buffer
4根据策略可能合并多个包
网卡控制器 发 MSIPCIe 写到 APIC
5Local APICCPU2接收
产生中断 vector=80
6CPU2 在指令边界:
- 保存现场
- 查 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网卡收到数据
4DMA 写入内存
5网卡发中断
6CPU处理中断
→ 把数据放入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 返回”
这个会把你现在所有概念彻底锁死,不再混。
---