在计算机的世界里,数据传输就像是一场繁忙的物资搬运工作。

在早期,CPU 就如同一位勤劳的 “搬砖工”,亲力亲为地参与每一次数据的搬运。当外设(如网卡、磁盘等)需要与内存进行数据交换时,CPU 需要一条指令接着一条指令地将数据从内存读取到寄存器,再从寄存器写入到外设,反之亦然。这种传统的编程 I/O(PIO)方式,虽然能够完成数据传输任务,但效率极其低下。想象一下,CPU 本应专注于运行复杂的算法、处理系统调度等高价值任务,却不得不花费大量时间在重复性的数据搬运上,就如同让一位高级工程师天天去做体力搬运工作,实在是大材小用。

而 DMA 技术的出现,就像是为数据传输配备了一位 “专属快递员”。DMA,即 Direct Memory Access(直接内存访问),它允许外部设备绕过 CPU,直接与内存进行高速数据交换。当有数据传输需求时,CPU 只需向 DMA 控制器下达 “任务清单”,包括源地址、目标地址、传输数据量以及传输方向等参数,然后 DMA 控制器就会接管系统总线,独立完成数据的搬运工作。在这个过程中,CPU 可以去执行其他更重要的任务,大大提高了系统的整体效率。

以网卡为例,当我们的计算机通过网络接收数据时,千兆甚至万兆网络接口卡每秒要处理大量的数据包。如果没有 DMA,CPU 需要频繁地响应网卡中断,逐个读取数据包并写入内存,这将导致 CPU 负载急剧升高,系统性能大幅下降。而借助 DMA,网卡可以直接将接收到的数据包写入内核缓冲区,CPU 只需在传输完成后进行一些简单的处理,极大地减轻了 CPU 的负担,提高了网络通信的速度。

再看磁盘 I/O,磁盘控制器使用 DMA 来读写文件系统缓存。当我们从硬盘读取一个大文件时,DMA 可以直接在硬盘和内存之间传输数据,避免了 CPU 介入每个数据块的传输过程,使得文件读取速度大幅提升。同样,在图形处理领域,现代显卡通过 DMA 传输显存数据,能够快速地将渲染所需的数据从内存传输到显卡,从而实现流畅的图形显示和高效的视频编辑等操作。

可以说,DMA 技术是 Linux 高性能 I/O 的 “隐形功臣”,它在幕后默默地提升着系统的性能,使得各种高性能的应用场景成为可能。在接下来的内容中,我们将深入探讨 Linux DMA 技术的原理、实现机制以及在实际开发中的应用。

一、Linux DMA 的底层逻辑

1.1 DMA 的核心使命

Linux DMA 子系统肩负着一项核心使命,那就是在 CPU 可见的虚拟 / 物理地址与设备可见的总线地址(DMA 地址)之间,构建起一座稳固且高效的桥梁。这看似简单的任务,实则面临着诸多复杂而棘手的技术挑战,其中最为关键的有以下三大难题。

首先是地址转换难题。不同设备的寻址能力千差万别,它们通常只能访问有限的物理地址范围,这个范围由设备的 dma_mask 所定义。而内核在分配内存时,可能会将内存分配到高地址区域,这就导致设备无法直接访问这些内存。DMA 子系统必须精准地判断设备的寻址能力,通过巧妙的地址转换机制,确保提供给设备的地址在其可访问的范围之内。例如,在一些 32 位设备中,dma_mask 可能只能覆盖较低的 4GB 地址空间,如果内核分配的内存位于 4GB 以上的高地址区域,DMA 子系统就需要采取特殊的转换方式,让设备能够 “看到” 并访问这些内存 。

缓存一致性问题也不容小觑。在现代计算机系统中,CPU 为了提高数据访问速度,普遍配备了多级高速缓存。当 CPU 访问内存时,首先会在缓存中查找数据,如果缓存中存在(即缓存命中),就直接从缓存中读取或写入数据;只有在缓存未命中时,才会访问主存。而 DMA 传输是直接访问物理内存,这就可能导致一个问题:如果 CPU 修改了缓存中的数据,但尚未将这些数据写回到主存,此时设备通过 DMA 访问主存,读取到的就是旧数据,从而引发数据不一致的错误。为了解决这个问题,DMA 子系统必须提供有效的缓存一致性机制,确保 CPU 缓存和主存之间的数据同步。

内存连续性问题同样困扰着 DMA 的实现。一些老式设备或者对内存访问有特殊要求的设备,要求 DMA 缓冲区在物理内存上必须是连续的。然而,内核的页分配器(如伙伴系统)在系统运行一段时间后,由于内存的频繁分配和释放,会产生内存碎片,很难再分配出大块连续的物理内存。这就需要 DMA 子系统寻找其他解决方案,以满足设备对连续内存的需求。

1.2 四类 DMA 内存类型

为了应对上述复杂多样的挑战,满足不同设备和应用场景的需求,Linux 内核精心设计了四类 DMA 内存类型,每一种类型都犹如一把独特的 “钥匙”,精准适配特定的场景。

1.2.1 一致性 DMA 内存(Coherent DMA Memory)

一致性 DMA 内存就像是 CPU 和设备之间的 “实时共享空间”。它的工作原理基于非缓存(uncached)或写通(write - combining)模式。在这种模式下,无论是 CPU 对内存的写入,还是设备对内存的写入,都会立即对另一方可见,就好像两者直接共享了同一份数据,无需任何额外的同步操作,硬件自动保证了数据的一致性。

在代码实现上,我们可以通过 dma_alloc_coherent () 函数来分配一致性 DMA 内存,使用 dma_free_coherent () 函数进行释放。例如:

#include <linux/dma-mapping.h>
#include <linux/device.h>


static int my_probe(struct device *dev) {
    dma_addr_t dma_handle;
    size_t size = 1024;
    void *cpu_addr;
    // 分配DMA缓冲区
    cpu_addr = dma_alloc_coherent(dev, size, &dma_handle, GFP_KERNEL);
    if (!cpu_addr) {
        dev_err(dev, "DMA alloc failed\n");
        return -ENOMEM;
    }
    // 使用CPU地址和DMA地址
    dev_info(dev, "CPU addr: %p, DMA addr: %pad\n", cpu_addr, &dma_handle);
    // 释放DMA缓冲区
    dma_free_coherent(dev, size, cpu_addr, dma_handle);
    return 0;
}

