4. IOMMU地址转换:DMA地址转换流程、I/O虚拟地址(IOVA)管理、地址转换缓存(IOTLB)
好,咱们今天聊点硬核的。IOMMU地址转换,说白了就是给DMA设备装上一副“虚拟眼镜”。设备本来只能看到物理地址,现在通过IOMMU,它看到的是一套虚拟地址空间。这套机制,我当年在调试NVMe驱动的时候,可是被它折腾得不轻。
4.1 DMA地址转换流程:从设备请求到内存访问
一个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?三种情况:
- 页表修改后:比如你解除了某个IOVA映射,必须刷掉对应的IOTLB条目
- 设备切换域:设备从一个IOMMU域移到另一个域
- 全局刷新:某些架构下,修改了全局映射表
注意: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没刷对。