type
status
date
slug
summary
tags
category
icon
password
URL
整体流程图
长图预警!!!
下图完整展示了 Intel IOMMU 的初始化流程,是对本文所有内容的总结。只要看懂这张图,读者就能够完全理解 Intel IOMMU 的初始化流程。
接下来,笔者将按流程图的顺序,结合代码,介绍 Intel IOMMU 初始化流程的一些关键步骤。图中部分细节,本文可能并未提到,读者可结合代码自行理解。

Intel IOMMU 初始化前的准备工作
函数调用树
其中的核心函数是 pci_iommu_alloc()。
pci_iommu_alloc()
下面展示该函数的代码。其中,for 循环内被注释的部分是原始代码,而为了调试方便,笔者对代码略作改动,在不改变其逻辑的前提下,输出一些关键信息。注:printk() 函数中的格式控制字符串 “%ps”,能够输出函数指针所指向的函数名的字符串。
首先看 for 循环之前的代码。在执行 pci_iommu_alloc() 之前,内核已经通过汇编指令,将 IOMMU 相关的启动函数,加载到 IOMMU Table 中。
那么,IOMMU Table 中到底有哪些启动函数呢?
根据上一篇文章所述,为了启用 Intel IOMMU,我们进行了如下配置:
在. config 文件中:
在启动参数文件中:
基于如上配置,编译并重启内核后,使用 dmesg 过滤输出信息:
结果如下:

在 pci_iommu_alloc() 代码的 for 循环中,p 每次指向一个 IOMMU table entry,而每个 entry 包含 detect 和 early_init 两个函数指针(其实还有第三个函数指针,late_init,在本文末尾介绍 pci_iommu_init() 函数时会涉及到)。pci_iommu_alloc() 会先调用 p->detect,只有当该函数返回值大于 0 时,才会调用 p->early_init。
那么,哪些 detect 函数的返回值是正数,从而会调用对应的 early_init 函数呢?在上图中,只有两个。
- p->detect = pci_swiotlb_detect_4gb。该 detect 函数返回 1,之后调用对应的 early_init 函数,pci_swiotlb_init(),用于初始化 SWIOTLB。
- p->detect = detect_intel_iommu。该 detect 函数返回 1,但并没有对应的 early_init 函数(看截图最后一行,p->early_init is 0x0)。
看到这里,读者想必会有两个疑问:
- 我们不是已经在启动参数中指定使用 Intel IOMMU 了吗?按照之前文章的说法,Intel IOMMU 与 SWIOTLB 不能共存。那么这里为什么还会初始化 SWIOTLB?
- 既然 detect_intel_iommu() 没有对应的 early_init 函数,那么 Intel IOMMU 的初始化函数,是如何被调用的呢?
对于这两个疑问,接下来我们逐一解答。
为什么会初始化 SWIOTLB
对于第一个问题,我们需要看 SWIOTLB 的 detect 函数——pci_swiotlb_detect_4gb() 代码。
需要说明的是,在上述代码中,swiotlb 是一个全局变量,它决定了 SWIOTLB 是否被初始化。相关代码很简单:
可见,只有 swiotlb = 1 时,SWIOTLB 初始化函数 swiotlb_init() 才会被调用——这个函数在本系列第二篇中进行过详细介绍。在调用 pci_swiotlb_init() 之前,有若干函数可能会将 swiotlb 的值置为 1,上面提到的 pci_swiotlb_detect_4gb() 便是其中之一。
我们只需关注 pci_swiotlb_detect_4gb() 的第一个 if 语句:
对于第一个子条件,顾名思义,!no_iommu 显然为 true(我们已经启用了 Intel IOMMU,显然不是 No IOMMU)。重点解释一下第二个子条件。MAX_DMA32_PFN 是 32 位设备能够寻址到的最大页数。所以,如果 “物理内存中的页数> MAX_DMA32_PFN”,则将 swiotlb 置为 1。
对于条件 “物理内存中的页数> MAX_DMA32_PFN”,我们把不等式两边同时乘以 PAGE_SIZE(页的大小),这个条件实际上等价于:“物理内存 > 32 位设备能够寻址的最大内存”。
而我们知道,32 位设备能够寻址的最大内存为 232 = 4GB。因此,这个判断条件最终转换为:
“物理内存> 4GB”
现在就非常明确了:如果物理内存大于 4GB,那么 pci_swiotlb_detect_4gb() 就会将 swiotlb 置为 1,从而导致后续 swiotlb_init() 被调用,以初始化 SWIOTLB。
笔者用 free 命令查看自己机器的可用物理内存,确实大于 4GB。