这种内存类型适用于一些小块数据且频繁交互的场景,比如设备的控制结构体、描述符环等。这些数据需要在 CPU 和设备之间频繁传递,并且对数据的一致性要求极高,一致性 DMA 内存能够很好地满足这些需求。不过,由于其非缓存的特性,数据访问速度相对较慢,这也是在使用时需要权衡的地方。

1.2.2 流式 DMA(Streaming DMA)

流式 DMA 是 Linux 系统中最为常用的 DMA 类型,它就像是一条高效的数据传输 “流水线”。内核为其分配的是普通的、可缓存的内存,这意味着它可以充分利用 CPU 缓存的优势,对于大块数据的传输性能表现尤为出色。

在进行 DMA 传输前和传输后,驱动程序需要扮演一个 “协调者” 的角色,显式地调用同步函数。在传输前,调用 dma_sync_single_for_device () 函数,通知 CPU 将缓存中的数据写回主存并失效缓存,确保设备读取到的是最新的数据;传输完成后,调用 dma_sync_single_for_cpu () 函数,通知 CPU 预取设备写入主存的数据到缓存,保证 CPU 后续访问的数据是最新的。通过这种方式,有效地解决了缓存一致性问题。

在 API 使用上,我们通过 dma_map_single () 函数来创建流式 DMA 映射,返回设备可访问的 DMA 地址;使用 dma_unmap_single () 函数来销毁映射。结合同步函数,其典型的使用方式如下:

dma_addr_t dma_handle;
dma_handle = dma_map_single(dev, buf, size, direction);
if (dma_mapping_error(dev, dma_handle)) {
    // 错误处理
}
// 调用同步函数,准备设备访问
dma_sync_single_for_device(dev, dma_handle, size, direction);
// 使用dma_handle进行DMA传输
// 传输完成后,调用同步函数,准备CPU访问
dma_sync_single_for_cpu(dev, dma_handle, size, direction);
dma_unmap_single(dev, dma_handle, size, direction);

流式 DMA 适用于许多场景,比如文件系统的读写操作、网络数据包的收发等。在这些场景中,往往需要传输大量的数据,流式 DMA 利用 CPU 缓存的特性,能够显著提高数据传输的效率。

1.2.3 直接映射(Direct Mapping)

直接映射堪称 DMA 操作的 “黄金路径”,一旦条件满足,它就能实现数据的高速、零拷贝传输。当设备的 dma_mask 足够大,能够覆盖整个物理内存时,物理地址(PA)就可以直接作为 DMA 地址(DA)使用,无需进行任何复杂的地址转换。这就好比在内存和设备之间建立了一条 “直达通道”,数据可以直接在两者之间快速传输,实现了真正意义上的零拷贝,大大提高了数据传输的性能。

要实现直接映射,设备必须具备足够宽的地址总线,通常是 64 位,这样才能支持对整个物理内存的寻址。或者,系统通过 I/O 内存管理单元(IOMMU)提供了平坦的 I/O 虚拟地址(IOVA)空间,同样可以满足直接映射的条件。在 Linux 内核中,直接映射的逻辑主要由 direct.c 模块负责处理。这种方式虽然性能极高,但对设备和系统的硬件条件要求也较为苛刻。

1.2.4 SWIOTLB:硬件 IOMMU 缺失时的 “后备方案”

当设备的 dma_mask 较小,例如只能访问低 32 位地址,而内核分配的内存却位于高地址时,直接映射就无法实现了。此时,SWIOTLB(Software I/O TLB)就像一位 “救火队员”,作为软件层面的 “bounce buffer”(弹跳缓冲区)机制介入。

SWIOTLB 会预先在低地址空间分配一块内存池,当需要映射高地址内存时,它会先将数据从高地址拷贝到低地址的 bounce buffer 中,然后将 bounce buffer 的地址交给设备进行访问。传输完成后,再根据数据传输的方向,将数据从 bounce buffer 拷贝回高地址内存。虽然这种方式解决了设备无法访问高地址内存的问题,但不可避免地带来了额外的数据拷贝开销,导致性能显著下降。因此,在实际应用中,应尽量避免使用 SWIOTLB,只有在硬件 IOMMU 不可用的情况下,才将其作为一种兼容性保障的后备方案。

1.3 核心数据结构:支撑 DMA 子系统的 “骨架”

在 Linux DMA 子系统中,有两个核心数据结构起着至关重要的作用,它们就像是支撑整个系统的 “骨架”,确保了 DMA 功能的高效实现。

1.3.1 struct device:设备 DMA 能力的 “信息库”

struct device 是 Linux 设备模型的核心结构,它就像是一个设备的 “信息宝库”,其中包含了丰富的 DMA 相关信息。在这个结构体中,dma_ops 字段是一个指向 dma_map_ops 结构的指针,它就像是一把 “钥匙”,决定了该设备使用哪种 DMA 实现方式,比如是直接映射、IOMMU 还是 SWIOTLB 等。dma_mask 和 coherent_dma_mask 字段则定义了设备能够寻址的最大物理地址,这对于 DMA 子系统在进行地址转换和内存分配时,判断设备的寻址能力至关重要。

cma_area 字段指向为该设备预留的连续内存分配器(CMA)区域,CMA 区域主要用于为设备提供大块连续的物理内存,满足一些对内存连续性有要求的设备需求。dma_io_tlb_mem 字段则指向 SWIOTLB 内存池,当设备需要使用 SWIOTLB 机制时,该字段就会发挥作用。总之,struct device 结构体承载了设备与 DMA 子系统交互的关键信息,是驱动程序与 DMA 子系统沟通的重要桥梁。

1.3.2 struct dma_map_ops:实现 DMA 多态性的 “函数指针表”

struct dma_map_ops 是 DMA 子系统实现多态性的关键所在,它就像是一个 “函数指针表”,定义了一系列标准的 DMA 操作接口,如 alloc、free、map_page、unmap_page、sync_single_for_cpu 等。不同的底层实现,比如 direct.c、swiotlb.c 或者 IOMMU 驱动,会根据自身的特点填充这个结构体,提供各自的实现方式。

上层驱动程序在进行 DMA 操作时,并不需要关心具体的硬件实现细节,只需要通过 dev->dma_ops 间接调用这些接口即可。这种设计模式实现了驱动程序与具体硬件的解耦,使得驱动开发者可以更加专注于功能的实现,而无需花费大量精力去了解底层硬件的差异,大大提高了代码的可移植性和可维护性。

1.4 Linux DMA 子系统目录结构:模块化设计一览

Linux 内核的 DMA 子系统采用了模块化的设计理念,其代码结构清晰,分工明确。在 Linux 6.6 内核中,DMA 子系统的核心代码主要位于 kernel/dma/ 目录下,各个文件各司其职,协同工作。

