Linux DMAEngine 与 IOMMU DMA 保护深度工程实践

当 NVMe SSD 以 7 GB/s 吞吐写入内存,当网卡在 100Gbps 线路上搬运数据,一个无形的引擎正在 CPU 之外默默工作——DMA(Direct Memory Access)。但 DMA 的强大也是一把双刃剑:绕过 CPU 直接操作内存的特性,使得一个恶意或故障的外设可以读写系统任何内存区域。这就是 IOMMU(Input-Output Memory Management Unit)存在的理由。本文将深入剖析 Linux 内核中 DMAEngine 控制器编程模型与 IOMMU DMA 保护机制,并给出生产级驱动开发实战。


一、DMA 基础:为什么需要绕过 CPU

在没有 DMA 的系统中(PIO 模式),外设与内存的数据搬运需 CPU 介入:

设备FIFO ──→ CPU寄存器 ──→ 内存  (每32位/64位需一次CPU指令)

假设一块 25Gbps 网卡要接收 1500B MTU 数据包,包到达率约 20M pps。若用 CPU 搬运,每个包需要约 375 次内存操作——CPU 光搬数据就耗尽了,根本来不及做协议处理。

DMA 控制器让外设直接与内存交互:

设备FIFO ──→ DMA引擎 ──→ 内存  (CPU 仅下发描述符,无需逐字节参与)

1.1 DMA 控制器类型

Linux 内核将 DMA 分为两类:

  • Master DMA(总线主控):设备自带 DMA 引擎,直接发起总线事务(如 NVMe、网卡内置 DMA)
  • Slave DMA(从属 DMA):系统级 DMA 控制器(如 Intel PXA DMA、TI EDMA、Xilinx DMA)为外设提供搬运服务

Linux DMAEngine 子系统主要解决 Slave DMA 场景。

1.2 DMA 传输基本参数

一次 DMA 传输需要确定的要素:

┌──────────────────────────────────────────┐
│  Source Address  (外设 FIFO 寄存器地址)    │
│  Destination Address (内存物理地址)        │
│  Transfer Width    (8/16/32/64 位)         │
│  Burst Size        (一次突发传输的节拍数)   │
│  Length            (传输总字节数)           │
│  Direction         (MEM_TO_DEV / DEV_TO_MEM│
│                     / MEM_TO_MEM)           │
└──────────────────────────────────────────┘

二、DMAEngine 子系统架构

DMAEngine 子系统自 2.6 时代引入,为 Slave DMA 控制器提供统一的驱动框架和用户 API。其核心层次结构如下:

┌─────────────────────────────────────────────────────┐
│  客户端驱动 (Client Driver)                          │
│  sound/soc, spi, uart, mmc, net...                  │
└──────────────┬──────────────────────────────────────┘
               │ dmaengine API
┌──────────────▼──────────────────────────────────────┐
│  DMAEngine Core  (drivers/dma/dmaengine.c)           │
│  ├─ dma_chan 管理 (channel 分配/引用计数)             │
│  ├─ capability bitmap (DMA_SLAVE/DMA_CYCLIC/...)    │
│  └─ async_tx 异步事务层                              │
└──────────────┬──────────────────────────────────────┘
               │ ops
┌──────────────▼──────────────────────────────────────┐
│  DMA Controller Driver                               │
│  dw_dmac / edma / pl330 / xilinx_dma / imx-sdma ... │
│  ├─ device_prep_memcpy()                            │
│  ├─ device_prep_slave_sg()                          │
│  ├─ device_prep_dma_cyclic()                        │
│  ├─ device_issue_pending()                          │
│  └─ device_pause() / device_terminate_all()         │
└──────────────┬──────────────────────────────────────┘
               │ reg
┌──────────────▼──────────────────────────────────────┐
│  DMA Hardware (AHB/AXI 总线矩阵)                      │
└─────────────────────────────────────────────────────┘

2.1 dma_chan:DMA 通道

每个 DMA 控制器包含若干硬件通道,内核以 struct dma_chan 表示一个通道实例:

