Linux内核分析之设备驱动-04
This language version is unavailable; showing the other language.
43.1 DMA 原理与 DMA 映射
DMA 的本质是"设备用 bus 地址访问内存",而内核必须负责三件事:地址翻译(虚拟→物理→bus,含 IOMMU 编址)、缓存一致性(CPU 缓存与设备视图的同步)、访问授权(IOMMU 页表给设备开权限)。本节建立翻译观与方向语义。
43.1.1 三种地址与翻译链
一次 DMA 的地址链:
CPU 侧 (内核) 设备侧
虚拟地址 (va)
│ 页表翻译
物理地址 (pa) ←── IOMMU? ──→ bus 地址 (dma_handle)
│ 设备用 dma_handle 发 DMA
(phys == bus, 无 IOMMU 时) (有 IOMMU: iommu 页表把
dma_handle 翻回 pa)
dma_map_single(dev, va, size, dir) 做的事:
1. va → pa (页表/页缓存页)
2. 缓存维护: 按 dir 决定 clean/invalidate (43.2 节)
3. pa → dma_handle (无 IOMMU 恒等; 有则建 IOMMU 映射)
4. 返回 dma_handle 填进设备命令 (41.2 节 queue_rq
填的描述符就是它!)
为什么驱动必须经 API 而不能自己算物理地址:IOMMU 开启时"物理地址"对设备无意义(要的是 IOVA);地址上限因设备而异(32 位 DMA 设备见不到高内存)——dma_set_mask_and_coherent()(dma-mapping.h:631)声明设备能力,内核据此选择直通/swiotlb/IOMMU 路径。
43.1.2 方向语义:enum dma_data_direction
// include/linux/dma-direction.h:6-9
DMA_BIDIRECTIONAL = 0,
DMA_TO_DEVICE = 1,
DMA_FROM_DEVICE = 2,
DMA_NONE = 3,
方向不是元数据而是缓存维护的指令:
| 方向 | map 时 | unmap 时 | 语义 |
|---|---|---|---|
DMA_TO_DEVICE |
clean(CPU 缓存写回内存) | 无操作 | 设备读内存(网卡发包) |
DMA_FROM_DEVICE |
invalidate(弃缓存副本) | invalidate(设备写完,CPU 弃旧缓存) | 设备写内存(收包) |
DMA_BIDIRECTIONAL |
clean | invalidate | 双向(控制块) |
DMA_NONE |
— | — | 调试用:帮助抓方向撒谎 |
弄错方向的代价是数据损坏:TO_DEVICE 忘了 clean,设备读到的是内存里的旧数据;FROM_DEVICE 不 invalidate,CPU 缓存里的旧副本覆盖设备刚写的包——两者都是"偶发的、难复现的"驱动 bug 典型。弱序架构(ARM64)上这些维护不可省略,x86 的强序+缓存一致性 DMA 会掩盖 bug 直到换平台(5.8 节架构差异的又一实例)。
43.1.3 后端选择:dma_map_ops
// include/linux/dma-map-ops.h:16(后端操作表, 节选要点)
struct dma_map_ops {
dma_addr_t (*map_page)(...);
int (*map_sg)(...);
void *(*alloc)(...); /* 一致性分配 (43.2.2) */
...
};
内核按设备能力在多个后端间择路:direct(直通,phys==bus,常态)、swiotlb(弹跳区中转,io_tlb_default_mem,kernel/dma/swiotlb.c:88 的默认实例——32 位设备/加密内存/无 IOMMU 时的兜底)、IOMMU(建映射表)、Xen/seccomp 专用变体。dma_map_single_attrs(dma-mapping.h:509)是分发入口——驱动永远只见统一 API。
小结
DMA 映射的三大职责(翻译、缓存维护、授权)由统一 API 封装:dma_map_single 完成 va→pa→dma_handle 的链条并按方向做 clean/invalidate,方向枚举是缓存维护的指令而非注释,弄错即数据损坏;后端在 direct/swiotlb/IOMMU 间按设备掩码与平台能力自动择路。下一节深入两种映射形态——流式与一致性的协议差异。
43.2 流式 DMA 映射与一致性 DMA 缓冲区
DMA 映射分两种生命周期:流式映射(streaming,每次 IO 临时映射一块已有内存——页缓存页/栈缓冲)与一致性映射(coherent,长期存在、CPU 与设备同视——描述符环)。两者的 API、缓存策略与使用纪律完全不同。本节对照解剖。
43.2.1 流式映射:每次 IO 的临时契约
// include/linux/dma-mapping.h:509-521
static inline dma_addr_t dma_map_single_attrs(struct device *dev, void *ptr,
size_t size, enum dma_data_direction dir, unsigned long attrs)
static inline void dma_unmap_single_attrs(struct device *dev, dma_addr_t addr,
size_t size, enum dma_data_direction dir, unsigned long attrs)
流式映射的生命周期协议 (网卡收发包):
分配内存 (kmalloc/页缓存页, 驱动不碰内容)
dma_map_single(dev, buf, len, DMA_TO_DEVICE)
→ 得 dma_handle 填命令 → 提交 (41.2 节)
设备 DMA 完成 (中断)
dma_unmap_single(dev, handle, len, 同方向)
→ 解除映射 + 缓存维护 → CPU 才可读 buf
流式纪律 (违反即损坏):
[1] map 与 unmap 之间 CPU 不得碰缓冲
(缓存处于"设备拥有"态)
[2] unmap 后不得再让设备访问 (IOMMU 已拆映射)
[3] 方向如实申报 (43.1.2 节)
[4] map_page 变体映射页缓存页 (36 章回写的
bio → 设备 正是页级流式映射)
分散聚合: dma_map_sg(dev, sgl, nents, dir)
→ 一个请求多段一次映射 (41.2 节 rq_for_each_segment
的每段对应一个 sg 条目)
43.2.2 一致性映射:描述符环的家
// include/linux/dma-mapping.h:604
static inline void *dma_alloc_coherent(struct device *dev, size_t size,
dma_addr_t *dma_handle, gfp_t flag)
一致性缓冲的形态:
dma_alloc_coherent(dev, size, &handle, GFP_KERNEL):
返回两个东西 (指针+句柄一次拿全):
cpu_addr: 内核虚拟地址 (CPU 读写)
dma_handle: 设备地址 (设备读写)
同一块内存, CPU 与设备"永远看到一致" —
实现: 非 cached 页 (weakly ordered 架构) 或
IOMMU/coherent 域标记
NVMe 提交/完成环就是它的经典用户:
环表在一致性缓冲里, 设备与 CPU 实时同视
(41.2 节 queue_rq 填的环、完成环读出的 CQE)
大小约束: 环是常驻小缓冲 (页级) — 一致性映射贵
(非缓存内存性能差), 大数据必须走流式
dma_free_coherent 对称释放;
dma_pool (小型一致性块的池) 供描述符阵列复用
43.2.3 选型与协议速查
| 维度 | 流式映射 | 一致性映射 |
|---|---|---|
| 生命周期 | 单次 IO(map..unmap) | 驱动整个生命周期 |
| 内存来源 | 已有缓冲(页缓存/栈) | API 分配的新块 |
| 缓存策略 | 按方向 clean/invalidate | 非 cached(或 coherent 域) |
| 性能 | 高(缓存命中正常) | 低(无缓存)——只适合小控制块 |
| 典型 | 包数据、bio 段 | 描述符环、门铃块、状态块 |
驱动作者的黄金法则:"数据走流式,控制走一致性"。把大数据放一致性缓冲(图省事)会让性能掉一个数量级;把描述符走流式(想省内存)则每次 IO 都付映射+缓存税。
小结
流式映射是"单次 IO 的临时契约"(map→设备独占→unmap,方向决定缓存维护,sg 版批量处理多段),一致性映射是"描述符环的常驻家"(alloc 返回 CPU/设备双地址,非缓存实现永远同视)——前者快但纪律严,后者稳但只宜小而常驻。两者统一在 dma_map_ops 后端(direct/swiotlb/IOMMU)之上,41 章的 queue_rq 填的句柄正是本节 API 的产出。下一节看 IOMMU 如何给设备加一层带权限的页表。
43.3 IOMMU —— 设备地址隔离与翻译
IOMMU 给每个设备一个独立页表:设备地址(IOVA)经 IOMMU 翻译成物理地址,未映射即故障——DMA 从"任意写内存"变成"受控访问"。它同时是虚拟化(49 章 VFIO/EPT 联动)与 DMA 重映射的基础设施。本节拆解 iommu_domain、IOVA 分配器与各架构实现。
43.3.1 iommu_domain:设备的地址空间
// include/linux/iommu.h:223(节选要点)
struct iommu_domain {
unsigned type; /* DMA/UNMANAGED/IDENTITY/SVA 域类型 */
const struct iommu_domain_ops *ops; /* map/unmap/attach 缺省实现 */
...
struct io_pgtable_ops *pgtbl_ops; /* 页表格式适配 (arm_lpae 等) */
...
};
// drivers/iommu/iommu.c:2191
int iommu_attach_device(struct iommu_domain *domain, struct device *dev)
// :2696
int iommu_map(struct iommu_domain *domain, unsigned long iova,
phys_addr_t paddr, size_t size, int prot, gfp_t gfp)
域的四种类型与用途:
DMA domain: 常规设备用 (内核分配 IOVA, map/unmap
陪伴 43.2 节流式映射 — dma_map 的
IOMMU 后端就是在此建映射)
UNMANAGED: 用户态/虚拟机全权管理 (VFIO 49 章:
guest 拿到裸 domain, 自己填页表)
IDENTITY: 直通 (bus==phys, 兼容老驱动)
SVA (共享虚拟地址): 设备直接用进程页表
(进程 va == 设备 va, GPU/DSA 高级用法)
io_pgtable_ops 是页表格式的适配层:
ARM SMMU 的 LPAE 表 / x86 VT-d 的一级二级表 /
RISC-V IOMMU 表 — core 只见统一 map/unmap
43.3.2 IOVA 分配器与 DMA 重映射路径
// include/linux/iova.h:28
struct iova_domain { ... /* IOVA 的区间分配器 (伙伴式) */ };
开 IOMMU 后 dma_map 的完整路径 (43.1.3 节第 3 步展开):
dma_map_single → IOMMU 后端:
[1] alloc_iova: 从设备 IOVA 空间分配一段 iova
(iova_domain, 与 18.3 节 buddy 同构的区间管理)
[2] iommu_map(domain, iova, pa, size, IOMMU_READ|WRITE)
→ 页表格式适配 → 写入该设备域的 IOMMU 页表
→ IOMMU 缓存 (iotlb) 刷新
[3] 返回 iova 作 dma_handle
dma_unmap → iommu_unmap + 放回 iova 池
获得的能力:
- 隔离: 设备只能访问映射过的页 (恶意/错误 DMA
触发 IOMMU fault → 记账+禁设备, 不再是内存损坏)
- 重映射: 借 DMA 重映射做"跨 NUMA 镜像/加密内存
中转" (swiotlb 与 iommu 组合)
- 虚拟化: 49 章 VFIO 直接把 UNMANAGED domain
交给 guest — EPT(49.4)+IOMMU 两级受控
43.3.3 架构实现与默认策略
| 架构/平台 | IOMMU | 页表 | 特点 |
|---|---|---|---|
| Intel | VT-d (intel-iommu) | 一级+二级 | 大规模 DMA 重映射、SVA |
| AMD | AMD-Vi (amd-iommu) | v2 表 | 同级能力 |
| ARM64 | SMMUv3 | LPAE 风格 | 与 GIC-ITS 同代设计(42.5.2) |
| RISC-V | RISC-V IOMMU | 规范表 | 与 IMSIC/AIA 配套 |
默认策略 (iommu= 参数可调):
服务器/桌面: 设备默认 DMA domain (受控翻译)
passthrough=1: 全部 IDENTITY (性能优先, 隔离降级)
容器/云: VFIO/设备插件按设备粒度分域
排查面: /sys/kernel/iommu_groups/*/ (组拓扑),
dmesg 的 "iommu: Add device" 与 fault 报告
第七部分的收束:设备模型(39)让设备被发现并绑定驱动,字符(40)/块(41)驱动承接数据面,中断(42)让设备说话,DMA(本章)让设备直取内存——四个子系统合起来就是"一台设备在 Linux 里的一生"。IOMMU 的三种域类型同时预告了第八至十部分的方向:SVA 连着高级用户态驱动,UNMANAGED 通向 49 章虚拟化,DMA domain 是一切的地基。
小结
IOMMU 以 iommu_domain(四类型:DMA/UNMANAGED/IDENTITY/SVA)为设备级地址空间,iommu_map/attach + io_pgtable 适配层吸收页表格式差异,IOVA 分配器以伙伴式区间管理支撑映射;开 IOMMU 后 dma_map 变为"分配 IOVA→建映射→返回",换来隔离(fault 化任意 DMA)、重映射与虚拟化根基。第七部分(设备驱动)五章节至此全部完成。