Linux 内核 DMAEngine 深度实战:从 DMA 物理地址映射到异步分散/聚集传输的全链路工程艺术

引言

理解 Linux 内核的 DMA(Direct Memory Access)引擎子系统,是通往高性能存储、网络和异构计算开发的必经之路。当 NVMe SSD 达到 7GB/s 的吞吐、25GbE/100GbE 网卡线速转发、GPU 与 CPU 通过 CXL 共享内存时——在这些每个场景中,真正让数据"跑"起来的底层引擎就是 DMA。

然而 DMA 的技术栈横跨硬件(IOMMU/SMMU、DMA 控制器、总线协议)、内核抽象层(dma-mapping、coherent pool、DMAEngine 框架)、以及驱动实现(pci_alloc_consistent 的现代演进、async_tx 异步传输 API),使得它成为 Linux 内核中最难系统掌握的模块之一。

本文将从硬件 DMA 传输的物理机制出发,逐层拆解内核 DMA 子系统的完整工程架构,最终给出在 DPDK、SPDK、io_uring 和 RDMA 场景下的生产级 DMA 优化实战。

第一章:DMA 传输的硬件本质与总线架构

1.1 为什么需要 DMA

在没有 DMA 的系统中,外设与内存的数据交换完全由 CPU 执行拷贝指令(Programmed I/O, PIO)。以一个 1Gbps 网卡为例:

  • 线速吞吐下每秒 ~148万 个数据包(64B 小包)
  • 每个数据包需要 CPU 从网卡 FIFO 读取到内存,再由协议栈处理
  • 纯 PIO 模式会消耗 100% CPU 仍然无法线速处理

DMA 的核心思想:让外设控制器在获得总线主控权(Bus Master)后,直接读写系统内存,仅在传输完成时通过中断通知 CPU。这使得 CPU 解放出来执行计算任务,形成"数据传输"与"数据计算"的流水线。

1.2 PCIe 总线中的 DMA 通信

现代外设通过 PCIe 总线与 CPU 通信。PCIe 的 DMA 本质是总线主控传输(Bus Master Transaction):

┌─────────────┐      Read/Write TLPs      ┌──────────────┐
│  NVMe SSD   │ ◄────────────────────────► │  Root Complex │
│  (Endpoint) │     Memory Transaction      │  (CPU/DRAM)   │
└─────────────┘                             └──────────────┘

DMA Write (设备→内存):
  SSD控制器 → Memory Write TLP(带64位物理地址) → Root Complex → DRAM

DMA Read (内存→设备):
  SSD控制器 → Memory Read TLP → Root Complex返回Completion TLP(带数据)

关键要点:PCIe 设备发出的 DMA 请求携带的是物理地址(不是虚拟地址),这意味着设备必须知道数据在内存中的物理位置。这个物理地址由内核驱动在发起传输前通过 DMA Mapping API 分配并告知设备。

1.3 地址翻译的困境:IOMMU 的角色

直接向设备暴露物理地址存在安全隐患(恶意设备可读写任意内存)。IOMMU(Intel VT-d / AMD-Vi / ARM SMMU)通过页表将设备视角的"IO Virtual Address"(IOVA)翻译为物理地址:

设备 DMA: IOVA = 0x1000
    ↓ IOMMU 二级页表翻译
物理地址: 0x8000_1000

好处:
1. 设备只能访问 IOMMU 页表中映射的内存区域(沙盒化)
2. 支持 32位老设备访问 64 位高内存(bounce buffer 替代方案)
3. 支持 SVA(Shared Virtual Addressing)——设备直接使用进程虚拟地址

第二章:内核 DMA Mapping API —— 缓冲区生命周期的物理层

2.1 dma_alloc_coherent:一致性 DMA 映射

某些 DMA 控制器的场景需要 CPU 和设备同时看到最新数据(无缓存一致性保证的总线架构)。此时必须分配绕过 CPU 缓存的"一致性内存"(Coherent Memory):

// 在内核驱动中分配一致性 DMA 缓冲区
void *cpu_addr;
dma_addr_t dma_handle;

cpu_addr = dma_alloc_coherent(dev, size, &dma_handle, GFP_KERNEL);

