🧶Linux x86-64 IOMMU 详解(六)——Intel IOMMU 参与下的 DMA Coherent Mapping 流程
2023-10-17
| 2023-10-17
字数 2689阅读时长 7 分钟
type
status
date
slug
summary
tags
category
icon
password
URL

前情回顾

在上一篇文章中,我们详细介绍了 Intel IOMMU 的初始化流程,并耗费大量笔墨讲述了此过程中 Intel IOMMU 与 SWIOTLB 二虎相争的故事。最终,SWIOTLB 被禁用,而 Intel IOMMU 得以保留。现在,所有的 DMA 操作,都要经由 Intel IOMMU 了。本文将介绍 Intel IOMMU 在 DMA Coherent Mapping 过程中的作用。

整体流程图

下面这张流程图是对本文内容的概括。
notion image

intel_dma_ops 结构体

在 DMA 代码中,有一个重要的结构体声明,那就是 struct dma_map_ops。这个结构体包含了一系列函数指针,规定了各种 DMA 操作的函数。所有的硬件 IOMMU 都必须用相应的函数实例化 struct dma_map_ops 的函数指针。在 Intel IOMMU 中,这样实例化后的结构体,就是 intel_dma_ops。以下是 intel_dma_ops 的定义。
需要指出的是,intel_dma_ops 并未实例化 struct dma_map_ops 的所有函数指针——例如,它并未实例化任何 sync 函数,包括 sync_single_for_device、sync_single_for_cpu 等等。
本文将重点介绍第一个函数,即 alloc 函数——intel_alloc_coherent。它是 Intel IOMMU 用于实现 DMA Coherent Mapping 的函数。让我们看看,Intel IOMMU 的存在,会导致 DMA Coherent Mapping 出现哪些变化。

函数调用树

下面的函数调用树,清晰地描述了 Intel IOMMU 参与下的 DMA Coherent Mapping(下文称为 Intel IOMMU DMA Coherent Mapping)流程中涉及的主要函数。
上述函数调用树中,相同缩进的函数,可能为顺序结构,也可能在同一分支结构的不同分支上,读者可以自行结合代码理解。不过,笔者用注释标明了其中最重要的一个分支结构:函数 intel_alloc_coherent() 会根据函数 iommu_need_mapping() 的返回值——即设备是否需要 Intel IOMMU 映射,来决定 DMA Coherent Mapping 的实现方式。
如果设备不需要映射,那么走第一个分支,调用 dma_direct_alloc(),这完全是传统 DMA Coherent Mapping 流程。所谓 “传统” 是指,即使没有启用 Intel IOMMU,DMA Coherent Mapping 也会沿用这一流程。
如果设备需要映射,那么走第二个分支。实现映射的入口函数是__intel_map_single()。
接下来我们介绍 intel_alloc_coherent() 函数,读者可以结合代码理解上述分支。

intel_alloc_coherent() 函数

我们先从顶层函数 intel_alloc_coherent() 函数开始,其代码如下。笔者加了两行简单的注释,以标记两种 DMA Coherent Mapping 的实现流程。

iommu_need_mapping() 函数

现在我们分析这个关键的判断函数 iommu_need_mapping(),它判断设备是否需要进行 Intel IOMMU 映射,代码如下。
判断依据主要有二:
  1. 设备本身的 archdata.iommu 属性。iommu_need_mapping() 会调用 identity_mapping(),而后者的返回值,取决于设备的 archdata.iommu 属性。
  1. 设备的 dma_mask 属性,代表设备的 DMA 寻址能力。其中最关键的判断代码是下面两行,在笔者的机器上,其含义相当于:如果设备能够直接寻址的内存范围不小于 1GB,则不需要映射,反之则需要。从这个意义上来说,倒是与 SWIOTLB map 的触发条件非常相似。
