前段时间调一个视频采集驱动,碰到一个折腾了很久的问题:应用一调用VIDIOC_REQBUFS,内核日志里就出现 DMA 映射失败的报错,摄像头不出图。当时第一反应是怀疑硬件、怀疑接线,查到最后才发现,问题出在自己对videobuf2 和 DMA 内存管理之间的关系理解不够完整。videobuf2 这个框架说起来只是 V4L2 的缓冲区管理机制,但它跟 DMA 地址、cache 一致性、IOMMU 映射、物理内存连续性这些东西紧紧绑在一起。不懂这一层,驱动写出来往往是"半身不遂":平时看似正常,一换平台、一换内存配置就崩。
下面这篇文章,我把 videobuf2 的内存模型、DMA 映射的关键点、以及自己实现一个内存后端时踩过的坑完整梳理一遍。适合正在写 V4L2 采集驱动、或者想搞懂"为什么用某个 mem_ops"的内核开发朋友参考。
1. 一次实际调试:DMA 映射失败背后暴露的选型盲区
1.1 现象与现场
那台板子用的是一颗带 SMMU 的 ARM SoC,跑的是比较新的内核,采集端是标准的 video capture 设备。应用层流程很简单:VIDIOC_REQBUFS申请 4 个缓冲区,然后VIDIOC_QBUF把缓冲区排进队列,再VIDIOC_STREAMON开始采集。结果第一次REQBUFS阶段就失败,dmesg 里的报错信息大致是"dma memory allocation failed"或者"mapping failed",具体措辞记不清了,但问题本质是:驱动向 DMA 层要了一块内存,DMA 层给不出来。
一开始我以为是硬件环境问题,因为同样的应用在另外一块没有 SMMU 的板子上是正常的。后来翻驱动注册vb2_queue的代码才发现,驱动里把q->mem_ops设置成了&vb2_dma_contig_memops,也就是要求分配物理连续的 DMA 内存。那块板子上没有配置足够的 CMA 预留区域,系统内存又比较紧张,连续内存申请失败,整个REQBUFS就直接失败了。
1.2 videobuf2 到底管了什么
我当时对 videobuf2 的理解太粗:以为它只是一个"帮你排队缓冲区的框架"。实际上它是内存生命周期的调度者,不是内存分配器。整个链路是这样的:
应用VIDIOC_REQBUFS之后,vb2 框架会调用驱动实现的queue_setup()来确定格式、平面数量、每个平面的字节数,然后根据q->mem_ops去执行真正的内存分配。分配出来的内存,要能被 DMA 设备访问,又要能被应用 mmap 访问,中间还有 cache 同步、SG 表构建、IOMMU 映射等一系列操作。
换句话说,vb2 决定什么时候需要内存、什么时候可以释放内存、缓冲区在用户态和内核态之间怎么流转,而具体的内存操作策略,完全由mem_ops决定。驱动开发者选择不同的mem_ops,就等同于选择了不同的 DMA 内存管理方案。这一步选错,后面全是坑。
后来我把驱动改成&vb2_dma_sg_memops,同时确认 DMA 控制器能走 SMMU 映射,问题就消失了。为什么?因为vb2_dma_sg_memops使用的是"分散内存 + 通过 DMA API 建立映射"的思路,不要求物理连续,对 CMA 的依赖小很多。
2. videobuf2 的缓冲区生命周期:从 queue 到 plane
2.1 三个核心结构体
要理解 videobuf2 与 DMA 内存管理的关系,先得把框架里的 '容器' 概念理清楚。最核心的三个结构体是:
struct vb2_queue:对应一个设备节点的采集队列。它保存了格式、内存操作符mem_ops、驱动回调ops、缓冲区数量、多平面配置等。struct vb2_buffer:对应一个已经申请到的缓冲区。它包含了index、vb2_queue指针、以及指向各 plane 信息的数组。struct vb2_plane:对应一个 buffer 里的"平面"。对 NV12 这类多平面格式,一个 buffer 有 Y 和 UV 两个 plane;对单平面格式,只有一个 plane。每个 plane 记录了length、bytesused、mem_priv等关键信息。
mem_priv是非常关键的一个字段。它不直接存 DMA 地址,而是指向内存后端自己定义的私有结构体。比如vb2_dma_contig_memops在分配时,会在mem_priv里放一个结构体,保存虚拟地址、DMA 地址、大小、方向等信息。框架层不关心你这个私有结构体长什么样,只负责在合适的时机调用你的回调。
2.2 缓冲区状态机:谁在哪个环节碰内存
vb2 内部有一个清晰的状态机,常见状态包括VB2_BUF_STATE_DEQUEUED、PREPARED、QUEUED、DONE、ERROR。理解状态机有助于搞清楚 DMA 内存操作在哪个时机被触发:
REQBUFS阶段:框架分配内存,buffer 处于DEQUEUED之前的初始化状态。VIDIOC_QBUF阶段:框架调用buf_prepare(),驱动在这里填bytesused、配置硬件描述符,此后 buffer 进入PREPARED。对于 DMA 操作来说,prepare回调就是要在这里完成"设备即将访问内存"之前的 cache 同步。VIDIOC_STREAMON阶段:框架调用start_streaming(),驱动把已经PREPARED的 buffer 挂到硬件中断/队列里,进入QUEUED。- 硬件完成一帧后,驱动调用
vb2_buffer_done(),buffer 进入DONE。在DONE状态,如果驱动用的内存后端实现了finish回调,框架会在用户态读取之前调用它,完成"CPU 访问之前"的 cache 同步。 - 应用
DQBUF拿到 buffer 后,buffer 又回到DEQUEUED。
我常跟人打比方:vb2 就是传送带,mem_ops是每一站点的工人,buf_prepare/buf_finish是工人动手干活前的"清场检查"。DMA 映射和 cache 同步就是清场检查的核心内容。
2.3 内核自带的几种 mem_ops 怎么选
Linux 内核里常用的几个内存后端如下:
| 后端 | 内存来源 | DMA 映射方式 | 适用场景 |
|---|---|---|---|
vb2_vmalloc_memops | vmalloc 分配的虚拟地址 | 通常不直接用于硬件 DMA,适合软件模拟/无 DMA 场景 | 虚拟设备、测试设备 |
vb2_dma_contig_memops | dma_alloc_coherent()/CMA | 返回物理连续的 DMA 地址 | 大多数不带 IOMMU 的视频控制器、硬件要求连续地址时 |
vb2_dma_sg_memops | 普通 page 分配 | 构建 SG 表并通过dma_map_sg()映射,可配合 IOMMU | 大分辨率、多缓冲、带 SMMU/IOMMU 的平台 |
选型逻辑其实很直接:
- 如果 DMA 控制器本身就带 SMMU/IOMMU,允许分散的物理页,优先用
vb2_dma_sg_memops。它更灵活,也更能抵抗内存碎片。 - 如果 DMA 控制器只支持物理连续地址,而且你的分辨率不高、缓冲区个数不多,用
vb2_dma_contig_memops就够。但要注意 CMA 区域大小,别在 4K 分辨率 + 16 个缓冲的场景里硬上连续内存。 vb2_vmalloc_memops严格来说不是给真实 DMA 场景用的。它的内存是 vmalloc 出来的虚拟地址连续,物理页完全不保证连续,不能直接给 DMA 控制器用。
3. 与 DMA 强相关的三个核心命题:连续、一致、SG
3.1 物理连续性与 CMA/IOMMU
DMA 设备要访问内存,最朴素的要求是"地址要连续"。有些硬件模块只认一段连续的物理地址,你给它一个 SG 表它反而不会用。这种硬件上,dma_alloc_coherent()是最常见的选择。它内部可能从 CMA 区域分配连续页,也可能直接分配大块高阶页,返回的 DMA 地址可以被设备直接使用。
但连续物理内存在长时间运行的系统中很容易成为稀缺资源。系统跑得越久,物理页碎片化越严重,想一次性分配 8MB 连续内存,失败概率会越来越高。所以现在很多带 SMMU/IOMMU 的 SoC 都倾向于让 DMA 控制器"看"一个连续的 IOVA 空间:物理页可以分散,IOMMU 把它们映射成连续的 IOVA,设备眼中依然是连续地址。
这里有一个很多新手容易踩的误区:dma_alloc_coherent()返回的 DMA 地址,在带 IOMMU 的平台上并不等于物理地址,它是经过 IOMMU 映射后的"总线地址"。你不能拿这个数直接去做phys_to_virt(),也不能把它交给另一个不走 SMMU 的模块。反过来,virt_to_phys()得到的物理地址,也不一定就是 DMA 设备能用的地址。DMA 地址和物理地址是两套概念,中间隔了一个 IOMMU 的映射关系。
从容量角度算一下:1080p 的 NV12 格式,sizeimage = 1920 * 1080 * 3 / 2 ≈ 3MB。申请 8 个 buffer 就是 24MB 连续内存。4K 分辨率的话,单帧约 8.3MB,8 个 buffer 接近 66MB。如果依赖 CMA,你必须确认预留区域是否够用,否则高负载时就会在REQBUFS阶段报内存分配失败。这类问题不是代码逻辑错误,而是内存规划问题,特别隐蔽。
3.2 Cache 一致性:prepare 和 finish 是同一个硬币的两面
无论是 MMAP 模式还是 USERPTR 模式,只要 CPU 和设备共享同一块物理内存,cache 一致性问题就绕不开。DMA 控制器在写内存时,可以绕过 CPU 的 cache 直接写 DRAM;如果 CPU 的 cache 里还留着旧的副本,CPU 去读时就会读到脏数据。反过来,CPU 写好了数据,如果只停留在 cache 里,设备从 DRAM 里读到的可能就是旧数据。
内核为此提供了dma_sync_single_for_device()/dma_sync_single_for_cpu()这类 API,语义上就是"在设备访问前同步给设备"和"在 CPU 访问前同步给 CPU"。videobuf2 的vb2_mem_ops里专门设计了两个回调:
prepare():在 buffer 进入准备状态、即将交给硬件之前触发。通常在这里调用dma_sync_*_for_device(),确保 CPU 端 cache 里的修改已经刷到内存,或者使 cache 失效,让设备能读到最新数据。finish():在设备完成采集、buffer 进入DONE状态之后触发。通常在这里调用dma_sync_*_for_cpu(),让 CPU 能读到设备写入的最新数据。
为什么不能全程用DMA_BIDIRECTIONAL一了百了?能是能,但代价是每次同步都要做双向处理,CPU 和设备来回"互相等",性能下降明显。在视频采集这种高帧率场景里,帧率就是生命,能用单向方向就用单向。
cache 一致性相关 bug 最常见的表现是:图像偶尔出现一条一条的"花屏"、颜色通道错位,或者数据看起来是"半新半旧"的。这类问题很难复现,但只要你把dma_sync的调用时机和分析工具对上,基本一查一个准。
3.3 SG 表:DMA 映射的"分段投递"
不连续内存怎么给 DMA 设备用?答案是 SG 表(scatter-gather table)。一个 buffer 可以对应多个物理页,每个页的地址和长度组成一个 entry,多个 entry 合起来就是struct sg_table。DMA 层会尝试把这些段映射给设备,可能通过 IOMMU 映射成连续 IOVA,也可能逐段告诉 DMA 控制器"你按段搬"。
这里有一个操作层面的要点:dma_map_sg()返回的是映射后的段数,可能小于原来 SG 表的 entry 数,因为某些段在 IOMMU 下被合并了。代码里应该始终以dma_map_sg()的返回值遍历 mapped SG list,而不是直接用原始的sg_table.nents。类似地,dma_unmap_sg()时必须传回原始构造的nents作为参数。这类细节只有在出了问题、去看CONFIG_DMA_API_DEBUG的告警时才会意识到。
对 videobuf2 而言,vb2_dma_sg_memops的核心工作就是把"一堆 page"变成"一个可用的 DMA 映射结果"。如果平台没有 IOMMU,dma_map_sg()之后每个 entry 的dma_address就是物理页地址的转换;如果平台有 IOMMU,则可能是 IOVA。驱动本身不需要关心这些,DMA API 已经帮你封装掉了,但前提是你不要绕过这些 API 去手动操作物理地址。
4. 手写一个简单视频缓冲内存后端:从 alloc 到 map_dmabuf
很多情况下,内核自带的vb2_dma_contig_memops和vb2_dma_sg_memops已经够用。但有些私有平台、特殊内存池、或者需要对接自研内存调度的场景,驱动开发者需要自己实现vb2_mem_ops。我把自己实现过程中最关键的回调逻辑整理如下。
4.1 定义回调组与私有结构体
先看一个最简的私有结构体:
struct my_priv_buf { void *vaddr; dma_addr_t dma_addr; size_t size; enum dma_data_direction dma_dir; struct device *dev; struct sg_table *sgt; /* 如果用 SG 模式 */ struct dma_buf_attachment *dbuf_attach; /* DMABUF 模式 */ atomic_t refcount; };对应的mem_ops大致是:
static const struct vb2_mem_ops my_memops = { .alloc = my_alloc, .put = my_put, .prepare = my_prepare, .finish = my_finish, .vaddr = my_vaddr, .size = my_size, .attach_dmabuf = my_attach_dmabuf, .detach_dmabuf = my_detach_dmabuf, .map_dmabuf = my_map_dmabuf, .unmap_dmabuf = my_unmap_dmabuf, .num_users = my_num_users, };get_userptr/put_userptr的优先级对于 USERPTR 模式同样很重要。如果驱动声明支持VB2_USERPTR,这两个回调必须实现。如果你是纯内核分配模式,可以不管。
4.2 alloc 与 put:分配和释放不能只看一半
最简单的alloc实现,可以直接用 DMA API 分配连续内存:
static void *my_alloc(void *alloc_ctx, unsigned long size, enum dma_data_direction dma_dir, gfp_t gfp_flags) { struct my_priv_buf *buf; struct device *dev = alloc_ctx; buf = kzalloc(sizeof(*buf), GFP_KERNEL); if (!buf) return NULL; buf->vaddr = dma_alloc_coherent(dev, size, &buf->dma_addr, GFP_KERNEL); if (!buf->vaddr) { kfree(buf); return NULL; } buf->dev = dev; buf->size = size; buf->dma_dir = dma_dir; atomic_set(&buf->refcount, 1); return buf; }这里的alloc_ctx在早期版本里经常被忽略,但它实际上是从vb2_queue传下来的设备上下文,很多驱动实现里就是dev指针。如果没有它,dma_alloc_coherent()不知道该用哪个设备去映射,后续 DMA 操作也会出问题。
put回调做的事正好相反:释放 DMA 映射、释放私有结构体。但要注意顺序:先dma_free_coherent(),再kfree()。如果引用了num_users(),还应该在确认没有任何 mmap 引用之后才真正释放内存。很多内存泄漏的驱动,就是在put里漏了dma_free_coherent(),导致每申请一次 buffer,系统就少一块连续内存。
4.3 prepare 与 finish:命中 cache 同步的时机
以下是一个使用 DMA API 同步的示例:
static int my_prepare(void *buf_priv) { struct my_priv_buf *buf = buf_priv; if (buf->dma_dir != DMA_NONE) dma_sync_single_for_device(buf->dev, buf->dma_addr, buf->size, buf->dma_dir); return 0; } static void my_finish(void *buf_priv) { struct my_priv_buf *buf = buf_priv; if (buf->dma_dir != DMA_NONE) dma_sync_single_for_cpu(buf->dev, buf->dma_addr, buf->size, buf->dma_dir); }看起来很短,但这正是容易出错的地方。如果你的内存不是连续单段,而是 SG 表,就得换成dma_sync_sg_for_device()/dma_sync_sg_for_cpu(),并且遍历 mapped 后的表。另一个常见错误是:在prepare里做了dma_map_sg(),在finish里提前做了dma_unmap_sg()。这个顺序一旦混乱,设备访问时映射已经被拆掉,轻则报错,重则 DMA 写穿到别的内存。
vb2 的框架调用时机其实已经把选择留得很清楚了:只要是"CPU 访问完、要交给设备",就对应prepare;只要是"设备写完了、要交给 CPU",就对应finish。不要试图在buf_init/buf_cleanup里做这件事,它们不在正确的缓存同步窗口内。
4.4 USERPTR 和 DMABUF 的特殊处理
USERPTR 模式意味着内存由用户态分配,内核不拥有页的所有权,只能通过get_user_pages()把用户页"钉"住,再把页建立成 SG 表映射给 DMA 设备。这个模式性能上限不如 MMAP,但省去了一次用户态到内核态的数据拷贝?实际上不一定,关键看数据路径。对采集驱动来说,因为设备要直接通过 DMA 写用户内存,USERPTR 对页对齐、cache 策略要求更高。很多 ARM 平台上,用户态 malloc 出来的指针不保证 block 对齐,vb2 内部会做一个偏移处理,这块逻辑如果自己实现,必须仔细看get_user_pages返回的页与用户地址的关系。
DMABUF 模式则是零拷贝的核心。设备 A 导出的struct dma_buf,可以作为设备 B 的采集 buffer 导入。在attach_dmabuf回调里,通常调用dma_buf_attach()建立 attachment 关系;map_dmabuf里调用dma_buf_map_attachment()拿到 SG 表;unmap_dmabuf和detach_dmabuf对应执行反向操作。这个模式下最容易犯的错误是 attachment 生命周期不匹配:dma_buf_map_attachment得到的 SG 表,必须在dma_buf_unmap_attachment之后才能 detach,否则 exporter 可能仍在访问已经释放的映射。
5. 我在实际项目中踩过的三个坑和排查经验
5.1 花屏:不是硬件问题,是 cache 同步时机错误
某次调试一版输出分辨率从 720p 升到 1080p 的驱动,画面小范围花屏,尤其每帧左上角区域频繁出现旧数据。一开始怀疑时钟、怀疑 DDR 带宽,后来开了CONFIG_DMA_API_DEBUG也没报 mapping 错误。最后在驱动里加打印,发现finish回调里的dma_sync_single_for_cpu()只同步了bytesused的前半段,没有同步整个 plane 大小。设备把一整帧写完后,CPU 访问时有一部分还在 cache 里是旧值。
修复方法很简单:把同步范围统一调整为vb2_plane.length对应的字节数,而不是驱动在某个时刻手动填写的bytesused。这次经历让我意识到,不要试图在 DMA 同步范围上"省"那几字节,DMA 访问粒度是 cache line 级别的,边界处理错了,省下的性能远不够修 bug 的时间。
5.2 连续内存分配失败:CMA 预留区域太小
一次在 4K 分辨率产品上跑稳定性测试,持续采集几十分钟后,REQBUFS偶尔失败,dmesg 里出现连续内存分配失败。最开始怀疑内存泄漏,检查put回调、vb2_buffer_done调用次数,都没问题。后来查看内核 cmdline,发现 CMA 区域只预留给了一个很小的值,而当时应用一共申请了 8 个 4K NV12 buffer,单帧 8MB,8 帧就是 66MB。加上系统其他模块也要用 CMA,区域早就满了。
这里的经验是:使用vb2_dma_contig_memops之前,必须做CMA 容量估算。公式很简单:
总需求 = 帧大小 * 缓冲区个数 + 其他模块 CMA 占用量预留 CMA 如果不够,不要犹豫,要么调大cma=内存参数,要么切换成vb2_dma_sg_memops+ IOMMU 方案。对现代平台来说,后者是更顺手的长期方案。
5.3 IOMMU 打开之后:DMA 地址不再等于物理地址
还有一次是调试一个硬件加密模块,它要从采集 buffer 里读取数据。当时驱动直接把vb2_dma_sg_memops里拿到的 DMA 地址传给硬件加密模块,结果加密模块读到的全是垃圾。原因就是:在 SMMU 已打开的路径上,vb2 提供的 DMA 地址是 SMMU 映射出来的 IOVA,而加密模块是独立于 SMMU 的,它需要的是物理地址。
这个问题的解决方式不是去拿物理地址,而是让采集驱动通过 DMABUF 方式把 buffer 共享给加密模块,让加密模块通过dma_buf_map_attachment()建立自己的映射。也就是说,多个硬件设备要共享同一块 buffer 时,应该走 dma-buf 的 attach/map 机制,而不是直接传 DMA 地址。DMA 地址是设备相关的,不是全局通用的一把钥匙。
5.4 多平面 buffer 的 plane 长度不一致
NV12 这类多平面格式在 videobuf2 里会拆成两个 plane:plane[0] 存 Y,plane[1] 存 UV。queue_setup()里如果只给 plane[0] 填了大小、漏掉 plane[1],或者把两个 plane 的大小填反,DMA 刚开始可能看不出问题,但一旦设备直接往 plane[1] 的 DMA 地址写数据,就可能越界访问。多用vb2_set_plane_payload()和vb2_plane_size()这类框架辅助函数来操作,不要自己维护一套 "长度"。
6. 调试与确认:让 DMA 映射问题无处藏身
6.1 打开内核的 DMA API 调试开关
写视频驱动,第一件事就是把内核相关调试选项打开。CONFIG_DMA_API_DEBUG是我最常用的开关,它能在dma_map/dma_unmap不匹配时直接给出告警,指出是哪个设备、哪个调用路径出了问题。很多看似随机崩溃的问题,都是在这里首先暴露的。
另外,CONFIG_DEBUG_FS打开后,可以配合dma_debug相关的 debugfs 节点查看当前 DMA 映射统计。虽然不同内核版本提供的节点名略有差异,但基本都能看到映射次数、泄漏提示。定位"内存泄漏"和"映射泄漏"时非常有用。
vb2 层面也有 debug 手段。加载驱动时video_debug或者v4l2_debug这类参数开大一点,能看到框架调用queue_setup、buf_prepare、start_streaming的完整轨迹。具体参数名不同内核版本略有区别,但思路是一致的:让框架本身多说话,比在驱动里到处插 printk 高效得多。
6.2 通过 dmesg 日志快速判断问题域
我总结了几个常见的日志信号和对应排查方向:
| dmesg 症状 | 常见原因 | 优先排查方向 |
|---|---|---|
dma_alloc_coherent失败 | 连续内存不足 / CMA 耗尽 | 查看 CMA 预留、内存碎片 |
dma_map_sg失败 | 映射段数过多 / IOMMU 配置错误 | 检查 IOMMU 驱动与 device tree |
| cache 相关花屏 | prepare/finish 未正确调用 | 检查mem_ops回调和同步范围 |
| vb2 报 buffer 状态错误 | 驱动在错误时机调用vb2_buffer_done() | 检查中断上下文与 lock 处理 |
6.3 实测验证的最后一公里
改完一处 DMA 内存管理逻辑,我的习惯是跑三类测试:
- 长时间稳定性测试:连续采集 8 小时以上,观察
REQBUFS是否偶尔失败,DMA 映射是否有泄漏。 - 高分辨率 + 多缓冲压力测试:把分辨率和缓冲区个数顶到规格书允许的上限,看内存分配和 IOMMU 映射是否稳定。
- 多设备共享测试:如果采集 buffer 要共享给显示、编码、算法模块,一定要跑 DMABUF 导入导出测试。这个场景最容易暴露"把 DMA 地址当作通用地址"的坏习惯。
我自己的体会是,videobuf2 这个框架本身挺成熟,大多数时候不需要你自己造轮子。真正让你头疼的,往往是你对 DMA 内存管理底层语义的假设出了问题。比如认为 DMA 地址等于物理地址,或者认为 cache 同步可以随便挑个时机做。把这些基础概念和框架的回调时机对齐,视频驱动的大半问题都不会发生。
现在再遇到 DMA 映射失败的问题,我基本不会先去怀疑硬件了,而是先问自己三个问题:我用的mem_ops适合当前 SoC 的 DMA 路径吗?prepare/finish的同步范围覆盖了完整的 plane 长度吗?多设备共享 buffer 时,我有没有真的走 dma-buf 机制而不是直接传地址?三问下来,问题基本能定位到八九不离十。