// cpu_addr: CPU 可用的虚拟地址(uncached 或 write-through 映射)
// dma_handle: 设备应使用的物理地址(传给 DMA 控制器的寄存器)
// 保证 CPU 写入 cpu_addr 后设备立即可见对应数据(反之亦然)

底层实现路径:

dma_alloc_coherent()
  → dma_alloc_attrs()
    → dma_alloc_from_coherent()  // 优先从 coherent pool 分配(设备预留内存)
      → 若无可用 coherent pool:
        → dma_alloc_from_global()  // 从全局 CMA(Contiguous Memory Allocator)分配
          → 若无足够连续内存:
            → dma_alloc_from_contiguous()  // 通过 page compaction + CMA 迁移获取

CMA(Contiguous Memory Allocator)是内核中解决"分配大块连续物理内存"的经典方案:

  • 在系统启动时预留一块内存区域(如 dma=256M 或 device tree memory-region)
  • 预留期间,CMA 页面作为可移动页分配给普通用户使用
  • 当 DMA 需求到来时,通过 __alloc_pages_direct_compact() 迁移走已用页面,腾出连续区域
  • 这使得"预留但不浪费"成为可能

2.2 dma_map_single / dma_map_page:流式 DMA 映射

大多数高性能场景使用"流式 DMA"(Streaming DMA)——数据缓冲区在传输前后被临时映射,映射完成后失效:

// Host → Device(如网卡发送数据包)
dma_addr_t dma_handle = dma_map_single(dev, skb->data, skb->len, DMA_TO_DEVICE);

// 设备开始 DMA 传输...
dma_unmap_single(dev, dma_handle, skb->len, DMA_TO_DEVICE);

三个方向参数的意义:

  • DMA_TO_DEVICE:CPU→设备,需 flush CPU cache 让设备看到最新数据(或标记该区域为 uncached)
  • DMA_FROM_DEVICE:设备→CPU,需 invalidate CPU cache 让 CPU 看到设备写入的真实数据
  • DMA_BIDIRECTIONAL:双向传输,需同时 flush + invalidate

在无缓存一致性协议(Cache Coherency)的总线架构(如某些 ARM SoC)上,dma_map_single() 可能涉及真实的 cache flush 操作(调用 __dma_map_area() 执行 DC CVAC 等 ARM64 cache 指令)。而在 x86 架构中(MESI 协议保证 cache coherency),这个操作通常只需返回物理地址与虚拟地址的直接映射。

2.3 Scatter-Gather 映射:dma_map_sg

现代 DMA 控制器不要求数据在物理内存中连续——它接受一个"散布/聚集列表"(Scatter-Gather List, SGL):

struct scatterlist sg[3];
sg_init_table(sg, 3);

sg_set_buf(&sg[0], header_data, header_len);    // 第一段:header
sg_set_buf(&sg[1], payload_data, payload_len);  // 第二段:payload  
sg_set_buf(&sg[2], trailer_data, trailer_len);  // 第三段:trailer

int nents = dma_map_sg(dev, sg, 3, DMA_TO_DEVICE);

// nents: 经过合并后的实际段数(相邻物理页可能被合并)
// sg[].dma_address: 每段的 DMA 地址
// sg[].length: 每段的长度

SG 传输的硬件实现:DMA 控制器内部有一个"描述符链表",每个描述符指向一个物理内存段。控制器按链表顺序、自动完成多段传输,期间无需 CPU 干预。这个特性让内核可以将 sk_buff 的多段数据(线性区 + frag 区 + frag_list)零额外拷贝地交给网卡。

第三章:DMAEngine 框架 —— 异步传输的通用抽象层

3.1 为什么需要 DMAEngine 框架

在没有 DMAEngine 之前,每个 DMA 控制器都需要自己实现:

  • 通道(Channel)管理——请求分配、释放、优先级仲裁
  • 描述符(Descriptor)分配——DMA 操作命令的内存结构
  • 异步完成通知——中断回调机制
  • 通用 memcpy/cmp/pq 操作——DMA 最常用的用途

