4. IOMMU地址转换:DMA地址转换流程、I/O虚拟地址(IOVA)管理、地址转换缓存(IOTLB)

好,咱们今天聊点硬核的。IOMMU地址转换,说白了就是给DMA设备装上一副“虚拟眼镜”。设备本来只能看到物理地址,现在通过IOMMU,它看到的是一套虚拟地址空间。这套机制,我当年在调试NVMe驱动的时候,可是被它折腾得不轻。

4.1 DMA地址转换流程:从设备请求到内存访问

一个DMA请求,从设备发出到真正访问内存,中间要过好几道关。我画了个流程图,你一看就明白。

设备发起DMA 携带IOVA地址 IOMMU接收请求 检查设备ID和域 查询IOTLB 命中?还是未命中? 命中 直接转换 IOVA → 物理地址 未命中 页表遍历 多级查询 访问物理内存 完成DMA数据传输 触发缺页异常

整个流程其实就三步:设备发请求、IOMMU查表、拿到物理地址。但这里有个关键点——IOTLB。如果缓存命中,那速度飞快;如果没命中,就得走页表遍历,那延迟就上去了。我在调优存储性能时,就特别关注IOTLB的命中率。

4.2 I/O虚拟地址(IOVA)管理:给设备分配“假地址”

IOVA,全称是I/O Virtual Address。说白了,就是给设备用的虚拟地址。你想想看,设备本来只能访问物理地址,多无趣啊。有了IOVA,设备也能享受虚拟化的好处了。

核心思想:IOVA管理就是维护一个地址池,设备要DMA时,从池子里分一段给它。用完再回收。

4.2.1 IOVA分配策略

我见过三种主流策略,各有各的适用场景:

策略名称 分配方式 适用场景 优缺点
Bottom-up 从低地址开始分配 32位设备,地址空间有限 兼容性好,但容易碎片化
Top-down 从高地址开始分配 64位设备,地址空间充足 减少碎片,但可能浪费高位地址
Best-fit 寻找最合适的空闲块 需要频繁分配/释放的场景 内存利用率高,但分配速度慢

我个人习惯用Top-down策略。为什么?因为大多数现代设备都支持64位地址,从高往低分配,能有效避免低地址空间的碎片化问题。我在一个NVMe SSD项目中,就因为这个选择,把IOVA分配失败率从5%降到了接近0。

4.2.2 IOVA管理的数据结构

内核里怎么管理IOVA?核心数据结构是红黑树。每个IOVA区域对应一个节点,按起始地址排序。分配时,在树上查找合适的空闲区间。

// Linux内核中的IOVA管理结构(简化版)
struct iova {
    unsigned long   start;    // IOVA起始地址
    unsigned long   end;      // IOVA结束地址
    struct rb_node  node;     // 红黑树节点
};

struct iova_domain {
    struct rb_root  rbroot;   // 红黑树根
    spinlock_t      lock;     // 保护锁
    unsigned long   start_pfn; // 起始页帧号
    unsigned long   end_pfn;   // 结束页帧号
};

避坑指南:我曾经在分配IOVA时忘记加锁,结果两个线程同时分配,拿到了重叠的地址。设备DMA时直接写乱了数据。嗯,从那以后,我每次操作IOVA域都记得拿spinlock。

4.3 地址转换缓存(IOTLB):加速DMA地址转换的利器

IOTLB,全称是I/O Translation Lookaside Buffer。它和CPU的TLB很像,但服务于IOMMU。每次地址转换结果,都会缓存到IOTLB里。下次同样的IOVA过来,直接命中,省去页表遍历的开销。

4.3.1 IOTLB的结构

IOTLB本质上是一个CAM(内容可寻址存储器)。它存储的是IOVA到物理地址的映射关系。每个条目包含:

  • IOVA标签:虚拟地址的高位部分
  • 物理地址:转换后的结果
  • 权限位:读、写、执行权限
  • ASID(地址空间标识符):区分不同设备的地址空间

4.3.2 IOTLB的刷新策略

什么时候需要刷新IOTLB?三种情况:

  1. 页表修改后:比如你解除了某个IOVA映射,必须刷掉对应的IOTLB条目
  2. 设备切换域:设备从一个IOMMU域移到另一个域
  3. 全局刷新:某些架构下,修改了全局映射表

注意:IOTLB刷新是个昂贵操作。我见过一个案例,频繁刷新IOTLB导致DMA性能下降30%。后来改成批量刷新,性能才恢复正常。所以,能少刷就少刷,能批量就别单个刷。

4.3.3 IOTLB的硬件实现差异

不同厂商的IOTLB实现差别很大。我整理了一个对比表:

厂商 IOTLB大小 关联方式 刷新方式
Intel VT-d 64-512条目 全相联 寄存器触发
AMD-Vi 128-1024条目 组相联 命令队列
ARM SMMU 可配置 全相联/组相联 TLBI指令

你看,Intel的IOTLB相对较小,但全相联意味着命中率高。AMD的更大,但组相联可能冲突。ARM最灵活,但配置起来也最复杂。我在做ARM服务器项目时,就花了不少时间调SMMU的IOTLB参数。

4.4 实战经验:IOVA泄漏排查

最后分享一个我踩过的坑。有一次,系统跑着跑着,DMA突然报错。查了半天,发现是IOVA泄漏了——分配了没释放,地址池被耗尽。

排查方法很简单:

# 查看IOVA使用情况(Linux)
cat /sys/kernel/debug/iommu/iova_domain/allocations

# 或者用ftrace跟踪分配和释放
echo 'iommu_alloc_iova' > /sys/kernel/debug/tracing/set_ftrace_filter
echo 'iommu_free_iova' >> /sys/kernel/debug/tracing/set_ftrace_filter
cat /sys/kernel/debug/tracing/trace_pipe

通过ftrace,我发现某个驱动在卸载时没有调用iommu_dma_free_iova。修复后,问题解决。嗯,这种问题,说白了就是驱动开发者的疏忽。但作为系统工程师,你得有工具能定位到它。

好了,IOMMU地址转换的核心内容就这些。记住三个关键词:转换流程、IOVA管理、IOTLB缓存。下次遇到DMA问题,先想想是不是IOVA分配出了问题,或者IOTLB没刷对。