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 可以直接操作任意位置。这意味着:
- 安全性:恶意网卡可以读取 SSH 密钥所在内存页
- 稳定性:有 bug 的 DMA 引擎可能写入内核代码段
- 灵活性: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() |
在驱动开发中的几个铁律:
- 永远不要让设备接收物理地址——除非你确定 IOMMU 被禁用
- DMA 方向必须正确——
DMA_FROM_DEVICE时 CPU 读取数据前必须先 sync - 描述符链上的
DMA_PREP_INTERRUPT只在最后一个设置——避免中断风暴 - 释放通道前务必 terminate——否则下一次 probe 会发生 "channel busy"
- 生产环境不要禁用 IOMMU——passthrough 模式是最后手段
DMA 编程是系统底层的硬核领域,理解其边界条件和 IOMMU 交互,是编写稳定、安全高质量设备驱动的前提。
本文基于 Linux 6.6+ 内核分析,DMAEngine 子系统代码位于 drivers/dma/,IOMMU 子系统代码位于 drivers/iommu/。

发表评论 取消回复