🎀Linux x86-64 IOMMU 详解(五)——Intel IOMMU 初始化流程
2023-10-17
| 2023-10-17
字数 2928阅读时长 8 分钟
type
status
date
slug
summary
tags
category
icon
password
URL

整体流程图

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

Intel IOMMU 初始化前的准备工作

函数调用树

其中的核心函数是 pci_iommu_alloc()。

pci_iommu_alloc()

下面展示该函数的代码。其中,for 循环内被注释的部分是原始代码,而为了调试方便,笔者对代码略作改动,在不改变其逻辑的前提下,输出一些关键信息。注:printk() 函数中的格式控制字符串 “%ps”,能够输出函数指针所指向的函数名的字符串。
首先看 for 循环之前的代码。在执行 pci_iommu_alloc() 之前,内核已经通过汇编指令,将 IOMMU 相关的启动函数,加载到 IOMMU Table 中。
那么,IOMMU Table 中到底有哪些启动函数呢?
根据上一篇文章所述,为了启用 Intel IOMMU,我们进行了如下配置:
在. config 文件中:
在启动参数文件中:
基于如上配置,编译并重启内核后,使用 dmesg 过滤输出信息:
结果如下:
notion image
在 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 函数呢?在上图中,只有两个。
  1. p->detect = pci_swiotlb_detect_4gb。该 detect 函数返回 1,之后调用对应的 early_init 函数,pci_swiotlb_init(),用于初始化 SWIOTLB。
  1. p->detect = detect_intel_iommu。该 detect 函数返回 1,但并没有对应的 early_init 函数(看截图最后一行,p->early_init is 0x0)。
看到这里,读者想必会有两个疑问:
  1. 我们不是已经在启动参数中指定使用 Intel IOMMU 了吗?按照之前文章的说法,Intel IOMMU 与 SWIOTLB 不能共存。那么这里为什么还会初始化 SWIOTLB?
  1. 既然 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。
notion image
重启内核,首先用:
确认可用物理内存确实为 1GB。
notion image
而后用:
查看内核日志,如下图所示。
notion image
这次我们可以看到,在可用内存不大于 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 个步骤,也正好对应本文开头流程图中用红色花括号和文字标注的内容。
notion image
 
  • 内存管理
  • iommu
  • dma
  • Linux x86-64 IOMMU 详解(四)——启用 Intel IOMMU 的配置Linux x86-64 IOMMU 详解(六)——Intel IOMMU 参与下的 DMA Coherent Mapping 流程
    Loading...