307 lines
5.0 KiB
Markdown
307 lines
5.0 KiB
Markdown
这三个问题正好卡在“虚拟内存—总线—中断控制器”的交界处,我直接给你一个工程级、可运行在脑子里的模型。
|
||
|
||
---
|
||
|
||
# 一、DMA 用的是虚拟地址还是物理地址?
|
||
|
||
**短答:DMA 面向的是“物理侧地址”,不是普通的 CPU 虚拟地址。**
|
||
但在现代系统里会多一层:**I/O 虚拟地址(IOVA)**。
|
||
|
||
---
|
||
|
||
## 1)最基础模型(无 IOMMU)
|
||
|
||
```text
|
||
CPU: 虚拟地址(VA) ──页表──> 物理地址(PA)
|
||
DMA: 直接使用 PA
|
||
```
|
||
|
||
流程:
|
||
|
||
1. 驱动在内核里分配一块内存(这时它有 VA 和对应的 PA)
|
||
2. 驱动把**物理地址 PA**写给设备(DMA 描述符里)
|
||
3. 设备按这个 PA 直接读/写内存
|
||
|
||
👉 结论:
|
||
|
||
```text
|
||
DMA 不认识进程的虚拟地址(VA),只认物理地址(PA)
|
||
```
|
||
|
||
---
|
||
|
||
## 2)现代模型(有 IOMMU)
|
||
|
||
引入一个关键组件:IOMMU
|
||
|
||
```text
|
||
CPU: VA ──> PA
|
||
DMA: IOVA ──IOMMU──> PA
|
||
```
|
||
|
||
此时流程变成:
|
||
|
||
1. 驱动向 IOMMU 注册一段内存映射
|
||
2. 得到一个 **IOVA(给设备用的“虚拟地址”)**
|
||
3. 设备用 IOVA 发起 DMA
|
||
4. IOMMU 翻译成真实物理地址 PA
|
||
|
||
👉 关键好处:
|
||
|
||
```text
|
||
✔ 安全:设备不能乱访问内存
|
||
✔ 灵活:可以映射不连续的物理页
|
||
✔ 虚拟化支持(多VM)
|
||
```
|
||
|
||
---
|
||
|
||
## 3)一句话总结
|
||
|
||
```text
|
||
无 IOMMU:DMA 用物理地址
|
||
有 IOMMU:DMA 用 IOVA(再映射到物理地址)
|
||
```
|
||
|
||
---
|
||
|
||
# 二、DMA 会不会和 CPU 冲突?能不能同时工作?
|
||
|
||
**短答:可以完全并行,但会“竞争资源”,不是互斥。**
|
||
|
||
---
|
||
|
||
## 1)并行性(关键认知)
|
||
|
||
```text
|
||
CPU:执行指令
|
||
DMA:走总线搬数据
|
||
```
|
||
|
||
👉 两者是**独立硬件单元**:
|
||
|
||
```text
|
||
✔ 可以同时工作
|
||
✔ 不需要互相等待
|
||
```
|
||
|
||
---
|
||
|
||
## 2)那会不会冲突?——会,但在“带宽层面”
|
||
|
||
冲突发生在这里:
|
||
|
||
```text
|
||
内存控制器 / 总线(如 PCIe)
|
||
```
|
||
|
||
多个参与者:
|
||
|
||
```text
|
||
CPU(load/store)
|
||
DMA(设备)
|
||
GPU(显存访问)
|
||
```
|
||
|
||
都要访问内存:
|
||
|
||
```text
|
||
→ 需要仲裁(arbitration)
|
||
```
|
||
|
||
---
|
||
|
||
## 3)真实情况
|
||
|
||
```text
|
||
CPU读写内存 ←→ DMA读写内存
|
||
↓
|
||
内存控制器调度
|
||
```
|
||
|
||
结果:
|
||
|
||
```text
|
||
✔ 可以并行
|
||
✔ 但会抢带宽(变慢)
|
||
❌ 不是“谁工作另一个就停”
|
||
```
|
||
|
||
---
|
||
|
||
## 4)一个直觉类比
|
||
|
||
```text
|
||
内存 = 公路
|
||
CPU = 小汽车
|
||
DMA = 卡车
|
||
|
||
✔ 可以同时跑
|
||
✔ 但车多会堵
|
||
```
|
||
|
||
---
|
||
|
||
# 三、DMA 如何让 CPU 产生中断?
|
||
|
||
这个是很多人最模糊的一点,我们拆到“硬件信号级”。
|
||
|
||
---
|
||
|
||
## 1)关键:DMA 本身“不发中断”
|
||
|
||
👉 非常重要:
|
||
|
||
```text
|
||
DMA ≠ 中断源
|
||
设备(或控制器)才是
|
||
```
|
||
|
||
---
|
||
|
||
## 2)完整路径(标准 PC 架构)
|
||
|
||
```text
|
||
设备/NIC/USB控制器
|
||
↓
|
||
发中断信号(MSI / MSI-X / 旧IRQ)
|
||
↓
|
||
中断控制器(APIC)
|
||
↓
|
||
CPU
|
||
```
|
||
|
||
---
|
||
|
||
## 3)现代方式:MSI(Message Signaled Interrupt)
|
||
|
||
不用传统“中断线”,而是:
|
||
|
||
```text
|
||
设备发一个“特殊的内存写”
|
||
```
|
||
|
||
具体:
|
||
|
||
```text
|
||
设备(通过 PCIe):
|
||
写入某个固定地址(APIC寄存器)
|
||
```
|
||
|
||
```text
|
||
这次“内存写” = 中断信号
|
||
```
|
||
|
||
---
|
||
|
||
## 4)流程串起来(DMA + 中断)
|
||
|
||
```text
|
||
1)设备 DMA 写数据到内存 buffer
|
||
|
||
2)设备决定:
|
||
- 立刻中断?
|
||
- 延迟(coalescing)?
|
||
|
||
3)如果要中断:
|
||
设备发 MSI(本质是一次内存写)
|
||
|
||
4)APIC 接收到
|
||
→ 通知 CPU
|
||
|
||
5)CPU:
|
||
- 保存上下文
|
||
- 跳转中断处理函数
|
||
```
|
||
|
||
---
|
||
|
||
## 5)为什么设计成“内存写触发中断”?
|
||
|
||
好处:
|
||
|
||
```text
|
||
✔ 不需要专用中断线(可扩展)
|
||
✔ 支持多核(可以指定哪个CPU)
|
||
✔ 和DMA同一套总线(PCIe)
|
||
```
|
||
|
||
---
|
||
|
||
# 四、把三件事彻底统一(你要形成这个模型)
|
||
|
||
---
|
||
|
||
## 1)地址体系
|
||
|
||
```text
|
||
CPU:VA → PA
|
||
DMA:IOVA → PA(或直接 PA)
|
||
```
|
||
|
||
---
|
||
|
||
## 2)执行关系
|
||
|
||
```text
|
||
CPU 执行程序
|
||
DMA 搬数据
|
||
→ 完全并行
|
||
```
|
||
|
||
---
|
||
|
||
## 3)通知机制
|
||
|
||
```text
|
||
DMA完成(只是数据好了)
|
||
↓
|
||
设备决定是否发中断
|
||
↓
|
||
MSI → APIC → CPU
|
||
```
|
||
|
||
---
|
||
|
||
# 五、一个完整“工业级时间线”(你可以脑内跑)
|
||
|
||
```text
|
||
t0:驱动配置 DMA buffer(IOVA/PA)
|
||
|
||
t1:设备收到数据
|
||
t2:DMA → 写入内存
|
||
|
||
t3:CPU 正在跑别的程序(完全并行)
|
||
|
||
t4:设备决定:
|
||
“现在通知CPU”
|
||
|
||
t5:发 MSI(一次PCIe内存写)
|
||
|
||
t6:APIC → CPU
|
||
CPU 被打断
|
||
|
||
t7:中断处理函数:
|
||
从 buffer 读数据
|
||
```
|
||
|
||
---
|
||
|
||
# 六、最后给你三句“压缩理解”
|
||
|
||
```text
|
||
1)DMA 只负责“搬数据”,不负责“通知”
|
||
2)地址上:DMA 不用 CPU 的虚拟地址(除非经 IOMMU 转换)
|
||
3)CPU 和 DMA 并行运行,只在“内存带宽”上竞争
|
||
```
|
||
|
||
---
|
||
|
||
如果你想再往下走一步,下一层是非常关键的:
|
||
|
||
* cache coherence(DMA 写内存,CPU cache 怎么一致?)
|
||
* zero-copy 为什么难(缓存 + 映射问题)
|
||
* DPDK 为什么要关 cache / 绑核
|
||
|
||
这些是“性能优化”的真正核心。
|