struct dma_chan {
    struct dma_device *device;       // 所属控制器
    dma_cookie_t cookie;             // 上次提交的 cookie
    dma_cookie_t completed_cookie;   // 上次完成的 cookie

    struct list_head device_node;    // 挂到控制器的 chan 链表
    struct dma_chan_percpu *local;   // 每个 CPU 的使用统计

    int slave_config_count; 
    struct dma_slave_config config;  // 从属配置(方向/FIFO/突发)
    ...
};

每个通道同一时刻只能为一个客户端服务,通过 dma_request_chan() 分配。

2.2 capabilities bitmap

控制器驱动在注册时声明其能力,客户端通过掩码匹配筛选:

/* include/linux/dmaengine.h */
#define DMA_MEMCPY      BIT(0)    // 内存到内存拷贝
#define DMA_SLAVE       BIT(1)    // 内存↔外设 (scatter-gather)
#define DMA_CYCLIC      BIT(2)    // 循环传输(音频)
#define DMA_INTERLEAVE  BIT(3)    // 交织传输(4K NAND)
#define DMA_PQ          BIT(4)    // P+Q RAID 运算
#define DMA_XOR         BIT(5)    // XOR RAID 运算
#define DMA_SLAVE_BUSWIDTH_8_BYTES  BIT(...)  // 8字节总线宽度

2.3 async_tx 层

对客户端透明地提供异步事务语义和依赖链:

struct dma_async_tx_descriptor {
    dma_cookie_t cookie;
    enum dma_ctrl_flags flags;       // ACK/PREP_PINTR/...

    struct dma_chan *chan;
    dma_addr_t tx_submit();          // 提交到控制器硬件队列

    dma_async_tx_callback callback;  // 完成回调
    void *callback_param;

    struct dmaengine_unmap_data *unmap;  // IOMMU 反向映射
};

三、DMAEngine API 完整编程模型

3.1 通道请求与释放

static int my_dma_probe(struct platform_device *pdev)
{
    struct dma_chan *chan;

    /* 通过设备树 "dmas" 属性匹配 */
    chan = dma_request_chan(dev, "rx");
    if (IS_ERR(chan))
        return PTR_ERR(chan);

    /* 或使用 filter 函数手动筛选 */
    struct dma_chan *chan2;
    dma_chan_filter filter = { .fn = my_filter_fn };
    chan2 = dma_request_channel(filter_mask, filter, filter_param);

    return 0;
}

static void my_dma_release(struct device *dev)
{
    dma_release_channel(chan);
}

3.2 Slave Scatter-Gather 传输

这是最常见的用途——在外设 FIFO 与分散的内存页之间传输数据:

static int do_slave_sg_transfer(struct dma_chan *chan, 
                                 struct scatterlist *sgl, int sg_len,
                                 enum dma_transfer_direction dir)
{
    struct dma_async_tx_descriptor *tx;
    dma_cookie_t cookie;
    int ret;

    /* 准备 SG 描述符 */
    tx = dmaengine_prep_slave_sg(chan, sgl, sg_len, dir,
        DMA_PREP_INTERRUPT | DMA_CTRL_ACK);
    if (!tx)
        return -EIO;

    /* 设置完成回调 */
    tx->callback = my_dma_callback;
    tx->callback_param = my_priv_data;

    /* 提交到控制器硬件队列 */
    cookie = dmaengine_submit(tx);
    if (dma_submit_error(cookie))
        return -EIO;

    /* 触发 DMA 引擎开始工作 */
    dma_async_issue_pending(chan);

    /* 等待完成 - 通常在中断上下文或 workqueue 中处理 */
    wait_for_completion(&my_dma_done);

    return 0;
}

3.3 Cyclic 传输(音频专属)

音频需要持续地将环形缓冲送入编解码器,Cyclic DMA 在一个环形描述符链上循环传输:

static int setup_audio_cyclic(struct dma_chan *chan) 
{
    struct dma_async_tx_descriptor *tx;

    tx = dmaengine_prep_dma_cyclic(chan,
        audio_buf_addr,      // DMA 目标地址
        buffer_bytes,        // 环形缓冲总大小
        period_bytes,        // 每个周期的传输大小
        DMA_MEM_TO_DEV,      // 写入音频 DAC
        DMA_PREP_INTERRUPT   // 每个 period 触发中断
    );

    tx->callback = audio_period_elapsed;
    tx->callback_param = substream;
    dmaengine_submit(tx);
    dma_async_issue_pending(chan);

    return 0;
}

3.4 内存拷贝(DMA_MEMCPY)

平台级 DMA 引擎可替代 CPU 做内存拷贝:

static void do_memcpy_dma(struct dma_chan *chan, 
                           dma_addr_t dst, dma_addr_t src, size_t len)
{
    struct dma_async_tx_descriptor *tx;

    tx = dmaengine_prep_dma_memcpy(chan, dst, src, len, 
                                    DMA_PREP_INTERRUPT | DMA_CTRL_ACK);
    tx->callback = NULL;  // 同步等待模式
    tx->callback_param = NULL;

    dmaengine_submit(tx);
    dma_async_issue_pending(chan);

    /* 轮询等待完成 */
    dma_async_tx_chan_status(chan, cookie, &state);
}

四、IOMMU:DMA 的保护伞

4.1 IOMMU 的必要性

如前所述,给外设一个物理地址后,DMA 可以直接操作任意位置。这意味着:

  1. 安全性:恶意网卡可以读取 SSH 密钥所在内存页
  2. 稳定性:有 bug 的 DMA 引擎可能写入内核代码段
  3. 灵活性:32 位设备无法访问高于 4GB 的物理内存,需 IOMMU 做地址转换(类似 CPU MMU 的角色)

IOMMU(ARM 叫 SMMU,Intel 叫 VT-d,AMD 叫 AMD-Vi)对设备发出的 DMA 请求做地址转换和权限检查:

设备 DMA 请求 (IOVA 0x1000)
    │
    ▼
┌────────────┐     ┌────────────┐
│  IOMMU     │────▶│  IOMMU 页表│
│  硬件单元   │     │  (多级)    │
└────────────┘     └────────────┘
    │ 转换
    ▼
物理地址 0xF0003000 (仅在授权映射范围内)

4.2 Linux IOMMU 子系统

drivers/iommu/
├── iommu.c              # 核心框架
├── io-pgtable.c         # 页面格式解析
├── dma-iommu.c          # DMA API ↔ IOMMU 桥接
├── iommu-dma.c          # DMA domain 管理
├── intel/
│   └── iommu.c          # VT-d 驱动
├── amd/
│   └── iommu.c          # AMD-Vi 驱动
└── arm/
    └── arm-smmu-v3.c    # SMMUv3 驱动 (ARM64)

核心概念:

  • struct iommu_device:一个 IOMMU 硬件实例
  • struct iommu_domain:一组地址转换表,一个 domain 对应一个地址空间
  • struct iommu_group:共享同一地址空间的一组设备

4.3 IOMMU 工作原理

假设一个使用 scatter-gather DMA 的网卡驱动:

/* 驱动收到一个网络包,需要写入内存 */
struct sk_buff *skb;
struct scatterlist *sg;
dma_addr_t dma_addr;

/* 映射 scatterlist 到设备可见的 I/O 虚拟地址 */
sg = skb_shinfo(skb)->sgl;
dma_addr = dma_map_sg(dev, sg, sg_nents, DMA_FROM_DEVICE);

/* ↓ dma_map_sg 内部:
 * if (IOMMU enabled) {
 *     1. 分配 IOVA (I/O Virtual Address)
 *     2. 建立 IOMMU 页表映射 IOVA → page_phys
 *     3. 返回 IOVA 作为设备的 DMA 地址
 * } else {
 *     直接返回 page_phys (no-IOMMU, 无保护)
 * }
 */