mapping.c 文件是 DMA 子系统的通用接口层,它就像是一个 “总调度员”,是驱动程序与 DMA 子系统直接交互的入口。所有对外的 dma_* API 都在这个文件中实现,并且它负责根据设备的 dma_ops 和配置信息,将驱动程序的请求准确无误地分发到正确的底层实现模块,无论是直接映射、SWIOTLB 还是 IOMMU。

direct.c 和 direct.h 文件则专注于直接映射的实现,它们处理物理地址可以直接作为 DMA 地址的 “快速路径”。同时,这两个文件中还包含了对 CMA 和 DMA 原子池的调用逻辑,为直接映射提供了必要的支持。

swiotlb.c 文件主要负责软件 IOTLB 的实现,它完整地实现了 bounce buffer 机制,包括内存池的管理、slot 的分配以及数据的拷贝(bounce)等操作,是 SWIOTLB 功能的核心实现文件。

coherent.c 文件承担着一致性内存管理的重任,它主要处理通过设备树(reserved - memory)或平台代码为特定设备预留的一致性内存池,确保一致性 DMA 内存的正确分配和使用。

contiguous.c 文件实现了 CMA(Contiguous Memory Allocator)的集成,提供了从 CMA 区域分配大块物理连续内存的接口,供 direct.c 等模块在需要时调用,以满足设备对连续内存的需求。

pool.c 文件负责 DMA 原子池的管理,为那些不能睡眠的原子上下文,比如中断处理程序,提供小块 DMA 内存的快速分配服务,确保在这些特殊场景下 DMA 操作的高效进行。

通过对这些文件的协同分析,我们可以清晰地看到 Linux DMA 子系统的模块化架构,各个模块之间既相互独立又紧密协作,共同构建了一个高效、灵活的 DMA 管理系统。

往期文章推荐

不懂 内存管理,别再说你懂 Linux 内核了

为什么很多人劝退学 C++,但大厂核心岗位还是要 C++?

【大厂标准】Linux C/C++ 后端进阶学习路线

音视频流媒体高级开发-学习路线

C++ Qt学习路线一条龙!(桌面开发&嵌入式开发)

Linux内核学习指南,硬核修炼手册

C/C++ 高频八股文面试题1000题(三)

手撕线程池:C++程序员的能力试金石

二、 核心框架解析:Linux DMA 的两大技术支柱

2.1 DMA Mapping:设备与内存的 “地址翻译官”

在 Linux 的 DMA 体系中,DMA Mapping 扮演着一个至关重要的角色,它就像是设备与内存之间的 “地址翻译官”。我们知道,CPU 使用虚拟地址来访问内存,这是一种经过内存管理单元(MMU)映射后的逻辑地址,主要是为了提供内存保护、地址空间隔离以及虚拟内存等功能,方便操作系统对内存进行高效管理。而设备(如网卡、硬盘控制器等)则是直接通过物理总线访问内存,它们只能识别总线地址(也称为 DMA 地址)。这就好比两个说不同语言的人,CPU 说的是 “虚拟地址语言”,设备说的是 “总线地址语言”,如果没有一个 “翻译官”,它们之间就无法顺畅地交流,数据传输也就无法完成 。

Linux 内核将 DMA Mapping 分为两大类,以适配不同的传输场景。

2.1.1 一致性映射(Coherent DMA Mapping)

一致性映射就像是为 CPU 和设备搭建了一座 “实时同步桥梁”。它会分配一块特殊的内存,这块内存同时被 CPU 和 DMA 设备访问,并且内核会保证该内存的缓存一致性。也就是说,无论 CPU 写入数据,还是 DMA 设备读取或写入数据,双方都能立即看到最新的数据,就好像它们在共享同一份实时更新的文档。

在代码实现上,我们可以使用 dma_alloc_coherent 函数来分配一致性 DMA 内存。例如:

#include <linux/dma-mapping.h>
#include <linux/device.h>


static int my_driver_probe(struct device *dev) {
    dma_addr_t dma_handle;
    size_t size = 1024; // 分配1KB内存
    void *cpu_addr = dma_alloc_coherent(dev, size, &dma_handle, GFP_KERNEL);
    if (!cpu_addr) {
        dev_err(dev, "Failed to allocate coherent DMA memory\n");
        return -ENOMEM;
    }
    // 使用cpu_addr进行CPU访问,dma_handle进行设备访问
    //...
    // 释放内存
    dma_free_coherent(dev, size, cpu_addr, dma_handle);
    return 0;
}

这种映射方式适用于一些对数据一致性要求极高的场景,比如 NVMe SSD 的提交队列(SQ)和完成队列(CQ)。这些队列长期存在,并且需要 CPU 与 NVMe SSD 控制器频繁交互,任何数据不一致都可能导致严重的错误,所以一致性映射能够很好地满足它们的需求。不过,由于一致性映射需要硬件支持缓存一致性协议,并且为了保证一致性,可能会有额外的性能开销,比如频繁地刷新缓存等。

2.1.2 流式映射(Streaming DMA Mapping)

流式映射则像是一个 “临时翻译服务”,它适用于一次性数据传输的场景,如文件读写、网络数据包收发等。在这种模式下,驱动程序在每次 DMA 传输前,动态地将一个已存在的内存缓冲区映射供 DMA 使用,并在传输结束后立即解除映射。

它不保证硬件 / CPU 缓存一致性,这就需要驱动程序在访问缓冲区之前,显式地调用同步函数来刷新缓存,从而同步缓冲区。例如,在数据从 CPU 传输到设备时,调用 dma_sync_single_for_device 函数,通知 CPU 将缓存中的数据写回主存,确保设备读取到的是最新数据;在数据从设备传输到 CPU 时,调用 dma_sync_single_for_cpu 函数,使 CPU 缓存失效,保证 CPU 读取的是设备写入主存的最新数据。

其 API 使用示例如下:

#include <linux/dma-mapping.h>
#include <linux/device.h>


static int my_driver_transfer_data(struct device *dev, void *buf, size_t size, enum dma_transfer_direction dir) {
    dma_addr_t dma_handle;
    dma_handle = dma_map_single(dev, buf, size, dir);
    if (dma_mapping_error(dev, dma_handle)) {
        dev_err(dev, "DMA map single failed\n");
        return -EINVAL;
    }
    // 调用同步函数,准备设备访问
    dma_sync_single_for_device(dev, dma_handle, size, dir);
    // 进行DMA传输
    //...
    // 传输完成后,调用同步函数,准备CPU访问
    dma_sync_single_for_cpu(dev, dma_handle, size, dir);
    dma_unmap_single(dev, dma_handle, size, dir);
    return 0;
}