__intel_map_single() 函数 ———————–
现在我们假定设备需要映射,进入 Intel IOMMU DMA Coherent Mapping 流程。
首先,内核会像传统流程那样,申请一段连续物理内存,用作 DMA Buffer,这就是函数 dma_alloc_from_contiguous() 和 alloc_pages() 的作用。
而后,调用__intel_map_single() 函数,完成映射。那么,映射的具体含义是什么呢?
在具体介绍此函数之前,我们有必要介绍一些 Intel IOMMU 涉及的概念。

IOVA

回顾本系列第一篇文章中的这张图,它解释了 IOMMU 得名的由来。
notion image
对虚拟内存机制略有耳闻的读者都知道,MMU(Memory Management Unit)是一个地址转译硬件,能够将 VA(Virtual Address)映射为 PA(Physical Address)。VA 是由谁来访问的呢?是用户进程。
IOMMU(I/O Memory Management Unit)的功能与 MMU 非常相似,也是将一个 “虚拟地址” 映射为 PA,只不过这个 “虚拟地址” 是设备进行 DMA 操作时访问的,因而称为 IOVA(I/O Virtual Address)。因此,IOMMU 的作用是将 IOVA 映射为 PA
需要注意的是,上图中使用的术语是 DMA Address,这是一个更为宽泛的概念——在不同的场合,DMA Address 有不同的表现形式。而在启用 Intel IOMMU 时,DMA Address 的表现形式就是 IOVA。

IOMMU 页表

我们知道,MMU 将 VA 映射为 PA,是通过页表(Page Table)来实现的。对于 Linux x86-64 操作系统,页表默认为 4 级。
Intel IOMMU 将 IOVA 转换为 PA 的过程,也是通过专用的页表来实现的,本文称为 IOMMU 页表。IOMMU 页表与 MMU 页表彼此独立,但原理基本相同,并且默认情况下也是 4 级页表。
下图展现了 IOMMU 页表将 IOVA 映射为 PA 的流程(基于 4K 标准页)。可以看出,它与 MMU 页表的地址转译流程,原理完全一致。
notion image

__intel_map_single() 函数代码

现在,我们可以开始介绍__intel_map_single() 函数了,其代码如下。笔者删除了非关键代码,并且对于其中的重要函数,用注释简要说明其功能。

intel_alloc_iova() 函数

顾名思义,这个函数申请一个 IOVA。我们截取前文函数调用树中从 intel_alloc_iova() 开始的部分,放在下面:
这个函数会调用 alloc_iova(),并且最后落到 kmem_cache_alloc() 函数,分配一个 struct iova 结构体。kmem_cache_alloc() 是从 slab 分配器申请结构体的入口函数。事实上,内核启动时,就通过 iova_cache_get() 函数,声明了 struct iova 结构体专用的 slab 分配器。

domain_pfn_mapping() 函数

这是__intel_map_single() 调用的另一个重要函数,它完成从 IOVA 到 PA 的映射。该函数最后会调用__domain_mapping()。这个函数的代码比较长,以下仅展示其最重要的部分:
上述代码中的 while 循环,即:
代表从页表的最高级逐级向下,直至到达 PTE 一级的过程。
在每一级中,函数都会分别计算 pteval 和 pte,二者分别由 PA 和 IOVA 对应区段转换而来。然后,下面这行代码将 pte->val 设置为 pteval,从而建立起从 IOVA(pte)到 PA(pteval)的映射关系。

Intel IOMMU 参与下的 DMA 操作流程

读者理解以上的 DMA alloc 过程后,接下来就很容易理解实际 DMA 的操作流程了。
DMA alloc 的结果是,intel_alloc_coherent() 将一个 IOVA 作为 DMA 地址,返回给设备。而后,设备就会向该 IOVA 发起 DMA 读写操作。此时,Intel IOMMU 就会根据 IOMMU 页表中已建立好的映射关系,将 IOVA 映射为 DMA Buffer 的物理地址 PA,从而完成 DMA 数据传输。
 
  • 内存管理
  • iommu
  • dma
  • Linux x86-64 IOMMU 详解(五)——Intel IOMMU 初始化流程[从零开始学Makefile]GCC介绍
    Loading...