/* 告知设备:请把收到的包 DMA 写入 dma_addr */
nic_rx_desc_addr_write(dev, dma_addr);

/* 设备完成后,驱动解除映射 */
dma_unmap_sg(dev, sg, sg_nents, DMA_FROM_DEVICE);

/* ↓ dma_unmap_sg 内部(有 IOMMU):
 *  1. 根据 IOVA 查找并释放 IOMMU 页表项
 *  2. 处理 ATS 设备:Invalidate IOMMU TLB
 */

五、DMA Mapping API 详解

5.1 流式 DMA 映射(Streaming DMA Mapping)

用于一次性的、有时间生命周期的 DMA:

/* 单页映射 */
dma_addr_t dma_map_page(struct device *dev, struct page *page,
                        size_t offset, size_t size,
                        enum dma_data_direction dir);

/* Scatter-Gather 映射 */
int dma_map_sg(struct device *dev, struct scatterlist *sg,
               int nents, enum dma_data_direction dir);

void dma_unmap_page(struct device *dev, dma_addr_t dma_handle,
                    size_t size, enum dma_data_direction dir);
void dma_unmap_sg(struct device *dev, struct scatterlist *sg,
                  int nents, enum dma_data_direction dir);

dir 参数至关重要:

enum dma_data_direction {
    DMA_BIDIRECTIONAL = 0,  // 双向(cache 同步开销最大)
    DMA_TO_DEVICE = 1,      // 内存 → 设备(CPU 写入后交给设备读)
    DMA_FROM_DEVICE = 2,    // 设备 → 内存(设备写入后交给 CPU 读)
    DMA_NONE = 3            // 仅用于 debug/PCI quirk
};

5.2 一致性 DMA 映射(Coherent DMA Mapping)

用于需要 CPU 和设备同时访问的环形缓冲(如网络环描述符、NVMe SQ/CQ):

/* 分配一致性 DMA 内存 */
void *dma_alloc_coherent(struct device *dev, size_t size,
                         dma_addr_t *dma_handle, gfp_t flag);
void dma_free_coherent(struct device *dev, size_t size,
                       void *vaddr, dma_addr_t dma_handle);

一致性内存的特殊性:

  • 硬件通过 cache snooping 保证 CPU 和设备看到相同数据
  • 在 x86 上通常通过非缓存(UC)或写通(WC)实现
  • 在 ARM 上通常由 IOMMU 维护,映射到 MAIR_ATTR_nGnRE 属性
  • 禁用 CPU cache 会导致性能下降,只为频繁访问的控制结构使用

5.3 dma_pool:减小映射开销

对频繁分配/释放的小块 DMA 一致性内存,使用 dma_pool 可显著减少 IOMMU 映射操作的开销:

struct dma_pool *pool;
void *va;
dma_addr_t pa;

/* 创建 pool(一次性支付 IOMMU 映射开销) */
pool = dma_pool_create("rx_desc_pool", dev, 
                        sizeof(struct rx_desc), 64, 0);
/* 从池中分配(复用已有映射) */
va = dma_pool_alloc(pool, GFP_KERNEL, &pa);
/* 释放回池(不解除 IOMMU 映射) */
dma_pool_free(pool, va, pa);

5.4 SMMU Bypass 与 passthrough

某些对性能要求极高的场景(如 RDMA/NVMe),IOMMU 的地址转换延迟和 TLB 维护成本不可接受。VT-d 提供一种模式:

# 内核参数:全局禁用 IOMMU
# 或加入内核参数:
intel_iommu=off
amd_iommu=off

# 仅对特定设备禁用(VT-d):
# 0000:04:00.0 设备的 IOMMU 组

注意:禁用 IOMMU 后,DMA API 将返回物理地址,设备可直接访问所有内存。


六、DMAEngine + IOMMU 协同工作时的地址问题

这是驱动开发中最容易踩坑的地方:DMA 控制器看到的是物理地址还是 IOVA?

