type
status
date
slug
summary
tags
category
icon
password
URL
SWIOTLB 概述
上一篇文章已经提到,IOMMU 的核心功能就是,实现在 low buffer 和 high buffer 之间的 sync,也就是内存内容的复制操作。

读者可能会想,内存的复制,在内核中,不就是调用 memcpy() 函数来实现的吗?没错,这就是本文要介绍的 IOMMU 的软件实现方式——SWIOTLB。之所以说是软件实现,是因为 sync 操作在底层正是调用 memcpy() 函数,这完全是软件实现的。
SWIOTLB 的作用在于,使得寻址能力较低、无法直接寻址到内核所分配的 DMA buffer 的那些设备,也能够进行 DMA 操作。记住这句话——它将贯穿全文。由此,我们对本文开头图片稍作修改,制作了一个 SWIOTLB 的实现版本。

在目前主流的 Linux 操作系统中,SWIOTLB 发挥作用的场合并不多见。这主要是由于以下原因:
- 现代的外部设备,通常都是 32 位或 64 位设备。64 位设备毫无疑问可以直接寻址整个物理内存空间;而 32 位设备能够直接寻址的范围也达到了 4G。如果操作系统运行内存不大于 4G,则所有内存都可以被这些设备直接寻址到,此时设备的 DMA 操作,就无需 SWIOTLB 的辅助。
- 相比硬件 IOMMU,SWIOTLB 存在 memcpy() 操作,需要 CPU 的参与,降低了效率,这是软件实现的固有弊端。
- 后面的文章将会提到,如果启动参数中同时启用 SWIOTLB 和硬件 IOMMU(即 Intel IOMMU),那么当 Linux 系统启动完成后,SWIOTLB 将会被禁用,而仅保留硬件 IOMMU。
DMA 的两种映射方式与 SWIOTLB 的关系
DMA 映射方式包含两种,一是 DMA Coherent Mapping(一致性 DMA 映射),二是 DMA Streaming Mapping(流式 DMA 映射)。关于这两种映射方式的区别,网上有很多详尽的资料,故本文并不展开介绍。
读者应当注意的是,在 Linux 4.0 及更高的版本,只有 DMA Streaming Mapping 有可能触发 SWIOTLB 机制,而 DMA Coherent Mapping 与 SWIOTLB 没有任何联系。之所以要强调 Linux 4.0 及更高版本,是因为,在 Linux 4.0 之前的版本,DMA Coherent Mapping 也会借助 SWIOTLB 来实现,而这一情况从 Linux 4.0 起就不复存在了。
SWIOTLB 部分术语解释
为了让读者更好地理解本文及其他与 SWIOTLB/DMA Streaming Mapping 有关的文章,笔者认为还是需要解释一下一些在 SWIOTLB 中经常出现的术语的含义。
map:直译为映射。本质上,map 是 DMA Streaming Mapping 中一系列函数的统称,它们反映了 DMA Streaming Mapping 的核心机制(下文称为 “map 函数”)。map 函数的典型代表是 dma_map_single() 和 dma_map_sg(),其基本流程是:
- 设备调用 map 函数。设备调用时,会提供一个或一组位于高内存地址的页(以下称为 “原始页”),作为需要被 map 的页。map 函数需要返回被 map 页的 DMA 地址。
- map 函数根据设备的寻址能力,并结合 SWIOTLB 启动参数,决定是否采取实际的映射行为。
- 如果设备能够直接寻址到原始页,则 map 函数不进行实际映射,而是直接返回原始页的 DMA 地址(在 x86 体系中,DMA 地址就等于物理地址,所以实际上返回的是原始页的物理地址)。
- 如果设备不能直接寻址到原始页,则 map 函数将执行实际映射——在 SWIOTLB buffer 中申请一个页(low buffer),而后将原始页(high buffer)的内容复制过来,最后将 SWIOTLB 中申请的页的物理地址,作为 DMA 地址,返回给设备。之后,low buffer 和 high buffer 之间需要进行 sync 操作以保证一致性。因此,这种实际映射会降低设备 DMA 效率。
- 如果启动参数指定了 “swiotlb=force”(后文将会讲到启用 SWIOTLB 的配置),那么正如其名,即使设备能够直接寻址到原始页,map 函数也会强制执行实际映射。
sync:直译为同步。在 SWIOTLB 机制中,如果 CPU / 设备向 high buffer/low buffer 写入内容,那么两段 buffer 的内容就会不一致,此时便需要进行 sync,将被写入的 buffer 的内容,复制到另一个 buffer。sync 的方式很简单,就是使用 memcpy() 函数。
bounce buffer:bounce 原意为 “弹跳”,这个词形象地描述了 low buffer 与 high buffer 之间数据 sync 的行为,因而可以看作对 SWIOTLB 机制更为直观的描述。可以这么说:bounce buffer 就是 map + sync,二者共同构成了 SWIOTLB 机制。
SWIOTLB 实现原理
SWIOTLB 的实现原理,并不难理解。
在内核启动过程中,会使用 memblock 分配器,从较低的内存中,预留出一段连续物理内存(默认为 64MB),用于 SWIOTLB(以下称为 SWIOTLB Buffer)。之所以要从低地址内存中分配 SWIOTLB Buffer,原因很简单,是为了保证那些位数较低、寻址范围较小的设备,也能够寻址到 SWIOTLB Buffer,从而实现 sync。
对于 SWIOTLB Buffer,内核会默认按照 2KB 的粒度,进行切分(至于为何不采用标准页大小 4KB,笔者也不甚了解)。每一段 2KB 的连续物理内存,称为一个 slab。
默认情况下,slab 数目 = 64MB / 2KB = 32K = 32768。
在预留出 SWIOTLB Buffer 后,接下来将会初始化 SWIOTLB 的核心管理数据结构,它是一个名为 io_tlb 的数组:
io_tlb_list 的每个元素,对应的正是一个 slab。
io_tlb_list[i] 代表:从 SWIOTLB Buffer 的第 i 个 slab 开始,有多少个连续 slab 是可用(空闲)的。这个值存在上限,用常量 IO_TLB_SEGSIZE 表示。IO_TLB_SEGSIZE 默认值为 128。 这个值是由 x86 PCIe 规范所确定的——x86 PCIe 硬件设计决定了,每次 DMA 读写操作中,有效数据长度不得超过 128 字节。
以下图为例。io_tlb_list[0] - [1] 均为 0,代表第 0、1 个 slab 已经被占用。io_tlb_list[2] = 5,代表从 SWIOTLB Buffer 的第 2 个 slab 开始,有连续 5 个 slab 是可用的,即下标为 2-6 的 slab 是可用的。由此可以推断,io_tlb_list[2]-[6] 的值分别为 4、3、2、1。io_tlb_list[7] 一定为 0,即第 7 个 slab 必然已被占用;否则,io_tlb_list[2] 应至少为 6。

