3. IOMMU数据结构:I/O页表、设备上下文与域

好,咱们今天聊聊IOMMU里最核心的三个数据结构。说实话,我刚接触IOMMU那会儿,最头疼的就是搞不清这几个东西之间的关系。I/O页表、设备上下文、域——这三个概念就像一家三口,各有各的职责,但又紧密配合。

3.1 I/O页表(IOPT)——地址翻译的“字典”

I/O页表,说白了就是一张地址映射表。它告诉IOMMU:“设备A想访问虚拟地址0x1000,你帮我翻译成物理地址0x8000。” 这和CPU的页表原理类似,但有个关键区别——CPU页表是给进程用的,而I/O页表是给设备用的。

我习惯把I/O页表理解成一本“字典”。设备发起DMA时,它手里拿的是虚拟地址(也叫I/O虚拟地址,IOVA),IOMMU就翻这本字典,找到对应的物理地址。

关键点: I/O页表支持多级结构,和CPU页表一样。比如Intel的IOMMU支持4级页表,每级页表项指向下一级或最终的物理页。

举个例子,一个简单的二级页表结构长这样:

// 伪代码:I/O页表查找过程
uint64_t iova_to_phys(struct iopt *iopt, uint64_t iova) {
    uint64_t l1_index = (iova >> 21) & 0x1FF;  // 一级索引
    uint64_t l2_index = (iova >> 12) & 0x1FF;  // 二级索引
    
    uint64_t *l1_table = iopt->l1_base;
    uint64_t l1_entry = l1_table[l1_index];
    
    uint64_t *l2_table = phys_to_virt(l1_entry & PAGE_MASK);
    uint64_t l2_entry = l2_table[l2_index];
    
    return (l2_entry & PAGE_MASK) + (iova & 0xFFF);
}

嗯,这里要注意:页表项里不光有物理地址,还有权限位。比如可读、可写、是否允许非缓存访问等。我在项目中遇到过一个问题——设备DMA写操作一直报错,查了半天发现是页表项里没设“可写”位。这种坑,你想想看,排查起来多费劲。

3.2 设备上下文(Device Context)——设备的“身份证”

设备上下文,我管它叫设备的“身份证”。每个PCIe设备在IOMMU里都有一个对应的上下文条目。这个条目记录了设备的基本信息,比如:

  • 设备所属的域(Domain)
  • 设备使用的I/O页表指针
  • 设备的能力标志(是否支持PASID、是否支持ATS等)

设备上下文通常存放在内存中的一个表格里,叫设备上下文表(Device Context Table)。IOMMU硬件通过设备的BDF号(Bus:Device.Function)来索引这个表。

我的经验: 设备上下文表的位置由IOMMU的寄存器指定。我记得有一次调试,发现设备上下文表的基地址寄存器被意外清零了,结果所有设备的DMA都失败。这种问题,说白了就是初始化顺序没搞对。

设备上下文的结构大致如下:

struct device_context {
    uint64_t domain_id;      // 所属域ID
    uint64_t iopt_ptr;       // I/O页表基地址
    uint32_t flags;          // 各种标志位
    uint32_t pasid_enabled;  // 是否启用PASID
    // ... 其他字段
};

3.3 域(Domain)——隔离的“沙盒”

域这个概念,我觉得是IOMMU里最优雅的设计。一个域就是一个独立的地址空间。你可以把多个设备放到同一个域里,它们共享同一套I/O页表。也可以让每个设备独享一个域,实现完全的地址隔离。

为什么会需要域?举个例子:虚拟化场景下,每个虚拟机(VM)通常对应一个域。VM里的所有设备(比如虚拟网卡、虚拟磁盘)都共享这个域的地址空间。这样,IOMMU就能保证VM A的设备无法访问VM B的内存。

避坑指南: 我曾经把两个不同安全级别的设备放到了同一个域里。结果一个设备可以通过DMA读取另一个设备的数据。嗯,这显然是个安全漏洞。所以,域的设计一定要遵循最小权限原则——不同安全级别的设备,坚决分到不同域。

域、设备上下文、I/O页表三者的关系,我用一张图来展示:

IOMMU核心数据结构关系图 域(Domain) 设备上下文 A 设备上下文 B 设备上下文 C I/O页表(IOPT) I/O页表(IOPT) 包含多个设备上下文 每个设备上下文指向一个I/O页表

从这张图能看出来:一个域可以包含多个设备上下文,每个设备上下文又指向一个I/O页表。实际应用中,同一个域里的设备上下文通常指向同一个I/O页表,这样它们就能共享地址空间。

3.4 三者如何协同工作

咱们捋一下整个流程:

  1. 设备发起DMA:设备发出一个内存访问请求,携带I/O虚拟地址(IOVA)。
  2. IOMMU查设备上下文:IOMMU根据设备的BDF号,找到对应的设备上下文条目。
  3. 获取I/O页表:从设备上下文里读出I/O页表的基地址。
  4. 地址翻译:IOMMU遍历I/O页表,把IOVA翻译成物理地址。
  5. 权限检查:检查页表项里的权限位,确认设备是否有权访问。
  6. 执行访问:通过后,IOMMU把物理地址发到内存控制器,完成DMA。
个人经验: 我在调试一个NVMe驱动时,发现DMA总是超时。后来用IOMMU的调试寄存器一查,发现设备上下文里的页表指针指向了一个无效地址。说白了,就是设备上下文初始化时没把页表地址写对。这种问题,用硬件调试工具一看就明白。

3.5 实际项目中的注意事项

最后,分享几个我在项目中踩过的坑:

  • 页表分配要连续:I/O页表的多级结构要求每级页表在物理内存中连续。用vmalloc分配的内存可能不连续,得用__get_free_pages或alloc_pages。
  • 设备上下文表要对齐:硬件要求设备上下文表的基地址按4KB对齐。我见过有人用kmalloc分配,结果地址没对齐,IOMMU直接报错。
  • 域的生命周期管理:域里的所有设备都移除后,才能释放域。否则,设备还在用,域却没了,那DMA就会访问到野指针。

嗯,这三个数据结构搞明白了,IOMMU的基本框架也就掌握了。下次咱们可以深入聊聊页表的具体格式和硬件加速的细节。


可可精品资料库