重启内核,首先用:
确认可用物理内存确实为 1GB。

而后用:
查看内核日志,如下图所示。

这次我们可以看到,在可用内存不大于 4GB 时,detect 函数 pci_swiotlb_detect_4gb() 返回值是 0,从而不会调用 early_init 函数 pci_swiotlb_init(),因而不会初始化 SWIOTLB。
Intel IOMMU 的初始化函数是如何被调用的
现在解答第二个疑问。
虽然 detect 函数 detect_intel_iommu() 对应的 early_init 函数是空函数,不过,detect_intel_iommu() 函数会将 x86_init.iommu.iommu_init 设置为 intel_iommu_init,后者正是 Intel IOMMU 的初始化函数。
那么,intel_iommu_init() 何时会被调用呢?相关的调用流程如下:
以下展示 pci_iommu_init() 的代码。此处调用 x86_init.iommu.iommu_init(),实际上就是调用 intel_iommu_init()。
不能共存?
讲到这里,细心的读者会发现,笔者还是没有回答 “Intel IOMMU 与 SWIOTLB 不能共存” 这一疑问——根据上述分析,如果物理内存大于 4GB,那么 SWIOTLB 就会被初始化;而根据我们配置的启动参数,Intel IOMMU 也会被初始化。既然二者都被初始化,那它们不就共存了吗?
这时,我们就要引用一句古话:“一山难容二虎”。聪明的读者应该能立即理解此言的含义。不理解也没关系,请看后续章节的分析。
intel_iommu_init() 的主要工作
intel_iommu_init() 是 Intel IOMMU 的初始化函数,其主要的函数调用树如下:
这个函数完成了 Intel IOMMU 所必需数据结构的初始化工作,本文对此不展开介绍。重点关注最后一行:“SET swiotlb = 0”。这不是一个函数名,而只是一个行为:将全局变量 swiotlb 置为 0。以下展示相关代码,非常简单:
我们看到,如果. config 文件中 CONFIG_X86 和 CONFIG_SWIOTLB 都为 y(在我们的实验机器上确实如此),那么这个 if 判断就会执行。结合代码与注释,可以得知:如果系统没有检测到不可信设备(untrusted device),或者全局变量 intel_no_bounce 为 1,那么 swiotlb 就会被置为 0。
那么,这两个条件是否成立呢?
一般情况下,系统并不会加载不可信设备。在我们的实验机器上也是如此。因此,第一个条件是成立的。由于这个 if 语句是条件或,所以直接返回 true,“swiotlb = 0” 会被执行。
至于 intel_no_bounce,我们也顺带介绍一下。它是一个全局变量,默认值为 0,代表 Intel IOMMU 会用到 bounce buffer(就是先前文章提到的 SWIOTLB bounce 机制)。除非在启动参数中进行如下配置,才会将其置为 1,代表禁用 bounce buffer 机制:
我们并没有进行如此配置,所以它等于默认值 0。因此,如果对第二个条件进行判断,那么会返回 false——当然,根据 if 语句的短路原则,在第一个条件返回 true 的情况下,第二个条件根本不会进行判断。
释放已分配的 SWIOTLB Buffer 和 SWIOTLB 管理数据结构
是时候水到渠成地解释 “一山难容二虎” 的含义了。
上一节我们讲到,一般情况下,Intel IOMMU 初始化过程中,也就是函数 intel_iommu_init() 函数体内,会将全局变量 swiotlb 置为 0。前面我们已经介绍过 swiotlb 的作用——如果它为 0,那么内核后续就不会调用 swiotlb_init(),从而不会初始化 SWIOTLB。可是,假如内存大于 4GB,那么现在 SWIOTLB 都已经初始化完成,此时再将 swiotlb 置为 0,岂不是为时已晚?
答案就在如下函数调用流程中。
再次看 pci_iommu_init() 函数代码:
该函数在 for 循环中,会遍历 IOMMU table entry,调用对应的 p->late_init 函数。SWIOTLB 对应的 late_init 函数为 pci_swiotlb_late_init(),其代码如下:
很明显,当 swiotlb 为 0 时,函数 swiotlb_exit() 将会被调用。以下展示 swiotlb_exit() 的代码,它释放了已分配的 SWIOTLB Buffer 和所有 SWIOTLB 管理数据结构。
如此,SWIOTLB 便不复存在,只剩下 Intel IOMMU。
总结
我们用一张简单的流程图,描述 Intel IOMMU 初始化流程的主要步骤。实际上,这 5 个步骤,也正好对应本文开头流程图中用红色花括号和文字标注的内容。
