4. 缓冲区映射机制:DMA映射原理、sg_table与scatterlist、IOMMU与SMMU映射流程
在V4L2 videobuf2框架中,缓冲区映射是连接用户空间、内核空间与硬件DMA引擎的核心桥梁。本章深入剖析DMA映射的底层原理、sg_table与scatterlist的数据结构设计,以及IOMMU/SMMU在映射流程中的关键作用,帮助开发者精准优化内存访问路径。
4.1 DMA映射原理与核心挑战
DMA(Direct Memory Access)允许硬件设备直接读写系统内存,无需CPU逐字节搬运。但在现代系统中,DMA映射面临三大挑战:
- 物理地址不连续:内核分配的大缓冲区可能由多个物理碎片组成,而DMA引擎通常要求连续的物理地址。
- Cache一致性:CPU与DMA设备对同一内存区域的访问可能因Cache导致数据不一致。
- 地址空间隔离:设备使用总线地址(如PCI总线地址),与CPU的虚拟/物理地址不同。
核心原则:DMA映射的本质是建立“设备可见地址”与“内存物理页”之间的映射关系,并保证数据一致性。
videobuf2通过dma_buf和sg_table机制统一处理这些挑战。下图展示了DMA映射的基本数据流:
4.2 sg_table与scatterlist数据结构
sg_table(Scatter-Gather Table)是videobuf2管理DMA映射的核心数据结构,它描述了一组物理上不连续但逻辑上连续的内存块。其底层依赖scatterlist链表。
4.2.1 数据结构定义
// include/linux/scatterlist.h
struct scatterlist {
unsigned long page_link; // 指向物理页,低2位用于标志
unsigned int offset; // 页内偏移
unsigned int length; // 该段长度
dma_addr_t dma_address; // DMA总线地址(映射后填充)
unsigned int dma_length; // DMA传输长度
};
// include/linux/dma-mapping.h
struct sg_table {
struct scatterlist *sgl; // scatterlist数组首地址
unsigned int nents; // 实际DMA映射的段数
unsigned int orig_nents; // 原始段数(映射前)
};
关键区别:
nents是经过DMA映射后的有效段数,可能小于orig_nents(当IOMMU合并相邻段时);驱动应使用nents遍历DMA地址。
4.2.2 scatterlist链表结构图
4.3 IOMMU与SMMU映射流程
IOMMU(I/O Memory Management Unit)和SMMU(System MMU)为DMA提供地址转换和访问控制能力,使得设备可以使用虚拟地址访问内存,内核只需维护IOMMU页表。
4.3.1 IOMMU vs 非IOMMU对比
| 特性 | 无IOMMU | 有IOMMU/SMMU |
|---|---|---|
| 地址要求 | 物理连续 | 支持物理非连续 |
| 映射开销 | 低(直接使用物理地址) | 中(维护页表) |
| 最大缓冲区 | 受限于连续内存大小 | 仅受系统内存总量限制 |
| 安全性 | 设备可访问任意物理内存 | 隔离设备访问范围 |
| sg_table nents | nents == orig_nents | nents ≤ orig_nents(可能合并) |
4.3.2 IOMMU映射流程详解
videobuf2通过dma_map_sg()和dma_unmap_sg()完成IOMMU映射与解映射。典型流程如下:
- 准备sg_table:从v4l2_buffer获取已分配的物理页,填充scatterlist数组。
- 调用dma_map_sg():传入设备指针、sg_table和DMA方向。
- IOMMU页表更新:内核为每个scatterlist段分配IOMMU地址空间,建立映射。
- 合并优化:若相邻段在IOMMU地址空间连续,则合并为单一DMA段。
- 获取DMA地址:通过sg_dma_address()和sg_dma_len()获取映射后的地址和长度。
- 启动DMA传输:将DMA地址列表提交给硬件DMA引擎。
- 传输完成:调用dma_unmap_sg()释放IOMMU映射。
性能陷阱:频繁的dma_map/unmap操作会引入IOMMU TLB刷新开销。对于视频流场景,建议在缓冲区初始化时一次性映射,在流关闭时统一解映射,避免每帧都进行映射操作。
4.3.3 IOMMU映射流程SVG图
4.4 videobuf2中的DMA映射实现
videobuf2通过内存后端(memory backend)抽象DMA映射操作。以vb2_dma_contig和vb2_dma_sg为例:
4.4.1 核心回调函数
| 回调函数 | 作用 | 典型实现 |
|---|---|---|
alloc |
分配DMA缓冲区 | dma_alloc_coherent() 或 dma_alloc_attrs() |
put_dma_buf |
引用计数管理 | dma_buf_put() |
get_dma_buf |
获取dma_buf引用 | dma_buf_get() |
map_dma_buf |
建立DMA映射 | dma_map_sg_attrs() |
unmap_dma_buf |
解除DMA映射 | dma_unmap_sg_attrs() |
attach |
附加设备 | dma_buf_attach() |
detach |
分离设备 | dma_buf_detach() |
4.4.2 映射代码示例(dma_sg后端)
// drivers/media/common/videobuf2/videobuf2-dma-sg.c
static int vb2_dma_sg_map_dma_buf(void *buf_priv, struct device *dev,
struct sg_table *sgt,
enum dma_data_direction dma_dir)
{
struct vb2_dma_sg_buf *buf = buf_priv;
struct sg_table *sgt_local;
int ret;
sgt_local = kzalloc(sizeof(*sgt_local), GFP_KERNEL);
if (!sgt_local)
return -ENOMEM;
// 复制原始sg_table
ret = sg_alloc_table_from_sg(sgt_local, sgt->sgl, sgt->orig_nents, GFP_KERNEL);
if (ret) {
kfree(sgt_local);
return ret;
}
// 执行DMA映射,nents可能被IOMMU合并
sgt_local->nents = dma_map_sg_attrs(dev, sgt_local->sgl,
sgt_local->orig_nents,
dma_dir,
DMA_ATTR_SKIP_CPU_SYNC);
if (!sgt_local->nents) {
sg_free_table(sgt_local);
kfree(sgt_local);
return -ENOMEM;
}
buf->dma_dir = dma_dir;
buf->dma_sgt = sgt_local;
return 0;
}
调优要点:
DMA_ATTR_SKIP_CPU_SYNC标志可跳过CPU Cache同步,适用于驱动自行管理一致性的场景,能减少约30%的映射开销。
4.5 映射调优实战建议
- 批量映射:在
start_streaming中一次性映射所有缓冲区,在stop_streaming中统一解映射,避免每帧映射开销。 - IOMMU合并利用:通过
iommu_pgsize了解IOMMU页大小,分配时尽量对齐页大小以促进合并。 - Cache一致性策略:对于视频数据,优先使用
dma_alloc_coherent(一致性映射)或dma_map_sg_attrs配合DMA_ATTR_NON_CONSISTENT。 - 调试工具:使用
trace_iommu_map和/sys/kernel/debug/iommu观察映射行为。 - 避免过度映射:对于小缓冲区(< 64KB),非连续映射的开销可能超过收益,考虑使用dma_contig后端。
性能基准:在ARM64平台(SMMUv3)上,批量映射相比逐帧映射可降低DMA映射延迟约40%-60%,帧率提升可达15%-25%。