6.1 地址域划分

                    ┌──────────────────────────────────┐
                    │        IOMMU Domain (IOVA 空间)    │
                    │   IOVA: 0x0000_0000 - 0xFFFF_FFFF │
                    └──────────────┬───────────────────┘
                                   │ IOMMU 地址转换
┌────────────┐                     │            ┌────────────┐
│  CPU       │                     │            │  物理内存    │
│  MMU       │─── 物理地址 ─────────┘            │            │
└────────────┘                                  └────────────┘

当配置 Slave DMA 的 dma_slave_config.src_addr 时:

  • 外设 FIFO 地址:对于固定功能外设(如 SPI、I2S),FIFO 寄存器地址通常是物理地址(不经过 CPU MMU,但可能经过 IOMMU)
  • 内存地址:必须是设备可访问的地址(即 IOVA)

正确的做法:

struct dma_slave_config cfg = {0};
struct dma_chan *chan;

cfg.direction = DMA_MEM_TO_DEV;
cfg.dst_addr_width = DMA_SLAVE_BUSWIDTH_4_BYTES;
cfg.dst_maxburst = 8;

/* src_addr 和 dst_addr 是什么取决于传输方向 */
if (direction == DMA_MEM_TO_DEV) {
    /* 写入外设:dst_addr = 外设寄存器物理地址 */
    cfg.dst_addr = fifo_phys_addr;  // 通常来自设备树 reg
    /* src_addr(内存)是 DMA 引擎内部使用,由客户端驱动通过 map */
}

dmaengine_slave_config(chan, &cfg);

6.2 SWIOTLB:无 IOMMU 时的 bounce buffer

当设备无法访问高地址内存(如 32 位设备在 >4GB 物理内存的系统上),Linux 使用 SWIOTLB(Software IO TLB/IOMMU)模拟 IOMMU:

# 全局预留 SWIOTLB 缓冲(默认 64MB)
swiotlb=524288     # 512MB,适用于大流量网络设备

SWIOTLB 工作方式:

分配高内存 → 需要 DMA → dma_map_sg 检查设备寻址能力
    ├── 不能访问 → 分配 SWIOTLB 低内存缓冲 → CPU 拷贝高→低 → DMA 写入低缓冲 → DMA完成 → 再拷贝回高内存
    └── 可以访问 → 直接映射(零拷贝)

SWIOTLB 的透明性对驱动代码无感,但性能损失可能显著——如果每次 DMA 都 bounce,吞吐可能下降 50%+。在高 I/O 场景下确保设备使用 64 位 DMA 掩码或使用真实 IOMMU。


七、实战:编写 DMAEngine 客户端驱动

下面给出一个完整的 DMAEngine 客户端驱动框架,以 SPI TX 为例:

#include <linux/dmaengine.h>
#include <linux/dma-mapping.h>
#include <linux/spi/spi.h>

struct my_spi_dma {
    struct spi_device     *spi;
    struct dma_chan       *tx_chan;
    struct dma_chan       *rx_chan;
    struct completion     done;
    size_t                xfer_len;
};

static void my_spi_dma_callback(void *param)
{
    struct my_spi_dma *sd = param;
    complete(&sd->done);
}

static int my_spi_dma_transfer(struct my_spi_dma *sd,
                                struct spi_transfer *xfer)
{
    struct dma_async_tx_descriptor *tx_desc, *rx_desc;
    struct dma_slave_config tx_cfg = {}, rx_cfg = {};
    enum dma_ctrl_flags flags = DMA_PREP_INTERRUPT | DMA_CTRL_ACK;
    dma_cookie_t tx_cookie, rx_cookie;
    int ret;

    /* 1. 配置 TX 通道 (memory -> SPI TX FIFO) */
    tx_cfg.direction = DMA_MEM_TO_DEV;
    tx_cfg.dst_addr = sd->spi->fifo_phys;        // 来自设备树
    tx_cfg.dst_addr_width = DMA_SLAVE_BUSWIDTH_1_BYTE;
    tx_cfg.dst_maxburst = 1;
    dmaengine_slave_config(sd->tx_chan, &tx_cfg);