流式映射的优点在于它可以使用普通的、可缓存的内存,能够充分利用 CPU 缓存的优势,对于大块数据的传输性能表现出色。同时,它的灵活性高,可以映射任意已有的内存缓冲区。但缺点是需要手动管理同步时机,如果忘记同步,很容易导致数据不一致的问题,并且其实现复杂度相对较高,需要开发者深入理解缓存一致性的原理。

2.2 DMA Engine:屏蔽硬件差异的 “通用抽象层”

DMA Engine 是 Linux 内核中为传统外设 DMA 传输设计的通用抽象框架,它就像是一个 “通用翻译器”,核心目标是屏蔽不同厂商 DMA 控制器的硬件差异,为上层外设驱动(如网卡、声卡、串口驱动)提供统一的 DMA 操作接口,极大地简化了驱动开发的过程,并提升了代码的复用性。

其代码实现位于内核源码的 drivers/dma/ 目录下,与 DMA Mapping 的代码位置(kernel/dma/)不同,这也从侧面反映了它们不同的职责和定位。在这个目录下,包含了各种具体 DMA 控制器的驱动代码,以及实现 DMA Engine 核心逻辑的文件。

以网卡驱动开发为例,在没有 DMA Engine 之前,不同厂商的网卡 DMA 控制器可能有不同的寄存器布局、操作方式和中断处理逻辑。驱动开发者需要针对每一款网卡,深入了解其硬件细节,编写大量与硬件紧密耦合的代码。而有了 DMA Engine 之后,驱动开发者只需要使用 DMA Engine 提供的统一接口,如 dma_request_channel 获取 DMA 通道、dmaengine_submit 提交 DMA 传输请求等,就可以实现数据的高效传输,无需关心底层硬件的具体实现。

例如,下面是一段简单的使用 DMA Engine 进行内存到内存数据拷贝的代码示例:

#include <linux/dmaengine.h>
#include <linux/device.h>


static int my_memcpy_with_dmaengine(struct device *dev, void *dst, void *src, size_t size) {
    struct dma_chan *chan;
    struct dma_slave_config config;
    struct dma_transaction tx;
    int ret;


    // 请求DMA通道
    chan = dma_request_channel(DMA_MEMCPY, NULL, NULL);
    if (!chan) {
        dev_err(dev, "Failed to request DMA channel\n");
        return -ENODEV;
    }


    // 配置DMA从设备
    memset(&config, 0, sizeof(config));
    config.direction = DMA_MEM_TO_MEM;
    config.src_addr = (dma_addr_t)src;
    config.dst_addr = (dma_addr_t)dst;
    config.src_maxburst = 16;
    config.dst_maxburst = 16;
    config.src_addr_width = DMA_SLAVE_BUSWIDTH_32BIT;
    config.dst_addr_width = DMA_SLAVE_BUSWIDTH_32BIT;
    ret = dmaengine_slave_config(chan, &config);
    if (ret) {
        dev_err(dev, "Failed to configure DMA slave\n");
        dma_release_channel(chan);
        return ret;
    }


    // 提交DMA传输请求
    memset(&tx, 0, sizeof(tx));
    tx.chained = false;
    tx.src_addr = (dma_addr_t)src;
    tx.dst_addr = (dma_addr_t)dst;
    tx.len = size;
    ret = dmaengine_submit(chan, &tx);
    if (ret) {
        dev_err(dev, "Failed to submit DMA transaction\n");
        dma_release_channel(chan);
        return ret;
    }


    // 启动DMA传输
    dma_async_issue_pending(chan);


    // 等待传输完成
    dmaengine_terminate_sync(chan);


    // 释放DMA通道
    dma_release_channel(chan);
    return 0;
}

通过这段代码,我们可以清晰地看到 DMA Engine 提供的接口的使用方式,以及它如何将复杂的硬件操作抽象化,使得驱动开发变得更加简洁和高效。

对比 DMA Mapping 和 DMA Engine,DMA Mapping 主要关注的是如何为设备提供可访问的内存,并处理好地址转换和缓存一致性等问题;而 DMA Engine 则侧重于提供统一的 DMA 操作接口,屏蔽硬件差异,实现数据的高效传输。两者相互配合,共同构建了 Linux 强大的 DMA 体系。

2.3 补充知识点:DMA 的传输模式与地址类型

2.3.1 传输模式:Block DMA 与 Scatter-Gather DMA

在 DMA 的数据传输过程中,有两种主要的传输模式,它们各自有着独特的特点和适用场景,就像是不同类型的运输工具,适用于不同的货物运输需求。

Block DMA

Block DMA,也被称为传统的普通 DMA,它就像是一辆只能运输整箱货物的卡车,一次 DMA 传输只能处理一个物理上连续的内存区域。例如,当我们需要从磁盘读取一个文件时,如果文件在磁盘上的存储位置是连续的,并且文件大小不超过内存中连续可用空间的大小,那么就可以使用 Block DMA 来进行传输。它的硬件复杂度相对较低,因为它只需要处理单一的连续内存块,控制逻辑较为简单。在一些对数据传输要求不高、数据存储相对连续的场景中,Block DMA 能够很好地发挥作用,比如早期的软盘驱动器数据传输,由于软盘的数据存储方式相对简单,使用 Block DMA 就可以满足其基本的传输需求。

Scatter-Gather DMA

Scatter-Gather DMA 则像是一支拥有多辆卡车的运输车队,单次 DMA 传输可以处理多个物理上不连续的内存区域。这些不连续的内存区域通过一个链表来描述,DMA 控制器得到链表首地址后,会根据链表的指引,依次传输下一块物理上连续的数据,直到所有数据传输完毕。这种传输模式的出现,主要是为了应对现代计算机系统中内存碎片化的问题。随着系统的运行,内存会被频繁地分配和释放,导致内存空间变得碎片化,很难找到大块连续的内存区域。而 Scatter-Gather DMA 能够将分散在不同内存区域的数据,通过链表组织起来,一次性完成传输,大大提高了数据传输的效率。

在网络数据包的传输中,由于数据包在内存中的存储可能是分散的,使用 Scatter-Gather DMA 就可以避免将分散的数据先拷贝到一个连续的内存区域,然后再进行传输的繁琐过程,直接从分散的内存区域中读取数据并传输,减少了内存拷贝的开销,提高了网络传输的性能。在文件系统操作中,当读取或写入一个大文件时,如果文件片段分布在磁盘的不同位置,Scatter-Gather DMA 也可以有效地提高 I/O 操作的速度。