DMAEngine 框架(drivers/dma/)抽象了这些通用能力,让 DMA 控制器驱动只需注册硬件特定操作。

3.2 Drive 架构与核心数据结构

struct dma_device {
    struct list_head        global_node;     // 全局 DMA 设备链表
    struct dma_chan         *channels;       // 通道数组
    struct list_head        channels_list;   // 通道链表(供分配遍历)
    struct dma_filter       filter;          // 通道过滤函数(匹配设备树属性)
    dma_cap_mask_t          cap_mask;        // 能力位图(MEMCPY/SLAVE/XOR...)
    u32                     src_addr_widths; // 支持的源地址宽度
    u32                     dst_addr_widths; // 支持的目的地址宽度
    enum dma_residue_granularity residue_granularity; // 余量粒度
};

struct dma_chan {
    struct dma_device       *device;         // 所属设备
    dma_cookie_t            cookie;          // 上次提交的 cookie
    dma_cookie_t            completed_cookie;// 上次完成的 cookie
    struct dma_chan_percpu  *chan_percpu;    // per-CPU 私有数据
    struct list_head        device_node;     // 挂到 device 链表上
    struct dma_slave_config slave;           // slave 模式配置
};

struct dma_async_tx_descriptor {
    dma_cookie_t            cookie;          // 提交时返回的 cookie
    enum dma_ctrl_flags     flags;           // 中断/回调标志
    dma_addr_t              phys;            // 描述符物理地址
    struct virt_dma_desc    vd;              // 虚拟描述符(virt-dma 层)
    dma_tx_callback         callback;        // 完成回调
    void                    *callback_param; // 回调参数
};

3.3 通道分配:从设备树到 dma_chan

设备树中的 DMA 引用通过 phandle 将外设与 DMA 控制器绑定:

// 设备树示例
dmac: dma-controller@12340000 {
    compatible = "vendor,pl330";
    #dma-cells = 

; // 1个通道号参数 reg = <0x12340000 0x10000>; }; mmc0: mmc@12350000 { dmas = <&dmac 0>; // 使用 dmac 的通道 0 dmas-names = "rx-tx"; };

驱动通过以下路径请求通道:

struct dma_chan *chan = dma_request_chan(dev, "rx-tx");
// → of_dma_request_chan()  // 设备树路径
// → of_dma_xlate()         // 调用驱动注册的 of_dma_xlate 解析 cells
// → dma_chan_is_free()     // 检查通道是否已被占用
// → chan->private = dev    // 记录通道归属

当设备不支持设备树时,也可通过 dma_request_slave_channel() 或 ID 匹配方式获取通道。

3.4 事务提交 API

DMAEngine 定义了四大类事务类型,对应不同的 DMA 传输语义:

(1) Slave SG 传输(外设↔内存)

struct dma_async_tx_descriptor *tx;
tx = dmaengine_prep_slave_sg(chan, sg, sg_len, DMA_MEM_TO_DEV, DMA_PREP_INTERRUPT);

// 设置完成回调
tx->callback = my_tx_complete;
tx->callback_param = my_dev_priv;

// 提交到硬件队列
dmaengine_submit(tx);

// 触发硬件启动(或通道轮询模式自动启动)
dma_async_issue_pending(chan);

(2) Cyclic DMA(循环传输——音频场景)

音频编解码器需要持续不断地从内存读取 PCM 数据,使用 Cyclic 模式:

tx = dmaengine_prep_dma_cyclic(chan, buf_addr, buf_len, period_len,
                                DMA_MEM_TO_DEV, DMA_PREP_INTERRUPT);

// 硬件在完成一个 period 后自动回到缓冲区起点重复传输
// Linux 可用于 I2S/SAI/SPDIF 等音频场景

(3) Interleave DMA(交错传输——视频/图像处理)

视频数据通常以"行间交错"(Interleave)方式存储,DMA 控制器需要知道 stride、frame dimension:

struct dma_interleaved_template xt = {
    .src_start = src_addr,
    .dst_start = dst_addr,
    .dir = DMA_MEM_TO_DEV,
    .numf = 480,        // 帧高度
    .frame_size = 2,    // 2个 chunk
    .sgl[0] = { .size = 1920, .icg = 0 },       // Y plane
    .sgl[1] = = { .size = 960,  .icg = 256 },    // UV plane 带间隙
};

tx = dmaengine_prep_interleaved_dma(chan, &xt, DMA_PREP_INTERRUPT);

(4) memcpy / xor / pq(内存到内存的 DMA 操作)

纯内存拷贝操作由 I/OAT(Intel I/O Acceleration Technology)或 SoC 内置 DMA 引擎完成:

struct dma_async_tx_descriptor *tx;

// 异步 memcpy
tx = dmaengine_prep_dma_memcpy(chan, dst_addr, src_addr, len, DMA_PREP_INTERRUPT | DMA_CTRL_ACK);

// 提交并等待完成
cookie = dmaengine_submit(tx);
dma_async_issue_pending(chan);
dma_sync_wait(chan, cookie);  // 同步等待完成

io_dmaengine 是现代高效场景下 memcpy 的替代方案——当拷贝数据量 > 阈值时(通常 4KB-128KB,取决于平台),DMA 引擎的吞吐远高于 CPU memcpy(因为释放了 CPU 流水线,且不污染 L1/L2 cache)。

第四章:Virt-DMA 层 —— 软件描述符队列的管理者

现代 DMA 控制器的硬件描述符队列通常很有限(如 16-256 个槽位),而软件经常需要排队大量待处理事务。Virt-DMA 层在软件层维护一个虚拟描述符队列,实现硬件队列的扩展:

struct virt_dma_desc {
    struct dma_async_tx_descriptor tx;      // 事务描述符
    struct list_head node;                  // 挂在 virt_dma_chan 链表
    struct list_head desc_submitted;        // 已提交到硬件链表
    struct list_head desc_issued;           // 硬件正在运行的链表
};

struct virt_dma_chan {
    struct dma_chan     chan;               // 底层硬件通道
    struct tasklet_struct task;             // 底半部处理
    struct list_head    desc_submitted;     // 等待提交的描述符
    struct list_head    desc_issued;        // 已提交给硬件的描述符
    struct virt_dma_desc *cyclic;           // 当前 cyclic 描述符
};

Virt-DMA 的工作流程:

dmaengine_prep_*()     → 创建 virt_dma_desc 挂在 desc_submitted 链表
dmaengine_submit()     → cookie 赋值,移动到 desc_submitted 尾
dma_async_issue_pending() → virt_dma_issue_pending()
    → 检查硬件是否有空槽,若有:
        → vc->desc_submitted → vc->desc_issued (移动到已运行链表)
        → 填写硬件描述符 → 推入硬件队列

硬件中断 (传输完成):
    → virt_dma_tasklet()
    → 遍历 vc->desc_issued 找已完成项
    → 调用完成回调(tx_callback)
    → 尝试从 desc_submitted 推新硬件发送

第五章:DMAEngine 与 io_uring / SPDK 的协同

5.1 用户态 DMA:IORING_REGISTER_BUFFERS

io_uring 通过 IORING_REGISTER_BUFFERS(Fixed Buffers)机制实现了用户态零拷贝 DMA:

// 用户态注册缓冲区
struct iovec iov = { .buf = ptr, .len = 4096 };
io_uring_register_buffers(&ring, &iov, 1);

// 内核内部:
// dma_map_sg() — 一次性将用户缓冲区映射到 IOMMU 页表
// 后续所有 I/O 操作复用同一 IOMMU 映射(避免每次 map/unmap 开销)

SPDK(Storage Performance Development Kit)进一步在用户态通过 vfio-pci 或 uio_pci_generic 获得设备的 PCI BAR 空间映射,直接操作 NVMe SQ/CQ 环形队列,全程无需系统调用:

SPDK DMA 流程:
1. spdk_dma_malloc() — 分配物理连续大页(2MB/1GB hugepage)
2. spdk_vtophys() — 获取虚拟地址对应的物理地址(读 /proc/self/pagemap)
3. 将物理地址填入 NVMe Submission Queue Entry (SQE)
4. 写 Doorbell 寄存器提交
5. 轮询 Completion Queue (busy-polling,零中断)

5.2 vfio-pci:用户态 DMA 的安全方案

标准的 uio_pci_generic 允许用户态访问设备但无法隔离 DMA。vfio-pci 通过 IOMMU 对用户态 DMA 进行沙盒化:

vfio 组(group) → 设备(device) → 容器(container)
  ↓
VFIO_IOMMU_MAP_DMA ioctl:
  1. 验证调用进程是否拥有该 IO 页面的访问权限
  2. 在 IOMMU 页表中建立 IOVA → 物理页 映射
  3. 进程仅能映射授予的页面(不能越界)
  4. DMA 设备只能访问进程映射的 IOVA 区域

第六章:DMA 性能优化 —— 生产级调优矩阵

6.1 缓存行对齐 vs 跨页传输

DMA 控制器的物理地址可直接使用非缓存映射(Write-Combined 或 Uncached),但业界常见的"4K对齐"规则并非来自 DMA 控制器限制,而是来自页面边界约束:大多数 DMA 控制器不能跨页传输——即一个 DMA 描述符只能覆盖同一物理页内的地址范围。解决方案是使用大页(HugePage):

// 2MB hugepage: 一个描述符可传输 2MB 连续数据(减少中断频率)
// 1GB hugepage: 单个描述符覆盖更大范围

echo 20 > /sys/kernel/mm/hugepages/hugepages-2048kB/nr_hugepages

6.2 DMA 合并(DMA Coalescing)

网卡驱动通过合并相邻 DMA 描述符来降低总线事务数:

// 苹果 M1/M2 的统一内存架构 (UMA) 允许设备在 LPDDR 上直接 DMA
// 此时 DMA 延迟极低,合并带来的收益主要是减少描述符中断

// Intel IOMMU 的 DMAR 合并:
// 多个分散的小 DMA 映射可以合并为一个大的 contiguous mapping
// 减少 IOMMU TLB Miss 次数

6.3 实例:NVMe 中的 DMA 描述符优化

NVMe 协议定义了 PRP(Physical Region Page)和 SGL(Scatter-Gather List)两种 DMA 描述符格式:

// PRP List (默认方式)
// PRP1: 第一个数据页的物理地址
// PRP2: 第二个数据页的物理地址 OR PRP List 指针

// 一个 4K 页面 = 1个 PRP entry (8字节) → 最多 512 个 entry → 2MB 数据
// 传输超过 2MB 需要使用 PRP List 块(类似多级页表)

// SGL (现代 NVMe 推荐)
// 每个 SGL 描述符: 16 字节 (地址8 + 长度4 + 标志4)
// 无长度限制,可直传分散内存(与内核 sg_table 一一对应)

SPDK 选择 SGL 模式的原因:可以直接将用户态 sg_table 翻译成 NVMe SGL 描述符,无需重新组织数据结构。

6.4 多队列 DMA 与 NUMA 绑定

现代 NVMe 设备支持最多 64K 个 I/O 队列对(SQ/CQ 各 Completion Queue)。每个队列对应独立的 DMA 引擎和中断向量。最优策略:

// 将 NVMe 中断绑定到与 SSD 设备同 NUMA 节点的 CPU
echo "mask" > /proc/irq/IRQ_NUMBER/smp_affinity

// SPDK 使用 reactor-per-core 模型:
// CPU 0 → 轮询 SQ/CQ of queue pair 0
// CPU 1 → 轮询 SQ/CQ of queue pair 1
// 每个 reactor 独占 DMA 通道(无锁)

第七章:DMAEngine 驱动开发实战

7.1 注册一个 DMA Controller

以下是一个简化的 DMA 控制器驱动注册流程,展示 DMAEngine 框架的核心调用链:

static int my_dma_probe(struct platform_device *pdev)
{
    struct dma_device *ddev;
    
    // 1. 分配 dma_device
    ddev = devm_kzalloc(&pdev->dev, sizeof(*ddev), GFP_KERNEL);
    
    // 2. 初始化能力掩码
    dma_cap_set(DMA_MEMCPY, ddev->cap_mask);
    dma_cap_set(DMA_SLAVE, ddev->cap_mask);
    dma_cap_set(DMA_CYCLIC, ddev->cap_mask);
    
    // 3. 设置操作函数表
    ddev->device_prep_dma_memcpy = my_dma_prep_memcpy;
    ddev->device_prep_slave_sg   = my_dma_prep_slave_sg;
    ddev->device_prep_dma_cyclic = my_dma_prep_cyclic;
    ddev->device_config          = my_dma_config;
    ddev->device_pause           = my_dma_pause;
    ddev->device_resume          = my_dma_resume;
    ddev->device_terminate_all   = my_dma_terminate_all;
    ddev->device_tx_status       = my_dma_tx_status;
    ddev->device_issue_pending   = my_dma_issue_pending;
    
    // 4. 初始化通道链表
    INIT_LIST_HEAD(&ddev->channels);
    
    // 5. 注册设备
    dmaenginem_async_device_register(ddev);  // 异步注册
    // 或 dma_async_device_register(ddev);    // 同步注册
    
    return 0;
}

7.2 描述符准备函数骨架

static struct dma_async_tx_descriptor *
my_dma_prep_memcpy(struct dma_chan *chan, dma_addr_t dst, dma_addr_t src,
                   size_t len, unsigned long flags)
{
    struct my_dma_desc *desc;
    struct my_dma_chan *mchan = to_my_chan(chan);
    
    // 1. 分配描述符
    desc = kzalloc(sizeof(*desc), GFP_NOWAIT);
    
    // 2. 填写硬件寄存器值
    desc->src_addr = src;
    desc->dst_addr = dst;
    desc->xfer_len = len;
    desc->ctrl_reg = MY_DMA_CTRL_IRQ_EN | MY_DMA_CTRL_START;
    
    // 3. 设置 dma_async_tx_descriptor 成员
    dma_async_tx_descriptor_init(&desc->tx, &mchan->chan);
    desc->tx.tx_submit = my_dma_tx_submit;
    desc->tx.phys = virt_to_physical(desc->hw_desc);
    
    return &desc->tx;
}

第八章:DMA 调试与性能分析

8.1 ftrace 追踪 DMA 事件

内核为 DMAEngine 子系统内置了 Tracepoints:

# 追踪 dma 传输提交
echo 1 > /sys/kernel/debug/tracing/events/dma/enable

# 查看追踪日志
cat /sys/kernel/debug/tracing/trace_pipe

# dma_fence_signaled: dma_fence 完成事件
# dma_run_dependencies: 依赖链执行

8.2 eBPF 监控 DMA 中断热点

// 统计各 DMA 通道的中断频率和延迟
kprobe:dmaengine_synchronize {
    @start[tid] = nsecs;
}

kretprobe:dmaengine_synchronize /@start[tid]/ {
    @us = hist((nsecs - @start[tid]) / 1000);
}

8.3 使用 perf 分析 DMA 性能瓶颈

// 查看 PCIe 总线流量(需要 uncore PMU)
perf stat -e amd_iommu/mem_pass_through/ -a sleep 1  // AMD平台
perf stat -e uncore_imc/free_running/ -a sleep 1       // Intel IMC

// 追踪 dma_map/dma_unmap 热点
perf probe --add 'dma_map_single'
perf record -e probe:dma_map_single -a -g -- sleep 1
perf report --sort=dso,symbol

第九章:DMA 在不同子系统的应用实例

9.1 网络驱动中的 DMA:NAPI + GRO + XDP

Linux 内核的 NAPI 收包流程大量依赖 DMA:

// 预分配 RX 环缓冲区
for (i = 0; i < RX_RING_SIZE; i++) {
    skb = netdev_alloc_skb_align(netdev, buffer_len);
    buffer->dma = dma_map_single(dev, skb->data, buffer_len, DMA_FROM_DEVICE);
    rx_ring[i].skb = skb;
    rx_ring[i].dma = buffer->dma;
}

// 设备 DMA 完成后触发中断/ polling
void napi_poll(...) {
    for (每个完成的 RX 描述符) {
        skb = rx_ring[i].skb;
        dma_unmap_single(dev, rx_ring[i].dma, len, DMA_FROM_DEVICE);
        skb_put(skb, len);
        netif_receive_skb(skb);  // 提交到协议栈
        
        // 分配新的缓冲区替换(DMA 双缓冲)
        rx_ring[i].skb = netdev_alloc_skb_align(...);
        rx_ring[i].dma = dma_map_single(...);
    }
}

9.2 SPI/I2C/DMA控制器

嵌入式 SoC 中 SPI 外设通常共享 DMA 控制器:

// SPI TX DMA:将数据从内存 DMA 到 SPI TX FIFO
tx_desc->sg[0].dma_address = dma_map_single(spi->dev, tx_buf, len, DMA_MEM_TO_DEV);
// SPI RX DMA:从 SPI RX FIFO DMA 到内存
rx_desc->sg[0].dma_address = dma_map_single(spi->dev, rx_buf, len, DMA_DEV_TO_MEM);

// SPI 外设硬件自动生成 DMA 请求信号(DRQ)
// DMA 控制器每收到 DRQ,执行一个 burst(如 4 beat * 32bit = 16字节)
// 全场无需 CPU 干预

9.3 GPU 与 DMA-BUF:跨设备零拷贝

现代 GPU 渲染输出通过 DMA-BUF 共享给显示控制器(DRM/KMS):

// GPU 渲染输出的 framebuffer 通过 dma_buf 分配
struct dma_buf *buf = drm_fb_get_dma_buf(fb);

// KMS(显示控制器)通过 dma_buf_map_attachment() 获得 DMA 映射
struct dma_buf_map map;
dma_buf_map_attachment(attach, DMA_TO_DEVICE, &map);

// 显示控制器持续扫描 map->dmabuf->pages 中的物理地址
// 实现 GPU 渲染→显示的全链路零拷贝

附录 A:Linux 内核 DMA 参数速查表

参数路径说明
swiotlb/sys/kernel/debug/swiotlb/io_tlb_used查看 SWIOTLB bounce buffer 使用量
coherent_pool/proc/meminfo (CmaTotal/CmaFree)CMA 内存池统计
iommu/sys/class/iommu/IOMMU 域列表
dma_alloc/sys/kernel/debug/dma_buf/bufinfoDMA-BUF 导出者统计
hugepage/sys/kernel/mm/hugepages/大页配置
dmaengine/sys/class/dma/已注册 DMA 通道

附录 B:编译选项依赖关系

配置项说明关键依赖
CONFIG_DMADEVICESDMAEngine 框架核心—
CONFIG_DMA_CMA连续内存分配器CONFIG_CMA
CONFIG_DMA_ENGINEDMA 引擎CONFIG_DMADEVICES
CONFIG_DMA_VIRT_CHANNELVirt-DMA 软件队列CONFIG_DMA_ENGINE
CONFIG_IOMMU_SUPPORTIOMMU/SMMUCONFIG_PCI / CONFIG_PLATFORM
CONFIG_SWIOTLBSoftware IO TLBCONFIG_IOMMU(DMA地址翻译)
CONFIG_DMA_SHARED_BUFFERDMA-BUF(跨设备共享)
CONFIG_DMABUF_HEAPS

总结

Linux 内核的 DMAEngine 子系统是一个横跨硬件协议、内存管理、设备模型和驱动框架的复杂工程体系。本文从三个层级展现其全貌:

  • 硬件层:PCIe 总线主控、IOMMU 地址翻译、Scatter-Gather DMA 控制器
  • 内核层:DMA Mapping API(map/unmap + CMA + coherent pool)、DMAEngine 框架(chan/prep/submit/issue)、Virt-DMA 层(软件描述符队列)
  • 用户态层:io_uring Fixed Buffers、SPDK VFIO 用户态驱动、DMA-BUF 跨设备共享

掌握这些层次的联系,无论是在驱动开发中避免 DMA 越界导致的系统崩溃,还是在存储/网络调优中实现零拷贝传输,都将具有直接的工程价值。

点赞(0) 打赏

评论列表 共有 0 条评论

暂无评论
立即
投稿

微信公众账号

微信扫一扫加关注

发表
评论
返回
顶部