io_tlb_list[i] 的最大值为 IO_TLB_SEGSIZE,即 128。我们可以把每 128 个连续 slab 看成一组。假定一个极端场景:SWIOTLB Buffer 中所有 slab 都是空闲的。那么,此时 io_tlb_list 各元素的值将会是:
例如:
io_tlb_list[0] = 128
io_tlb_list[1] = 127
io_tlb_list[127] = 1
io_tlb_list[128] = 128
IO_TLB_SEGSIZE 限制了 DMA Streaming Mapping 的最大可申请内存 —— 128 * 2KB = 256KB,也就是说,每次 DMA Streaming Mapping 申请,不得超过 256KB 的内存。如果确实需要申请超过 256KB 的内存呢?那么请使用 DMA Coherent Mapping,这才是适用于较大内存申请的 DMA 方式。
当内核接收到 DMA Streaming Mapping 请求时,如果判定需要 map(是否需要 map,取决于启动参数和设备寻址能力,详见最后一部分展示的函数 dma_direct_map_page() 代码),则会将请求的 size(以字节为单位)换算为 slab 数目 n(向上取整)。而后,内核从上一次查找时停下的位置开始,沿下标递增方向查找(若已经到达数组末尾,则回到数组开头,继续查找)。若找到一个下标 k,使得 io_tlb_list[k] >= n 成立,则表明找到需要分配的区段,查找停止。之后,内核将会更新 io_tlb_list 中对应下标元素的值。最后,返回这连续 n 个 slab 的起始地址,之后允许设备在该区段上进行 DMA 操作。如果遍历完 io_tlb_list,仍未找到符合条件的 k(遍历完的条件是,先到达数组末尾,而后从头查找,最后又回到了初始查找的下标),则拒绝此次 DMA 请求。
仍以上图场景为例。假设此时查找起始位置为下标 2。内核接收到一个需要 sync 的 DMA Streaming Mapping 请求,其长度被换算为 4 个 slab。由于 io_tlb_list[2] = 5,5 > 4,因此找到需要分配的起始 slab。此时,内核会将 io_tlb_list[2]-[5]均置为 0,表明它们已被分配。下次查找的起始下标变为 6。最后,将下标为 2 的 slab 起始地址返回给设备,设备可以占用下标 [2, 5] 区间的 slab(即 low buffer),用于 DMA。
如果其他条件不变,长度被换算为 6 个 slab 呢?显然,io_tlb_list[2] = 5,5 < 6,因而下标为 2 的 slab 无法满足需求,需要继续查找。下一个查找的下标,并不是 3,而是直接跳到 2 + 5 + 1 = 8。之后重复此步骤。
启用 SWIOTLB 的配置
Linux 内核默认是禁用 SWIOTLB 的。如需启用,则需要分别修改. config 文件和启动参数文件(在主流的 Linux 发行版本中,启动参数文件就是 grub 文件)。
.config 文件
在. config 文件中进行如下配置:
启动参数文件
在启动参数文件中,推荐使用:
以下方式也可,但不推荐:
原因前文已经解释过——第二种方式会强制进行 SWIOTLB map,即使设备能够直接寻址到 DMA 地址也是如此。这一配置会从整体上降低操作系统的 DMA 效率——因为绝大部分现代设备具备较强的寻址能力,无需实际映射,强制映射将会降低它们的 DMA 效率。因此,建议读者在对 SWIOTLB 机制尚未透彻理解的情况下,使用第一种方式,而不要使用第二种方式。
读取 SWIOTLB 启动参数
SWIOTLB 的启动参数格式为:
解析 SWIOTLB 启动参数的函数是 setup_io_tlb_npages():
swiotlb 的第一个值是反直觉的——它代表 SWIOTLB Buffer 中 slab 的数目,而非 SWIOTLB Buffer 的起始地址或长度。由于每个 slab 长度为 2KB,因此,指定 slab 的数目,也就相当于指定 SWIOTLB Buffer 的长度。
如果不指定 slab 数目,那么在后续的函数 swiotlb_init() 中,SWIOTLB Buffer 长度会被设置为默认的 64MB。而后,默认 slab 数目 = 64MB / 2KB = 32K。
预留 SWIOTLB Buffer
终于要讲到 SWIOTLB 的初始化函数 swiotlb_init()。
事实上,swiotlb_init() 函数并不一定会被调用。在调用此函数之前,内核还会做很多准备工作,包括根据启动参数,以及机器硬件环境,设置一些与 SWIOTLB 相关的全局变量,它们最终将决定 swiotlb_init() 函数是否会被调用。这些内容并不会在本文中介绍,而是被放到本系列的后续文章中——前文已经提到,SWIOTLB 和 Intel IOMMU 并不会同时存留。笔者希望先向读者介绍它们各自的原理,之后再讨论内核在初始化过程中,选择保留 SWIOTLB 或 Intel IOMMU 的原因。
因此,在本文中,我们假定 swiotlb_init() 函数会被执行。
前文已经提到,内核在启动过程中,会使用 memblock 分配器,从较低的内存中,预留出 SWIOTLB Buffer。这正是 swiotlb_init() 函数的主要工作。
注意上述代码中的:
它要求内核需要从低内存地址预留 SWIOTLB Buffer,以保证即使是寻址能力较为有限的设备,也能够直接访问 SWIOTLB Buffer。
初始化 SWIOTLB 管理数据结构
函数 swiotlb_init() 会调用 swiotlb_init_with_tbl(),后者初始化 SWIOTLB 管理数据结构,主要包括两个数组:一是 io_tlb_list,前文已经详细介绍其功能;二是 io_tlb_orig_addr,该数组的作用是保存 sync 的物理地址——io_tlb_orig_addr[i] 保存第 i 个 slab 所映射的高地址,即本文开头的图片中 high buffer 的起始地址。
有了 io_tlb_orig_addr,内核就可以根据 SWIOTLB Buffer 的 slab 下标,很方便地找到需要进行 sync 的两段内存地址。
举个例子:假设设备向下标为 2 的 slab 写入了数据,现在需要 sync。则:
起始地址 src = SWIOTLB Buffer 起始地址 + 2 * 2K
目标地址 dst = io_tlb_orig_addr[2]
后续将这两个地址传给 memcpy() 函数即可。
SWIOTLB 的触发时机——DMA Streaming 与 SWIOTLB 的函数调用树
DMA Streaming 共有两条路径,它们在底层最终都会调用 swiotlb_map 函数,从而进入 SWIOTLB 的路径。
第一条路径是 dma_map_single,每次映射一个页:
第二条路径是 dma_map_sg,sg 是 “scatter-gather” 的缩写,表示每次映射一系列的页。
这里面比较重要的函数有 dma_direct_map_page() 和 swiotlb_tlb_map_single()。
dma_direct_map_page() 是高层函数,其目的是接收一个高地址的物理页,并将其映射到 SWIOTLB Buffer 的一个 slab 中。以下展示该函数的代码,其中关键在于调用 swiotlb_map() 前的判断条件,笔者已经用注释写明。
swiotlb_tbl_map_single() 则是寻找该 slab 的过程,原理前文已经详细解释过。这里只展示查找 slab 的算法对应的代码,读者可结合前文提到的 SWIOTLB 原理,来理解代码。
最后,展示一下 swiotlb_bounce() 函数代码。在删除了冗长而又无用的分支后(见代码注释),可以看到,这个函数是直接调用了 memcpy()。注意,memcpy() 需要 CPU 参与,这降低了效率,也与 DMA 的初衷背道而驰(CPU:还得我亲自出马?那我要这 DMA 有何用)。
正因为 SWIOTLB 会降低效率,因此,它只被用于少数的 DMA 场景中——具体来说,只有在 DMA Streaming Mapping,并且设备无法直接寻址到内核分配的 DMA 地址时,SWIOTLB 才会派上用场。