除了这两种基本的传输模式,DMA 还使用 channel(通道)来实现并行传输。每个通道互相独立,拥有自己的寄存器组、请求 / 响应逻辑以及中断信号通路,可以独立服务一个外设或一个外设的某类传输任务。通过这种方式,多个外设可以同时进行 DMA 传输,提高了系统的整体数据传输能力。一些高性能服务器中,多个网卡可以通过各自独立的 DMA 通道,同时进行网络数据的收发,大大提升了服务器的网络吞吐能力。

另外,还可以通过时间分片和上下文快速切换,实现虚拟通道技术。这种技术可以在一个物理通道上,通过分时复用的方式,实现多个虚拟通道的功能,进一步提高了通道的利用率。在一些资源有限的嵌入式系统中,虚拟通道技术可以在不增加硬件成本的前提下,满足多个外设对 DMA 通道的需求。

2.3.2 三大地址:虚拟地址、物理地址、总线地址

在 Linux DMA 的世界里,有三类重要的地址概念,它们就像是三把不同的 “钥匙”,分别开启不同的 “大门”,理解它们之间的关系是掌握 DMA 技术的关键前提。

虚拟地址

虚拟地址是 CPU 使用的地址,它是一种逻辑地址,就像是一个房间的门牌号。在现代操作系统中,为了实现内存保护、地址空间隔离以及虚拟内存等功能,每个进程都被分配了一个独立的虚拟地址空间。例如,在 32 位系统中,每个进程可以拥有 4GB 的虚拟地址空间,进程在运行时,所有的内存访问操作都是基于虚拟地址进行的。虚拟地址通过内存管理单元(MMU)进行转换,最终映射到物理地址。这就好比我们通过门牌号找到对应的房间,但实际进入房间时,需要通过一个 “转换机制”(MMU)来找到房间的实际位置(物理地址)。

物理地址

物理地址是内存芯片上存储单元的实际地址,它是内存的真实位置,就像是房间在大楼中的实际楼层和房间号。当 CPU 需要访问内存时,经过 MMU 的地址转换,将虚拟地址转换为物理地址,才能真正访问到内存中的数据。物理地址是内存硬件层面的地址,与内存的物理布局和芯片特性密切相关。

总线地址

总线地址是设备(如 DMA 设备)使用的地址,用于在设备和内存之间进行数据传输。在没有 I/O 内存管理单元(IOMMU)的情况下,总线地址通常与物理地址一致,或者存在一个固定的偏移量。这是因为设备直接通过物理总线访问内存,所以它所使用的地址需要与内存的物理地址相对应。而在有 IOMMU 的情况下,IOMMU 会对总线地址进行地址映射,将设备使用的总线地址转换为物理地址,实现设备对内存的正确访问。这就好比设备通过一个 “中介”(IOMMU)来找到内存的实际位置,即使内存的物理地址发生了变化,设备只需要使用 IOMMU 映射后的总线地址,就可以正确地访问内存。

理解这三类地址的概念和它们之间的转换关系,对于深入理解 Linux DMA 的工作原理至关重要。在 DMA 传输过程中,需要将 CPU 使用的虚拟地址转换为设备可识别的总线地址,确保数据能够在设备和内存之间准确无误地传输。

三、 实战:从驱动开发到 AXI-DMA 优化

3.1 手把手教你写基础 DMA 驱动:核心步骤拆解

在 Linux 系统中开发基础的 DMA 驱动,就像是搭建一座稳固的桥梁,需要精心规划每一个步骤,确保数据能够在设备和内存之间顺畅传输。下面我们将逐步拆解核心步骤,帮助你掌握 DMA 驱动开发的精髓。

3.1.1 驱动初始化:DMA 控制器与设备配置

驱动初始化是 DMA 驱动开发的第一步,也是至关重要的一步。在这一步中,我们需要调用 init_dma () 函数来配置 DMA 控制器的各项参数,使其能够正常工作。这个过程就像是为 DMA 控制器设置 “工作指南”,明确它的工作方式和职责。

我们需要根据设备的具体需求,设置 DMA 控制器的源地址、目标地址、传输数据长度以及传输方向等关键参数。这些参数就像是为 DMA 控制器规划的 “运输路线”,决定了数据从哪里来,到哪里去,以及运输的数量和方向。例如:

static int init_dma(struct device *dev) {
    struct dma_device *dma_dev;
    struct dma_chan *chan;


    // 查找并获取DMA设备
    dma_dev = dma_find_device("my_dma_device", dev, NULL, NULL);
    if (!dma_dev) {
        dev_err(dev, "DMA device not found\n");
        return -ENODEV;
    }


    // 获取DMA通道
    chan = dma_request_channel(dma_dev, NULL, NULL);
    if (!chan) {
        dev_err(dev, "Failed to request DMA channel\n");
        dma_release_device(dma_dev);
        return -ENODEV;
    }


    // 设置dma_mask,指定设备的寻址能力
    if (!dma_set_mask_and_coherent(chan->device->dev, DMA_BIT_MASK(32))) {
        dev_err(dev, "DMA mask setting failed\n");
        dma_release_channel(chan);
        dma_release_device(dma_dev);
        return -EIO;
    }


    // 绑定设备与DMA通道
    dev_set_dma_ops(dev, &my_dma_ops, NULL);


    return 0;
}

在上述代码中,我们首先使用 dma_find_device 函数查找并获取指定的 DMA 设备,这就像是在众多设备中找到我们需要的那个 “运输工具”。然后,通过 dma_request_channel 函数获取 DMA 通道,这个通道就像是一条专门的数据运输通道,确保数据能够高效传输。接着,使用 dma_set_mask_and_coherent 函数设置 dma_mask,明确设备的寻址能力,避免出现地址访问错误。最后,通过 dev_set_dma_ops 函数将设备与 DMA 通道进行绑定,就像是将设备与运输通道连接起来,确保数据能够准确无误地传输到目标位置。

3.1.2 传输实现:参数设置与启停控制

完成驱动初始化后,接下来就是实现 DMA 传输的核心逻辑。这一步就像是启动一辆装满货物的卡车,需要精确设置各项参数,确保运输过程顺利进行。我们通过 start_dma_transfer () 函数来配置 DMA 传输的关键参数,包括源地址、目的地址和数据长度等。这些参数就像是为卡车规划的具体运输路线和货物数量,决定了数据传输的细节。例如:

static void start_dma_transfer(struct device *dev, void *src, void *dst, size_t len) {
    struct dma_chan *chan = dev_get_dma_chan(dev);
    struct dma_slave_config config;


    // 配置DMA从设备参数
    memset(&config, 0, sizeof(config));
    config.direction = DMA_MEM_TO_MEM;
    config.src_addr = (dma_addr_t)src;
    config.dst_addr = (dma_addr_t)dst;
    config.src_maxburst = 16;
    config.dst_maxburst = 16;
    config.src_addr_width = DMA_SLAVE_BUSWIDTH_32BIT;
    config.dst_addr_width = DMA_SLAVE_BUSWIDTH_32BIT;


    // 配置DMA从设备
    if (dmaengine_slave_config(chan, &config)) {
        dev_err(dev, "Failed to configure DMA slave\n");
        return;
    }


    // 提交DMA传输请求
    struct dma_transaction tx;
    memset(&tx, 0, sizeof(tx));
    tx.chained = false;
    tx.src_addr = (dma_addr_t)src;
    tx.dst_addr = (dma_addr_t)dst;
    tx.len = len;


    if (dmaengine_submit(chan, &tx)) {
        dev_err(dev, "Failed to submit DMA transaction\n");
        return;
    }


    // 启动DMA传输
    dma_async_issue_pending(chan);
}

在上述代码中,我们首先通过 dev_get_dma_chan 函数获取设备的 DMA 通道,这是数据传输的 “主干道”。然后,使用 dmaengine_slave_config 函数配置 DMA 从设备的参数,包括传输方向、源地址、目的地址以及地址宽度和突发传输大小等,确保设备能够正确接收和发送数据。接着,使用 dmaengine_submit 函数提交 DMA 传输请求,就像是向运输公司提交运输任务。最后,通过 dma_async_issue_pending 函数启动 DMA 传输,让数据开始在设备和内存之间传输。

在 DMA 传输完成后,我们还需要处理中断信号,以通知 CPU 传输已经结束。这就像是卡车到达目的地后,需要向发货人发送到货通知。我们可以通过设置中断处理函数来实现这一功能,例如:

static irqreturn_t my_dma_irq_handler(int irq, void *dev_id) {
    struct device *dev = (struct device *)dev_id;
    struct dma_chan *chan = dev_get_dma_chan(dev);


    // 检查DMA传输状态
    if (dmaengine_tx_status(chan, NULL, NULL) == DMA_COMPLETE) {
        // 传输完成,进行相应处理
        dev_info(dev, "DMA transfer completed\n");
    } else {
        // 传输出错,进行错误处理
        dev_err(dev, "DMA transfer error\n");
    }


    return IRQ_HANDLED;
}

在用户空间,我们可以通过 open () 和 ioctl () 函数与内核驱动进行交互,发起 DMA 传输请求。例如:

#include <stdio.h>
#include <fcntl.h>
#include <sys/ioctl.h>


#define MY_DMA_IOC_MAGIC 'Z'
#define MY_DMA_IOC_START_TRANSFER _IOWR(MY_DMA_IOC_MAGIC, 0, struct my_dma_transfer)


struct my_dma_transfer {
    void *src;
    void *dst;
    size_t len;
};


int main() {
    int fd = open("/dev/my_dma_device", O_RDWR);
    if (fd < 0) {
        perror("open");
        return 1;
    }


    struct my_dma_transfer transfer;
    transfer.src = (void *)0x10000000;
    transfer.dst = (void *)0x20000000;
    transfer.len = 1024;


    if (ioctl(fd, MY_DMA_IOC_START_TRANSFER, &transfer) < 0) {
        perror("ioctl");
    }


    close(fd);
    return 0;
}

在上述代码中,我们定义了一个自定义的 ioctl 命令 MY_DMA_IOC_START_TRANSFER,用于发起 DMA 传输请求。在 main 函数中,我们打开设备文件 /dev/my_dma_device,然后使用 ioctl 函数向内核驱动发送传输请求,传递源地址、目的地址和数据长度等参数。内核驱动接收到请求后,会根据这些参数启动 DMA 传输。

3.1.3 驱动卸载:资源释放与清理

当驱动不再使用时,我们需要进行驱动卸载操作,释放所有占用的系统资源,确保系统的稳定性和资源的有效利用。这就像是在运输任务完成后,需要将卡车归还,并清理运输过程中产生的垃圾。在驱动卸载阶段,我们首先需要调用 dma_free_coherent () 函数释放 DMA 内存,这就像是归还占用的仓库空间。例如:

static void my_driver_remove(struct device *dev) {
    void *cpu_addr;
    dma_addr_t dma_handle;


    // 获取之前分配的内存地址和DMA句柄
    // 假设之前的代码中已经将这些信息存储在设备的私有数据中
    struct my_driver_private *priv = dev_get_drvdata(dev);
    cpu_addr = priv->cpu_addr;
    dma_handle = priv->dma_handle;


    // 释放DMA内存
    dma_free_coherent(dev, priv->buffer_size, cpu_addr, dma_handle);


    // 解除地址映射
    // 假设之前使用了dma_map_single进行映射
    dma_unmap_single(dev, dma_handle, priv->buffer_size, DMA_TO_DEVICE);


    // 注销DMA通道
    struct dma_chan *chan = dev_get_dma_chan(dev);
    dma_release_channel(chan);


    // 注销中断
    free_irq(dev->irq, dev);


    // 清理设备私有数据
    kfree(priv);
}

在上述代码中,我们首先从设备的私有数据中获取之前分配的内存地址和 DMA 句柄,然后使用 dma_free_coherent 函数释放 DMA 内存,使用 dma_unmap_single 函数解除地址映射。接着,通过 dma_release_channel 函数注销 DMA 通道,就像是关闭数据运输通道。最后,使用 free_irq 函数注销中断,清理设备的私有数据,确保没有任何资源残留。规范释放资源对于驱动的稳定性和系统的健康运行至关重要,如果资源没有正确释放,可能会导致内存泄漏、系统崩溃等严重问题。

3.2 进阶实战:AXI-DMA 驱动优化的核心技巧

在嵌入式系统开发中,AXI-DMA(Advanced eXtensible Interface Direct Memory Access)因其高速的数据传输能力而被广泛应用。然而,要充分发挥 AXI-DMA 的性能优势,对其驱动进行优化是必不可少的。下面我们将深入探讨 AXI-DMA 驱动优化的核心技巧,帮助你提升系统的整体性能。