    /* 2. 配置 RX 通道 (SPI RX FIFO -> memory) */
    rx_cfg.direction = DMA_DEV_TO_MEM;
    rx_cfg.src_addr = sd->spi->fifo_phys;        // 来自设备树
    rx_cfg.src_addr_width = DMA_SLAVE_BUSWIDTH_1_BYTE;
    rx_cfg.src_maxburst = 1;
    dmaengine_slave_config(sd->rx_chan, &rx_cfg);

    /* 3. 准备 TX DMA 描述符 */
    tx_desc = dmaengine_prep_slave_sg(sd->tx_chan,
        xfer->tx_sgl, xfer->tx_nents, DMA_MEM_TO_DEV, flags);
    if (!tx_desc) {
        dev_err(&sd->spi->dev, "TX prep failed\n");
        return -EIO;
    }

    /* 4. 准备 RX DMA 描述符 */
    rx_desc = dmaengine_prep_slave_sg(sd->rx_chan,
        xfer->rx_sgl, xfer->rx_nents, DMA_DEV_TO_MEM, flags);
    if (!rx_desc) {
        dev_err(&sd->spi->dev, "RX prep failed\n");
        return -EIO;
    }

    /* 5. 设置 TX 完成回调 */
    tx_desc->callback = my_spi_dma_callback;
    tx_desc->callback_param = sd;

    /* 6. 提交描述符 */
    init_completion(&sd->done);
    tx_cookie = dmaengine_submit(tx_desc);
    rx_cookie = dmaengine_submit(rx_desc);

    if (dma_submit_error(tx_cookie) || dma_submit_error(rx_cookie))
        return -EIO;

    /* 7. 触发传输 */
    dma_async_issue_pending(sd->tx_chan);
    dma_async_issue_pending(sd->rx_chan);

    /* 8. 启动 SPI 外设(产生时钟,驱动数据移位) */
    my_spi_start_clocking(sd->spi);

    /* 9. 等待完成(带超时) */
    ret = wait_for_completion_timeout(&sd->done, msecs_to_jiffies(500));
    if (!ret) {
        dev_err(&sd->spi->dev, "DMA timeout\n");
        dmaengine_terminate_sync(sd->tx_chan);
        dmaengine_terminate_sync(sd->rx_chan);
        return -ETIMEDOUT;
    }

    return 0;
}

/* 驱动 probe */
static int my_spi_probe(struct platform_device *pdev)
{
    struct my_spi_dma *sd;
    dma_cap_mask_t mask;

    sd = devm_kzalloc(&pdev->dev, sizeof(*sd), GFP_KERNEL);

    /* 查询控制器能力 */
    dma_cap_zero(mask);
    dma_cap_set(DMA_SLAVE, mask);

    /* 请求 TX 通道 */
    sd->tx_chan = dma_request_chan(&pdev->dev, "tx");
    if (IS_ERR(sd->tx_chan))
        return PTR_ERR(sd->tx_chan);

    /* 请求 RX 通道 */
    sd->rx_chan = dma_request_chan(&pdev->dev, "rx");
    if (IS_ERR(sd->rx_chan)) {
        dma_release_channel(sd->tx_chan);
        return PTR_ERR(sd->rx_chan);
    }

    platform_set_drvdata(pdev, sd);
    return 0;
}

7.1 设备树绑定示例

spi@ff1d0000 {
    compatible = "vendor,my-spi";
    dmas = <&dma_controller 6>,    /* TX - 使用 DMA 通道 6 */
           <&dma_controller 7>;    /* RX - 使用 DMA 通道 7 */
    dma-names = "tx", "rx";
    ...
};

八、性能优化要点

8.1 合并 Scatter-Gather entries

过多的 SG entries 意味着 DMA 引擎需要加载更多描述符,增加延迟:

/* 合并连续物理地址的 SG entries */
int nents_merged = dma_map_sg_attrs(dev, sg, nents, dir,
    DMA_ATTR_SKIP_CPU_SYNC);  // 跳过 CPU 一致性操作

/* 使用大页 (2MB/1GB) 减少 IOMMU TLB miss */

8.2 DMA 描述符预分配

每次 dmaengine_prep_slave_sg() 会分配新的描述符,高频率场景下应预分配:

/* 可以在 probe 时预分配描述符并缓存 */
struct dma_async_tx_descriptor *pre_alloc_desc;
pre_alloc_desc = dmaengine_prep_slave_sg(chan, sg_template, 1, 
                                          DMA_MEM_TO_DEV, 0);

8.3 中断聚合

对于网络接口卡等高频场景,每个传输触发一次中断是不可接受的:

  • 使用 NAPI:每 N 个包才触发一次中断
  • 或使用 DMA_PREP_FENCE 在描述符链中插入 memory barrier,确保写顺序

8.4 中断亲和性

将 DMA 中断绑定到处理数据的同一 NUMA 节点 CPU:

# 查看 DMA 中断号
grep dma /proc/interrupts
# 设置亲和性到 CPU 0
echo 1 > /proc/irq/IRQNUM/smp_affinity

九、调试与 Trace

9.1 ftrace DMA trace

# 查看 dmaengine 事件
cd /sys/kernel/debug/tracing
echo 1 > events/dma/enable
cat trace_pipe

# 输出示例:
# dma_request_chan dev=ff1d0000.spi chan_id=6
# dma_prep_slave_sg chan_id=6 len=4096 src=0x... dst=0x...
# dma_complete chan_id=6 cookie=35 status=success

9.2 IOMMU debug

# 查看 IOMMU 设备分组
ls /sys/kernel/iommu_groups/*/

# 查看某设备的 IOMMU 映射
cat /sys/kernel/debug/iommu/domain0/mappings
# 输出每行:iova -> phys (size)

# IOMMU page fault 日志
dmesg | grep -i "iommu"
# 典型输出:
# [  +0.000000] DMAR: [DMA Write] Request device [04:00.0] 
# fault addr 0xdeadbeef PTE Read access is not set

9.3 crash 工具分析 DMA 死锁

# 当 DMA 通道被锁定时,分析持有通道的进程
crash> struct dma_chan -o device_node  # 遍历所有通道
crash> bt <pid>  # 查看等待 DMA 完成的进程栈

十、总结

Linux DMAEngine 与 IOMMU 子系统是现代高性能外设驱动的核心基础设施。回顾关键要点:

组件 职责 关键 API
DMAEngine Core 通道管理、能力描述 dma_request_chan()
async_tx 异步事务语义、依赖链 dmaengine_prep_slave_sg()
DMA Controller Drv 硬件寄存器操作 device_prep_memcpy() 等
IOMMU Core 地址转换机构 iommu_map() / iommu_unmap()
DMA Mapping API IOMMU ↔ 设备驱动 桥接 dma_map_sg() / dma_map_page()

在驱动开发中的几个铁律:

  1. 永远不要让设备接收物理地址——除非你确定 IOMMU 被禁用
  2. DMA 方向必须正确——DMA_FROM_DEVICE 时 CPU 读取数据前必须先 sync
  3. 描述符链上的 DMA_PREP_INTERRUPT只在最后一个设置——避免中断风暴
  4. 释放通道前务必 terminate——否则下一次 probe 会发生 "channel busy"
  5. 生产环境不要禁用 IOMMU——passthrough 模式是最后手段

DMA 编程是系统底层的硬核领域,理解其边界条件和 IOMMU 交互,是编写稳定、安全高质量设备驱动的前提。


本文基于 Linux 6.6+ 内核分析,DMAEngine 子系统代码位于 drivers/dma/,IOMMU 子系统代码位于 drivers/iommu/。

点赞(0) 打赏

评论列表 共有 0 条评论

暂无评论
立即
投稿

微信公众账号

微信扫一扫加关注

发表
评论
返回
顶部