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)、重映射与虚拟化根基。第七部分(设备驱动)五章节至此全部完成。