3.2.1 AXI-DMA 硬件架构:接口与缓冲区管理

AXI-DMA 的硬件架构是理解其工作原理和进行驱动优化的基础。它主要由 AXI 总线接口和 DMA 控制器组成,就像是一个高效的物流中心,负责在内存和外设之间快速传输数据。AXI 总线接口包括 AXI 读通道和 AXI 写通道,它们分别负责数据的读取和写入操作,就像是物流中心的进货通道和出货通道,分工明确,确保数据的高效传输。

在 AXI-DMA 的硬件架构中,流控制和缓冲区管理起着关键作用。流控制机制就像是交通警察,确保数据传输的顺序和速率,避免数据拥堵和丢失。缓冲区管理则涉及到缓冲区的创建、分配、映射以及在传输过程中的管理,直接影响着 DMA 操作的性能。缓冲区的大小、对齐方式等参数对传输效率有着重要影响。例如,较大的缓冲区可以减少数据传输的次数,但可能会占用更多的内存资源;而缓冲区的对齐方式不当,则可能导致数据访问错误,降低传输效率。因此,合理配置缓冲区是优化 AXI-DMA 驱动的基础。

3.2.2 性能调优三大方向:中断、内存与调度

在 Petalinux 环境下,我们可以从以下三个核心方向对 AXI-DMA 驱动进行性能调优,以实现数据的高效传输和系统资源的合理利用。

优化中断处理

在传统的 AXI-DMA 驱动中,每次数据传输完成都会产生一个中断信号,通知 CPU 进行处理。然而,频繁的中断响应会占用大量的 CPU 资源,降低系统的整体性能。为了优化这一问题,我们可以采用批量中断的方式,即将多个数据传输任务合并为一组,只有当这一组任务全部完成时,才产生一个中断信号。这样可以大大减少 CPU 的中断响应次数,提高系统的运行效率。

在驱动代码中,我们可以通过设置 DMA 控制器的相关寄存器,调整中断触发的条件和频率。例如,将多个较小的数据传输任务组合成一个较大的传输任务,在传输完成后统一触发中断。同时,我们还可以优化中断服务程序,减少其执行时间,使其能够快速处理中断事件,避免长时间占用 CPU 资源。

优化内存管理

内存管理是影响 AXI-DMA 性能的另一个重要因素。在 Linux 系统中,内存的分配和管理是一个复杂的过程,而 AXI-DMA 对内存的连续性和访问效率有着较高的要求。为了满足这一需求,我们可以利用连续内存分配器(CMA,Contiguous Memory Allocator)来分配连续的物理内存。CMA 就像是一个专门为 AXI-DMA 准备的内存仓库,它可以在系统启动时预留一块连续的内存区域,供 AXI-DMA 在需要时使用。

在 Petalinux 环境下,我们可以通过修改设备树文件,配置 CMA 内存区域的大小和属性。例如,在设备树文件中添加如下配置:

reserved-memory {
    #address-cells = <1>;
    #size-cells = <1>;
    ranges;


    cma_region: cma@0 {
        compatible = "shared-dma-pool";
        reg = <0x0 0x10000000>;  /* 1GB的CMA区域 */
        no-map;
    };
};

这样,系统启动时就会预留 1GB 的连续内存作为 CMA 区域,供 AXI-DMA 使用。在驱动代码中,我们可以通过调用相关的内核函数,从 CMA 区域中分配内存,确保 AXI-DMA 能够使用连续的物理内存进行数据传输,提高传输效率。

优化多通道任务调度

AXI-DMA 通常支持多个通道,每个通道可以独立进行数据传输。在多通道应用场景下,合理的任务调度可以充分利用硬件资源,提高系统的整体性能。我们可以根据不同任务的优先级和数据传输需求,动态分配通道资源,确保重要任务能够优先得到处理。

例如,在一个同时进行视频采集和网络数据传输的系统中,视频采集任务对实时性要求较高,我们可以将其分配到优先级较高的通道上,确保视频数据能够及时传输;而网络数据传输任务对实时性要求相对较低,可以分配到优先级较低的通道上。在驱动代码中,我们可以通过设置 DMA 通道的优先级和调度策略,实现多通道任务的高效调度。

在衡量 AXI-DMA 驱动优化的效果时,我们可以使用传输速率、CPU 利用率等指标。通过对比优化前后这些指标的变化,我们可以直观地评估优化措施的有效性,进而不断调整优化策略,实现 AXI-DMA 驱动性能的最大化。

3.3 实战踩坑:常见问题与排查方法

在 DMA 驱动开发的过程中,难免会遇到各种各样的问题,这些问题就像是路上的绊脚石,阻碍着我们实现高效的数据传输。下面我们将总结一些常见问题,并提供相应的排查方法,帮助你快速解决问题,顺利完成驱动开发。

缓存一致性导致的数据错乱

在 DMA 传输过程中,由于 CPU 缓存和内存之间的一致性问题,可能会出现数据错乱的情况。当 CPU 修改了缓存中的数据,但尚未将这些数据写回到内存时,DMA 设备直接从内存中读取数据,就会读取到旧数据,导致数据不一致。

为了解决这个问题,我们需要在 DMA 传输前后,正确调用同步函数,确保缓存和内存之间的数据一致性。在使用流式 DMA 映射时,在数据传输前,调用 dma_sync_single_for_device 函数,将 CPU 缓存中的数据写回到内存,并使缓存失效,确保设备读取到的是最新数据;在数据传输完成后,调用 dma_sync_single_for_cpu 函数,使 CPU 缓存失效,确保 CPU 读取的是设备写入内存的最新数据。

地址映射失败

地址映射是 DMA 驱动开发中的一个关键环节,如果地址映射失败,DMA 设备将无法正确访问内存,导致数据传输失败。地址映射失败的原因可能有多种,其中 dma_mask 配置不正确是一个常见的原因。dma_mask 定义了设备能够寻址的最大物理地址范围,如果 dma_mask 设置过小,设备将无法访问某些内存地址,从而导致地址映射失败。

当遇到地址映射失败的问题时,我们首先要检查 dma_mask 的配置是否正确。可以通过查看设备的硬件手册,确定设备的寻址能力,然后根据实际情况调整 dma_mask 的设置。此外,还需要检查内存分配和映射的相关代码,确保内存地址的合法性和映射的正确性。

SWIOTLB 性能瓶颈

