---
title: Linux内核DMA与IOMMU深度实战:从直接内存访问到I/O虚拟化工程指南
category: Linux内核
keywords: DMA, IOMMU, IOVA, dmaengine, dma_map_sg, dma_alloc_coherent, vfio, SVA, IOTLB, swiotlb
description: 深度解析Linux内核DMA子系统与IOMMU/I/O虚拟化的实现原理与工程实战,涵盖DMA映射类型、dmaengine框架、IOMMU工作原理、IOTLB管理、SVA直通、vfio设备直通、虚拟化场景DMA重映射及DMA攻击防御。
flag: recommend,top
---
# Linux内核DMA与IOMMU深度实战:从直接内存访问到I/O虚拟化工程指南
## 一、引言:为什么DMA和IOMMU是系统内核的基石
现代计算机系统中,CPU不是唯一能访问内存的实体。磁盘控制器、网卡、GPU、PCIe外设都通过**直接内存访问(DMA)**在不经过CPU参与的情况下读写主存。DMA使得高吞吐I/O成为可能——一块万兆网卡若要通过CPU拷贝每秒1250MB的数据,将不堪重负。
然而DMA也带来两大问题:
1. **地址空间不匹配**:外设看到的物理地址与CPU视角不同,32位设备无法访问4GB以上内存。
2. **安全隐患**:恶意或被入侵的外设可通过DMA任意读写物理内存,即"DMA攻击"。
**IOMMU(I/O Memory Management Unit)** 正是为解决这两个问题而生。它扮演外设地址翻译和访问控制的角色,类似CPU的MMU,但服务对象是I/O设备。
截止Linux 6.x内核,DMA子系统已成为驱动开发、虚拟化、安全防护的交叉核心。本文从工程实战角度,剖析从DMA操作到IOMMU虚拟化的完整路径。
## 二、DMA核心概念与映射模型
### 2.1 DMA传输模式
内核支持三种DMA传输模式:
| 模式 | 层API | 特点 |
|------|--------|------|
| 一致性DMA (Coherent) | `dma_alloc_coherent()` / `dma_map_single()` | 禁用CPU缓存,软件无需flush;适合长期共享缓冲区 |
| 流式DMA (Streaming) | `dma_map_single()` / `dma_map_sg()` / `dma_unmap_*()` | 单向传输,需显式sync;适合单次I/O |
| 分散- Gather (SG) | `dma_map_sg()` | 将不连续物理页面聚合为散列表,减少映射开销 |
### 2.2 DMA映射的关键约束
一次DMA映射需解决以下关键问题:
- **地址宽度**:设备dma_mask决定它能寻址的物理范围。32位设备只能访问4GB以下内存,需借助**bounce buffer**(swiotlb)或IOMMU重映射。
- **缓存一致性**:DMA写到内存的数据可能暂存于CPU cache中,读取前需`dma_sync_*()`。
- **映射粒度**、**方向**(`DMA_TO_DEVICE`、`DMA_FROM_DEVICE`、`DMA_BIDIRECTIONAL`)。
### 2.3 DMA映射API详解
```c
/* 1. 一致性DMA —— 分配设备可访问的一致性内存 */
void *dma_alloc_coherent(struct device *dev, size_t size,
dma_addr_t *dma_handle, gfp_t gfp);
/* 2. 流式单区映射 */
dma_addr_t dma_map_single(struct device *dev, void *ptr,
size_t size, enum dma_data_direction dir);
/* 3. 流式分散列表映射 —— 最高效 */
int dma_map_sg(struct device *dev, struct scatterlist *sg,
int nents, enum dma_data_direction dir);
/* 4. 解除映射 */
void dma_unmap_single(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);
/* 5. CPU访问前同步 */
void dma_sync_single_for_cpu(struct device *dev, dma_addr_t addr,
size_t size, enum dma_data_direction dir);
void dma_sync_single_for_device(struct device *dev, dma_addr_t addr,
size_t size, enum dma_data_direction dir);
```
> **工程实践**:流式映射在`dma_map_*()`后、DMA完成中断前,CPU不应触碰缓冲区;DMA完成后、CPU读取前需`dma_sync_single_for_cpu()`。
### 2.4 分散列表(scatterlist)内核实现
```c
struct scatterlist {
unsigned long page_link; // 指向struct page
unsigned int offset; // 页内偏移
unsigned int length; // 段长度
dma_addr_t dma_address; // 映射后的总线地址
unsigned int dma_length;
};
```
SG映射将CPU视角的不连续页面列表转换为DMA总线地址序列。核心算法在`dma_map_sg()`中:调用IOMMU或swiotlb将dma_address填入每个scatterlist项。
### 2.5 Bounce Buffer与swiotlb
当32位设备需访问4GB以上内存时,kernel启动参数`swiotlb=force`启用弹跳缓冲区:
```
物理内存 > 4GB ──swiotlb──> 拷贝到32位可访问区域 ──DMA──> 设备
```
`swiotlb`是内核的一段低4GB内存,DMA到该区域后再由CPU(非DMA)拷贝至真实目标。性能有损但有兼容性。
## 三、IOMMU子系统架构
### 3.1 IOMMU工作原理
IOMMU是外设地址翻译器,工作模型类似MMU:
```
设备发出 DMA 请求 (IOVA) ──> IOMMU ──> 查 I/O页表 ──> 物理地址
│
中断/异常 (页故障)
```
**IOVA(I/O Virtual Address)** 是设备看到的地址,IOMMU将其翻译为真实物理地址。通过页表项权限位,IOMMU可拦截非法访问。
主流IOMMU标准:
| 标准 | 平台 | 说明 |
|------|------|------|
| Intel VT-d | x86 Intel | 第二代支持PASID/SVA |
| AMD-Vi | AMD | sva PASID支持 |
| SMMU | ARM | SMMUv3支持多流、命令队列 |
| PAMU | NXP PowerPC | - |
### 3.2 Linux IOMMU子系统层次结构
```
┌──────────────────────────────┐
│ DMA API(通用层) │
└──────────────┬───────────────┘
│
┌──────────────▼───────────────┐
│ iommu.c / iommu-dma.c │ 通用IOMMU DMA层
└──────────────┬───────────────┘
│
┌──────────────▼───────────────┐
│ 硬件驱动(iommu/intel/ │
│ iommu/arm-smmu-v3.c 等) │
└──────────────────────────────┘
```
关键结构体:
```c
struct iommu_domain {
const struct iommu_domain_ops *ops;
unsigned long pgsize_bitmap;
void *priv;
...
};
struct iommu_domain_ops {
int (*attach_dev)(struct iommu_domain *domain, struct iommu_group *group);
int (*detach_dev)(struct iommu_domain *domain, struct iommu_group *group);
int (*map)(struct iommu_domain *domain, unsigned long iova,
phys_addr_t paddr, size_t size, int prot, gfp_t gfp);
size_t (*unmap)(struct iommu_domain *domain, unsigned long iova,
size_t size, struct iommu_iotlb_gather *gather);
void (*flush_iotlb_all)(struct iommu_domain *domain);
void (*iotlb_sync)(struct iommu_domain *domain,
struct iommu_iotlb_gather *gather);
phys_addr_t (*iova_to_phys)(struct iommu_domain *domain, dma_addr_t iova);
void (*free)(struct iommu_domain *domain);
};
```
### 3.3 IOMMU Domain类型
| 类型 | 创建API | 用途 |
|------|---------|------|
| `IOMMU_DOMAIN_BLOCKED` | 默认 | 阻止所有DMA |
| `IOMMU_DOMAIN_IDENTITY` | `iommu_domain_alloc()` | 1:1映射(iOVA=PA) |
| `IOMMU_DOMAIN_UNMANAGED` | 驱动手动map/unmap | 内核驱动直控页表 |
| `IOMMU_DOMAIN_DMA` | DMA API自动管理 | 自动翻译+IOTLB flush |
DMA API层使用的始终是`IOMMU_DOMAIN_DMA`,它维护IOVA分配器(默认`iova_cookie`)。
### 3.4 IOTLB管理
IOMMU硬件维护一个TLB加速地址翻译(IOTLB)。当驱动unmap或页表结构变化时,必须flush IOTLB:
```c
struct iommu_iotlb_gather {
unsigned long start;
unsigned long end;
unsigned long pgsize;
};
int iommu_unmap(struct iommu_domain *domain, unsigned long iova, size_t size);
void iommu_flush_iotlb_all(struct iommu_domain *domain);
void iommu_iotlb_sync(struct iommu_domain *domain, struct iommu_iotlb_gather *gather);
```
Linux内核通过**懒刷新策略**延迟flush,多次unmap合并为一次同步,减少MMIO开销。
## 四、dmaengine框架与实战
### 4.1 dmaengine概览
`dmaengine`是内核DMA控制器抽象层,让设备驱动以统一接口使用硬件DMA引擎(如Intel IOAT、PLX、FPGA DMA等),无需关心底层DMA控制器差异。
```c
struct dma_chan {
struct dma_device *device;
dma_cookie_t cookie;
...
};
struct dma_device {
struct list_head channels;
unsigned int cap_mask;
...
};
struct dma_async_tx_descriptor {
dma_cookie_t cookie;
enum dma_ctrl_flags flags;
dma_addr_t phys;
struct dma_chan *chan;
dmaengine_callback callback;
void *callback_param;
...
};
```
### 4.2 申请DMA通道
```c
/* 从设备树或ACPI获取DMA通道索引,再通过device_request_dma_chan_mask申请 */
struct dma_chan *dma_request_chan(struct device *dev, const char *name);
/* 过滤申请 —— 按控制器能力筛选 */
struct dma_chan *dma_request_chan_by_mask(const struct dma_cap_mask *mask);
```
设备树中DMA属性示例:
```dts
mydevice: mydevice@ff200000 {
compatible = "vendor,my-dma-device";
dmas = <&axi_dma 0>, <&axi_dma 1>;
dma-names = "tx", "rx";
};
```
### 4.3 准备传输描述符
```c
/* 单次传输(Slave SG) */
struct dma_async_tx_descriptor *
dmaengine_prep_slave_sg(struct dma_chan *chan, struct scatterlist *sgl,
unsigned int sg_len, enum dma_transfer_direction dir,
unsigned long flags);
/* cyclic —— 循环传输,用于音频 */
struct dma_async_tx_descriptor *
dmaengine_prep_dma_cyclic(struct dma_chan *chan, dma_addr_t buf_len,
dma_addr_t period_len,
enum dma_transfer_direction dir, unsigned long flags);
/* memcpy */
struct dma_async_tx_descriptor *
dmaengine_prep_dma_memcpy(struct dma_chan *chan, dma_addr_t dst,
dma_addr_t src, size_t len, unsigned long flags);
```
### 4.4 提交、等待、回调
```c
/* 提交描述符,获得cookie */
dma_cookie_t dmaengine_submit(struct dma_async_tx_descriptor *desc);
/* 启动该通道上所有已提交传输 */
void dma_async_issue_pending(struct dma_chan *chan);
/* 等待完成 */
enum dma_status dma_sync_wait(struct dma_chan *chan, dma_cookie_t cookie);
/* 完成回调 */
typedef void (*dmaengine_callback)(void *param);
```
### 4.5 实战:编写一个简单DMA字符驱动
```c
static void dma_complete(void *arg)
{
complete((struct completion *)arg);
}
static int do_dma_transfer(struct device *dev, dma_addr_t src, dma_addr_t dst, size_t len)
{
struct dma_chan *chan;
struct dma_async_tx_descriptor *tx;
DECLARE_COMPLETION_ONSTACK(done);
dma_cookie_t cookie;
enum dma_status status;
int ret = 0;
/* 1. 获取通道 */
chan = dma_request_chan(dev, "memcpy");
if (IS_ERR(chan))
return PTR_ERR(chan);
/* 2. 准备传输 */
tx = dmaengine_prep_dma_memcpy(chan, dst, src, len,
DMA_PREP_INTERRUPT | DMA_CTRL_ACK);
if (!tx) {
ret = -ENOMEM;
goto release;
}
/* 3. 设置回调 */
tx->callback = dma_complete;
tx->callback_param = &done;
/* 4. 提交 + 启动 */
cookie = dmaengine_submit(tx);
dma_async_issue_pending(chan);
/* 5. 等待完成 */
if (!wait_for_completion_timeout(&done, msecs_to_jiffies(1000))) {
ret = -ETIMEDOUT;
} else {
status = dma_sync_wait(chan, cookie);
if (status != DMA_COMPLETE)
ret = -EIO;
}
release:
dma_release_channel(chan);
return ret;
}
```
## 五、IOMMU在虚拟化与容器中的应用
### 5.1 vfio与设备直通
**VFIO(Virtual Function I/O)** 是Linux内核的用户态IOMMU管理框架,用于容器和虚拟机将物理设备直接"passthru"给guest,接近原生性能。
VFIO使用IOMMU构建IOVA→HPA映射,确保虚拟机内驱动操作设备时只能访问分配到的内存。
```c
/* VFIO 核心结构 */
struct vfio_iommu_driver_ops {
char *name;
struct module *owner;
void *(*open)(unsigned long arg);
void (*release)(void *iommu_data);
long (*ioctl)(void *iommu_data, unsigned int cmd,
unsigned long arg);
int (*attach_group)(void *iommu_data,
struct iommu_group *iommu_group,
enum vfio_group_type type);
void (*detach_group)(void *iommu_data,
struct iommu_group *iommu_group);
int (*pin_pages)(void *iommu_data, struct iommu_group *group,
unsigned long *phys_pfn, int npage,
int prot, unsigned long *phys_mm);
void (*unpin_pages)(void *iommu_data,
unsigned long *phys_pfn, int npage);
int (*register_notifier)(void *iommu_data,
unsigned long *args,
struct notifier_block *nb);
...
};
```
**QEMU vfio设备直通**常用命令:
```bash
# 解绑原驱动,绑定到vfio-pci
echo "0000:02:00.0" > /sys/bus/pci/devices/0000:02:00.0/driver/unbind
echo "vfio-pci" > /sys/bus/pci/devices/0000:02:00.0/driver_override
echo "0000:02:00.0" > /sys/bus/pci/drivers/vfio-pci/bind
# QEMU启动
qemu-system-x86_64 \
-device vfio-pci,host=02:00.0 \
...
```
### 5.2 Scalable I/O Virtualization (SIOV)
Intel SIOV是新一代I/O虚拟化架构,将物理设备拆为大量**ADF(Assignable Device Interface)**,每个容器有自己的轻量级IOVA空间,减少hypervisor开销。
| 特性 | SR-IOV | SIOV |
|------|--------|------|
| 虚拟功能 | PF/VF(数:数十) | PF/ADF(数百+) |
| 设备状态 | 每VF较大 | 极简(仅MMIO) |
| IOVA管理 | VFIO处理 | 硬件辅助辅助 |
### 5.3 SVA(Shared Virtual Addressing)
SVA让设备直接看到CPU页表(PASID),无需IOVA映射转换。设备通过PASID标签使用CPU页表:
```
CPU: CR3 ──MMU──> 物理地址
DEV: PASID ──IOMMU (共享CR3页表) ──> 物理地址
```
SVA需要IOMMU硬件支持PASID表(Intel VT-d 2nd Gen、ARM SMMUv3)。开启后``显示最大PASID数。
```c
/* 启用PASID */
iommu_dev_enable_feature(dev, IOMMU_DEV_FEAT_PASID);
/* 附加PASID到domain */
iommu_sva_bind_device(dev, mm, &handle);
```
### 5.4 DMA重映射与中断重映射
IOMMU不仅翻译地址,还处理**中断重映射(Interrupt Remapping)**,为虚拟机隔离中断源。对于直通设备,VFIO使用`VFIO_DMA_MAP_FLAG_READ | VFIO_DMA_MAP_FLAG_WRITE`注册DMA窗口。用户空间API:
```c
struct vfio_iommu_type1_dma_map {
__u32 argsz;
__u32 flags;
__u64 vaddr; // 用户空间IOVA
__u64 iova; // IO虚拟地址
__u64 size;
};
ioctl(vfio_fd, VFIO_IOMMU_MAP_DMA, &dma_map);
```
## 六、IOMMU性能调优与DMA工程最佳实践
### 6.1 IOTLB Flush策略
每次页变更都刷IOTLB是全核同步点。Linux内核默认启用**"batch"模式**,延迟批量flush:
```c
/* 默认已开启懒模式 */
/proc/sys/kernel/nmi_watchdog // 无关
/sys/module/iommu/parameters/ // 各种调试选项
```
关键参数:
| 参数 | 调整方向 |
|------|---------|
| `iommu.strict=1` | 严格模式:每次unmap立即flush(调试用) |
| `iommu.passthrough=1` | 关闭翻译(性能测试用) |
| `swiotlb=force` | 强制使用bounce buffer |
虚拟机直通场景下,过度flush严重降低性能。生产环境建议保持默认懒模式,除非检测到安全问题。
### 6.2 IOVA分配器选择
默认使用`iova_cookie`(基于IOVA-range分配器)。对于高吞吐场景(如NVMe),可配置:
```bash
# 扩大IOVA范围
intel_iommu=on iommu.passthrough=0
```
### 6.3 大页与IOMMU
IOMMU支持2MB/1GB大页映射(类似CPU大页)。内核6.0+默认在映射时使用大页:
```
dma_map_sg():
══> iommu_map() 尝试同名HugePage合并
══> 硬件IOTLB一次缓存更大范围
```
对高速外设(RDMA、NVMe-oF),大页映射显著减少IOTLB entry数。
### 6.4 设备DMA能力声明
驱动通过`dma_set_mask_and_coherent()`声明设备DMA能力:
```c
/* 声明设备支持64位DMA */
if (dma_set_mask_and_coherent(&pdev->dev, DMA_BIT_MASK(64))) {
/* 降级到32位 */
if (dma_set_mask_and_coherent(&pdev->dev, DMA_BIT_MASK(32))) {
dev_err(&pdev->dev, "No suitable DMA available\n");
return -ENODEV;
}
}
```
如果声明不准确,超出设备能力的物理地址会导致DMA失败或数据损坏。
### 6.5 DMA调试工具
| 工具/接口 | 用途 |
|-----------|------|
| `CONFIG_DMA_API_DEBUG` | 检测未unmap的泄漏映射 |
| `/sys/kernel/debug/dma_buf/bufinfo` | 查看所有DMA-BUF分配 |
| `trace_dma_map`/`trace_dma_unmap` | IOMMU映射追踪 |
| `intel_iommu=igfx_off` | 关闭集成显卡IOMMU(debug用) |
| `vfio_iommu_type1 allow_unsafe_interrupts=1` | 允许不安全中断直通 |
启用调试:
```bash
# 开启DMA调试
echo 1 > /sys/module/dma/parameters/debug_on
# 追踪IOMMU事件
echo 1 > /sys/kernel/debug/tracing/events/iommu/enable
```
## 七、DMA攻击与防御工程
### 7.1 DMA攻击面
恶意PCIe设备(如Thunderbolt外设)可绕过CPU防护直接读写内存:
- **冷启动攻击替代**:DMA读取内存中的密钥和敏感数据
- **权限提升**:覆写内核关键结构(如sys_call_table)
- **隐蔽通信**:DMA写入攻击者可控的物理内存
### 7.2 内核防御机制
| 防御层 | 机制 | 原理 |
|--------|------|------|
| IOMMU | 地址重映射+权限 | 设备只能访问分配到的物理页 |
| Thunderbolt安全级别 | `security=secure` | 用户态授权外设后才能DMA |
| `iommu.strict` | 严格模式 | 立即flush,减少竞争窗口 |
| `CONFIG_IOMMU_DEFAULT_DMA_STRICT` | 默认严格策略 | 所有映射默认不共享 |
```bash
# 查看当前IOMMU策略
cat /sys/class/iommu/*/policy
# 或 dmesg | grep -i iommu
```
### 7.3 内核启动参数安全加固
```bash
intel_iommu=on iommu.strict=1
amd_iommu=on iommu.strict=1
```
配合Secure Boot + IMA,DMA攻击面可被有效收窄。
## 八、性能基准与常见陷阱
### 8.1 性能基准参考
| 场景 | 吞吐量 |
|------|--------|
| 一致性DMA (coherent) | ~10-50 GB/s(带宽) |
| 流式 DMA (streaming) | ~30-80 GB/s |
| SWIOTLB bounce buffer | <2 GB/s(CPU拷贝额外开销) |
| IOMMU关闭(1:1) | ~30-80 GB/s(轻微下降) |
| SVA(共享页表) | ~60-100 GB/s(消除IOVA分配) |
### 8.2 常见陷阱与解决方案
| 问题 | 根因 | 解决 |
|------|------|------|
| DMA映射溢出错误 | nents > dma_dev->max_sg_entries | 拆分或增大segment限制 |
| DMA timeout | 设备未响应 | 检查dma_mask和中断路由 |
| IOMMU fault | 非法IOVA访问 | 驱动探测前映射不完整 |
| DMA-flush竞争 | CPU与设备并发修改同一页 | 正确sync_before/after |
| 驱动加载OOM | 映射过多未释放 | `dma_map_*()`后必须`dma_unmap_*()` |
### 8.3 正确与错误示例
**错误:**
```c
/* 错误:在DMA完成前CPU读取结果缓冲区(缓存未同步) */
dma_map_single(dev, buf, len, DMA_FROM_DEVICE);
/* ... 设备DMA写入内存 ... */
read_result(buf); /* ❌ CPU读到脏cache */
```
**正确:**
```c
/* DMA完成后同步 */
dma_map_single(dev, buf, len, DMA_FROM_DEVICE);
wait_for_completion(&dma_done);
dma_sync_single_for_cpu(dev, dma_handle, len, DMA_FROM_DEVICE);
read_result(buf); /* ✓ 获得最新数据 */
dma_unmap_single(dev, dma_handle, len, DMA_FROM_DEVICE);
```
## 九、总结与工程路线图
DMA与IOMMU子系统横跨驱动开发、虚拟化、性能优化和安全防护。掌握其核心API、IOMMU映射模型和错误处理原则,是高级Linux内核/驱动工程师的基本功。
**推荐学习路线:**
1. **DMA API基础**:从`dma_map_sg()`和scatter列表开始
2. **IOMMU工作原理**:理解domain、group、IOVA、IOTLB
3. **dmaengine框架**:用DMA引擎驱动SPI/I2S等外设
4. **vfio设备直通**:在KVM/QEMU中实践GPU/RDMA直通
5. **SVA/PASID**:在多进程DMA共享场景中应用
6. **DMA安全加固**:IOMMU策略与thunderbolt安全级别
随着SIOV、scalable IOMMU等新技术演进,I/O虚拟化将以更细粒度支持容器的原生设备访问,使得每个Docker容器能获得接近裸机性能的GPU、FPGA、SSD——这是未来基础设施的关键。
## 参考资料
- Linux内核文档:`Documentation/core-api/dma-api.rst`、`Documentation/admin-guide/kernel-parameters.txt`
- Intel VT-d Specification
- AMD IOMMU Specification
- `drivers/iommu/` 内核源码
- `Documentation/driver-api/dmaengine.rst`

发表评论 取消回复