在一些情况下,当设备的 dma_mask 较小,无法覆盖整个物理内存时,Linux 内核会使用 SWIOTLB(Software I/O TLB)来进行地址转换。SWIOTLB 通过在低地址空间分配一块 bounce buffer,将高地址内存的数据拷贝到 bounce buffer 中,然后让设备访问 bounce buffer。然而,这种方式会带来额外的数据拷贝开销,导致性能瓶颈。

为了解决 SWIOTLB 的性能瓶颈问题,我们应尽量启用 IOMMU(I/O Memory Management Unit)。IOMMU 可以提供硬件层面的地址转换功能,避免了 SWIOTLB 的软件拷贝开销,大大提高了数据传输的效率。如果硬件支持 IOMMU,我们可以通过修改内核配置,启用 IOMMU 功能,从而提升系统性能。

在排查问题时,日志分析是一个非常有效的工具。我们可以通过查看内核日志,获取 DMA 驱动运行时的详细信息,包括地址映射、传输状态、中断处理等。通过分析这些日志信息,我们可以快速定位问题所在,并采取相应的解决措施。例如,如果日志中出现 “DMA mapping error” 的错误提示,我们就可以根据提示信息,检查地址映射的相关代码和配置,找出 ## 四、 进阶优化与避坑:打造高性能 DMA 应用

4.1 缓存一致性优化:从原理到实践方案

在 DMA 传输中,缓存一致性问题是影响性能的关键因素之一。当 CPU 修改了缓存中的数据,而设备直接访问内存时,就可能读取到旧数据,导致数据不一致。这就好比两个人同时编辑一份文档,一个人在本地修改后没有保存,另一个人直接从服务器读取,就会拿到旧版本的文档。

为了解决这个问题,我们可以采用硬件自动同步和软件显式同步两种方式。硬件自动同步通常依赖于一致性内存,这种内存采用非缓存或写通模式,硬件会自动保证 CPU 和设备对内存的访问是一致的,就像两个人使用实时在线文档,任何一方的修改都会立即同步给对方。

而软件显式同步则是在流式 DMA 中,通过驱动程序在传输前后显式调用同步函数来实现。在数据从 CPU 传输到设备前,调用 dma_sync_single_for_device 函数,将 CPU 缓存中的数据写回内存并使缓存失效,确保设备读取到最新数据;传输完成后,调用 dma_sync_single_for_cpu 函数,使 CPU 缓存失效,保证 CPU 读取的是设备写入内存的最新数据。这就像是在文档传输前,手动保存并上传最新版本,传输后再从服务器下载最新版本。

在实际应用中,我们需要根据数据传输的特点来选择合适的方式。对于高频小数据的传输,由于数据量小且传输频繁,使用一致性内存可以简化编程模型,确保数据的实时一致性,就像频繁修改的小文件使用实时在线文档更方便。而对于大块批量数据的传输,流式 DMA 结合合理的同步时机可以充分利用 CPU 缓存,提高传输效率,同时避免过度同步导致的性能损耗。比如在传输大文件时,先将数据缓存到内存中,在传输完成后再进行一次同步,而不是每次小数据传输都进行同步,这样可以减少同步操作的开销。

4.2 内存分配策略:CMA、原子池与普通内存的选型

内存分配策略的选择对于 DMA 性能也有着重要影响。在不同的场景下,我们需要根据内存的连续性、分配的原子性以及性能要求等因素,合理选择 CMA(Contiguous Memory Allocator)、原子池(pool.c)与普通内存。

当需要大块连续物理内存时,CMA 是首选。例如在视频采集、图像处理等场景中,需要连续的内存空间来存储一帧帧的图像数据或视频流数据,CMA 可以在系统启动时预留一块连续的内存区域,供设备在需要时使用。在 Petalinux 环境下,我们可以通过修改设备树文件来配置 CMA 内存区域的大小和属性,如:

reserved-memory {
    #address-cells = <1>;
    #size-cells = <1>;
    ranges;


    cma_region: cma@0 {
        compatible = "shared-dma-pool";
        reg = <0x0 0x10000000>;  /* 1GB的CMA区域 */
        no-map;
    };
};

在驱动代码中,可以通过 dma_alloc_coherent 函数从 CMA 区域分配内存,确保设备能够使用连续的物理内存进行数据传输,提高传输效率。

对于中断等原子上下文场景,由于不能睡眠,需要快速分配内存,此时 DMA 原子池(pool.c)就发挥了作用。原子池预先分配了一些小块内存,当有原子上下文的内存分配请求时,可以快速从原子池中获取内存,避免了在中断处理中进行复杂的内存分配操作导致的延迟。在使用原子池时,需要注意其内存大小和分配策略,确保能够满足原子上下文的需求。

而在普通场景中,流式 DMA 的可缓存内存是常用的选择。这种内存分配方式灵活,能够利用 CPU 缓存提高数据访问速度。在进行流式 DMA 映射时,需要注意同步函数的调用,确保缓存一致性。

4.3 IOMMU 与 SWIOTLB 选型:性能与兼容性的权衡

在 DMA 应用中,IOMMU(I/O Memory Management Unit)和 SWIOTLB(Software I/O Translation Lookaside Buffer)的选型是一个需要谨慎考虑的问题,它涉及到性能与兼容性的权衡。

IOMMU 是一种硬件设备,它通过地址映射表将设备使用的 I/O 虚拟地址(IOVA)转换为物理地址,实现设备对内存的访问。这种硬件地址映射方式具有高性能的特点,能够实现大地址空间的访问,并且支持设备虚拟化和安全隔离。在服务器和高端嵌入式系统中,由于对性能要求较高,并且硬件通常支持 IOMMU,因此优先启用 IOMMU 是提升 DMA 性能的关键。在使用 IOMMU 时,需要配置 IOMMU 域,将设备附加到相应的域中,并进行地址映射操作。

而 SWIOTLB 是一种软件后备方案,当硬件不支持 IOMMU 或者 IOMMU 无法处理某些情况时,SWIOTLB 就会发挥作用。它通过在低地址空间预分配一块内存池作为 bounce buffer,当设备需要访问高地址内存时,先将数据从高地址拷贝到 bounce buffer 中,然后设备访问 bounce buffer,传输完成后再将数据拷贝回高地址内存。这种方式虽然保证了兼容性,但不可避免地带来了额外的数据拷贝开销,导致性能下降。在老旧硬件场景中,由于硬件不支持 IOMMU,只能使用 SWIOTLB 来保障设备的正常工作,但需要注意其性能瓶颈,尽量减少大数据量的传输操作。

Logo

汇聚全球AI编程工具,助力开发者即刻编程。

更多推荐