videobuf2与DMA内存管理
前言:
在嵌入式视频系统中,videobuf2和DMA是决定性能的两个核心组件。videobuf2负责视频缓冲区的生命周期管理,DMA负责数据的高效搬运。
include/media/videobuf2-core.hdrivers/media/common/videobuf2/videobuf2-core.cdrivers/media/common/videobuf2/videobuf2-dma-sg.cinclude/linux/dma-mapping.h
第一部分:软件设计模式分析
1.1 整体架构树形图
videobuf2框架采用分层设计模式,其核心是桥接模式和策略模式的结合:
┌─────────────────────────────────────────────────────────────────┐ │ 用户空间应用程序 │ │ (通过V4L2 ioctl与内核交互) │ └───────────────────────────────┬─────────────────────────────────┘ │ ▼ ┌─────────────────────────────────────────────────────────────────┐ │ V4L2设备节点层 │ │ (/dev/videoX, video_device) │ └───────────────────────────────┬─────────────────────────────────┘ │ ▼ ┌─────────────────────────────────────────────────────────────────┐ │ videobuf2核心框架[桥接模式] │ │ ┌───────────────────────────────────────────────────────────┐ │ │ │ struct vb2_queue (缓冲区队列管理器) │ │ │ │ - 管理整个缓冲区池的生命周期 │ │ │ │ - 实现生产者-消费者模式 │ │ │ └───────────────────────────────────────────────────────────┘ │ │ │ │ │ ┌───────────────┼───────────────┐ │ │ ▼ ▼ ▼ │ │ ┌────────────────────┐ ┌────────────────┐ ┌────────────────┐ │ │ │ struct vb2_ops │ │struct vb2_mem_ops│ │struct vb2_buffer│ │ │ │ [策略接口] │ │ [策略接口] │ │ [具体产品] │ │ │ │ - queue_setup │ │ - alloc │ │ - planes[] │ │ │ │ - buf_prepare │ │ - put │ │ - state │ │ │ │ - start_streaming │ │ - mmap │ │ - timestamp │ │ │ │ - stop_streaming │ │ - vaddr │ └────────────────┘ │ │ │ - buf_queue │ │ - dma_addr │ │ │ └────────────────────┘ └────────────────┘ │ └────────────────────────────────┬─────────────────────────────────┘ │ ┌────────────────┼────────────────┐ ▼ ▼ ▼ ┌───────────────────────┐ ┌───────────────┐ ┌───────────────┐ │ DMA Scatter-Gather │ │ DMA Contiguous │ │ vmalloc │ │ [具体策略实现] │ │ [具体策略实现] │ │ [具体策略实现] │ │ videobuf2-dma-sg.c │ │ dma-contig.c │ │ vmalloc.c │ │ - 支持不连续物理内存 │ │ - 物理地址连续 │ │ - 仅软件处理 │ │ - 需要硬件SG支持 │ │ - 性能最好 │ │ - 不支持DMA │ └───────────────────────┘ └───────────────┘ └───────────────┘
1.2 核心类关系图(UML风格)
从面向对象设计的视角,videobuf2的核心类关系如下:
┌─────────────────────────────────────────────────────────────────┐ │ <<interface>> │ │ vb2_ops │ │ ├───────────────────────────────────────────────────────────────┤│ │ │ +queue_setup(q, num_buffers, num_planes, sizes[]) : int ││ │ │ +buf_prepare(vb) : int ││ │ │ +buf_queue(vb) : void ││ │ │ +start_streaming(q, count) : int ││ │ │ +stop_streaming(q) : void ││ │ │ +wait_prepare(q) : void ││ │ │ +wait_finish(q) : void ││ │ └───────────────────────────────────────────────────────────────┘│ │ △ │ │ │ │ │ ┌───────┴───────┐ │ │ │ implements │ │ │ ▼ │ │ │ ┌─────────────────────────┐ │ │ │ │ camera_vb2_ops │ │ │ │ │ (具体驱动实现) │ │ │ │ └─────────────────────────┘ │ │ │ │ │ │ ┌─────────────────────────────────────┼─────────────────────────┐│ │ │ struct vb2_queue │ ││ │ │ ├────────────────────────────────────┼─────────────────────── ││ │ │ │ - type : enum v4l2_buf_type │ ││ │ │ │ - io_modes : unsigned int │ ││ │ │ │ - ops : const struct vb2_ops* │─────────┘ ││ │ │ │ - mem_ops : const struct vb2_mem_ops* ││ │ │ │ - drv_priv : void* │ ││ │ │ │ - bufs[VB2_MAX_FRAME] : struct vb2_buffer* ││ │ │ │ - num_buffers : unsigned int │ ││ │ │ │ - queued_list : struct list_head │ ││ │ │ │ - done_list : struct list_head │ ││ │ │ │ - streaming : unsigned int │ ││ │ │ └────────────────────────────────────┼─────────────────────── ││ │ └───────────────────────────────────────┼─────────────────────────┘│ │ │ │ │ ┌───────────────────────────────────────┼───────────────────────┐ │ │ │ struct vb2_buffer │ │ │ │ │ ├──────────────────────────────────────┼───────────────────── │ │ │ │ │ - vb2_queue : struct vb2_queue* │◄────────┘ │ │ │ │ │ - index : unsigned int │ │ │ │ │ │ - type : unsigned int │ │ │ │ │ │ - memory : unsigned int │ │ │ │ │ │ - state : enum vb2_buffer_state │ │ │ │ │ │ - planes[VB2_MAX_PLANES] : struct vb2_plane│ │ │ │ │ │ ├─ mem_priv : void* (内存分配器私有数据)│ │ │ │ │ │ ├─ bytesused : unsigned int │ │ │ │ │ │ ├─ length : unsigned int │ │ │ │ │ │ └─ m : union { offset, userptr, fd } │ │ │ │ │ └────────────────────────────────────────┼───────────────────── │ │ │ └──────────────────────────────────────────┼──────────────────────┘ │ │ │ │ │ ┌──────────────────────────────────────────┼───────────────────────┐ │ │ │ <<interface>> │ │ │ │ │ vb2_mem_ops │ │ │ │ │ ├──────────────────────────────────────────┼───────────────────── │ │ │ │ │ +alloc(alloc_ctx, size, dma_dir, gfp) : void* │ │ │ │ │ │ +put(buf_priv) : void │ │ │ │ │ │ +vaddr(buf_priv) : void* │ │ │ │ │ │ +dma_addr(buf_priv, plane) : dma_addr_t │ │ │ │ │ │ +mmap(buf_priv, vma) : int │ │ │ │ │ │ +prepare(buf_priv) : void │ │ │ │ │ │ +finish(buf_priv) : void │ │ │ │ │ └────────────────────────────────────────────┼───────────────────── │ │ │ └──────────────────────────────────────────────┼──────────────────────┘ │ │ │ │ │ ┌───────────────────────────┼───────────────────────┐ │ │ ▼ ▼ ▼ │ │ ┌─────────────────────────┐ ┌─────────────────────────┐ ┌───────────────┐│ │ │ vb2_dma_sg_memops │ │ vb2_dma_contig_memops │ │ vb2_vmalloc ││ │ │ (videobuf2-dma-sg.c) │ │ (videobuf2-dma-contig.c)│ │ (vmalloc.c) ││ │ │ - alloc: vb2_dma_sg_alloc│ │ - alloc: vb2_dc_alloc │ │ - alloc: vb2_ ││ │ │ - dma_addr: sg_dma_address│ │ - dma_addr: dma_addr │ │ vmalloc_alloc│ │ │ - 使用scatterlist │ │ - 使用dma_alloc_coherent│ │ - 不支持DMA ││ │ └─────────────────────────┘ └─────────────────────────┘ └───────────────┘│ └─────────────────────────────────────────────────────────────────────────────┘1.3 设计模式分析
| 设计模式 | 在videobuf2中的体现 | 源码位置 |
|---|---|---|
| 桥接模式 | 将抽象部分(缓冲区管理)与实现部分(内存分配)分离,使它们可以独立变化 | struct vb2_queue包含mem_ops指针 |
| 策略模式 | 不同的内存分配策略(DMA-SG、DMA-contig、vmalloc)实现相同的接口 | struct vb2_mem_ops及其具体实现 |
| 观察者模式 | 缓冲区状态变化通知驱动程序 | vb2_buffer_done()回调 |
| 工厂方法 | 通过mem_ops创建具体的内存对象 | call_memop(alloc) |
| 生产者-消费者 | 缓冲区队列的生产(硬件填充)和消费(应用程序出队) | queued_list和done_list |
第二部分:初始化流程全解析
2.1 初始化流程图
videobuf2的初始化流程分为驱动层初始化和应用层初始化两个阶段:
┌─────────────────────────────────────────────────────────────────┐ │ 驱动层初始化(probe函数中) │ └─────────────────────────────────────────────────────────────────┘ │ ▼ ┌───────────────────────────────┐ │ 1. 分配并初始化vb2_queue │ │ struct vb2_queue *q; │ │ q = v4l2_xxx_alloc(); │ └───────────────────────────────┘ │ ▼ ┌───────────────────────────────┐ │ 2. 设置vb2_queue参数 │ │ q->type = V4L2_BUF_TYPE_...│ │ q->io_modes = VB2_MMAP | ...│ │ q->ops = &driver_vb2_ops │ │ q->mem_ops = &vb2_dma_sg_...│ │ q->drv_priv = driver_data │ └───────────────────────────────┘ │ ▼ ┌───────────────────────────────┐ │ 3. 调用vb2_queue_init(q) │ │ 初始化锁、链表、等待队列 │ └───────────────────────────────┘ │ ▼ ┌───────────────────────────────┐ │ 4. 将vb2_queue关联到video_device│ │ vdev->queue = q; │ └───────────────────────────────┘ │ ▼ ┌─────────────────────────────────────────────────────────────────┐ │ 应用层初始化(open后ioctl调用) │ └─────────────────────────────────────────────────────────────────┘ │ ▼ ┌───────────────────────────────┐ │ 5. VIDIOC_REQBUFS │ │ → vb2_core_reqbufs() │ └───────────────────────────────┘ │ ▼ ┌─────────────────────┼─────────────────────┐ ▼ ▼ ▼ ┌───────────────┐ ┌───────────────┐ ┌───────────────┐ │ __vb2_queue_alloc│ │ call_memop(alloc) │ │ buf_init回调 │ │ 分配vb2_buffer数组│ │ 分配实际内存 │ │ 驱动初始化缓冲区│ └───────────────┘ └───────────────┘ └───────────────┘ │ ▼ ┌───────────────────────────────┐ │ 6. VIDIOC_QUERYBUF │ │ → vb2_core_querybuf() │ │ 获取缓冲区信息(偏移量等) │ └───────────────────────────────┘ │ ▼ ┌───────────────────────────────┐ │ 7. mmap() │ │ 将缓冲区映射到用户空间 │ │ → vb2_mmap() → mem_ops->mmap│ └───────────────────────────────┘
2.2 核心代码分析:vb2_queue_init
/** * @brief 初始化vb2_queue结构 * @param q 要初始化的vb2_queue指针 * @return 0成功,负值失败 * * @note 源码位置:drivers/media/common/videobuf2/videobuf2-core.c */ int vb2_queue_init(struct vb2_queue *q) { /* 1. 参数检查 */ if (!q || !q->ops || !q->mem_ops || !q->ops->queue_setup) return -EINVAL; /* 2. 验证必要的回调函数是否存在 */ if (!q->ops->buf_queue) { dprintk(1, "need buf_queue callback\n"); return -EINVAL; } /* 3. 初始化锁(如果驱动未提供,使用默认锁)*/ if (!q->lock) { /* 如果驱动没有提供锁,videobuf2会分配一个内部锁 */ q->lock = &q->mmap_lock; } /* 4. 初始化链表头 */ INIT_LIST_HEAD(&q->queued_list); INIT_LIST_HEAD(&q->done_list); /* 5. 初始化等待队列(用于poll/select)*/ init_waitqueue_head(&q->done_wq); /* 6. 初始化自旋锁(保护done_list)*/ spin_lock_init(&q->done_lock); /* 7. 初始化互斥锁(保护mmap操作)*/ mutex_init(&q->mmap_lock); /* 8. 设置初始状态 */ q->num_buffers = 0; q->queued_count = 0; q->streaming = 0; q->start_streaming_called = 0; return 0; } EXPORT_SYMBOL(vb2_queue_init);2.3 核心代码分析:vb2_core_reqbufs
/** * @brief 申请缓冲区核心函数 * @param q vb2_queue指针 * @param memory 内存类型(MMAP/USERPTR/DMABUF) * @param count 请求的缓冲区数量 * @return 实际分配的缓冲区数量 * * @note 源码位置:drivers/media/common/videobuf2/videobuf2-core.c */ int vb2_core_reqbufs(struct vb2_queue *q, enum vb2_memory memory, unsigned int *count) { unsigned int num_buffers, num_planes; unsigned int i; int ret; /* 1. 如果已经分配了缓冲区,先释放 */ if (q->num_buffers > 0) { /* 调用__vb2_queue_free释放所有缓冲区 */ ret = __vb2_queue_free(q, q->num_buffers); if (ret) return ret; } /* 2. 调用驱动的queue_setup回调,确定缓冲区参数 */ memset(q->alloc_ctx, 0, sizeof(q->alloc_ctx)); num_buffers = *count; num_planes = 0; ret = call_qop(q, queue_setup, q, &num_buffers, &num_planes, q->plane_sizes, q->alloc_ctx); if (ret) return ret; /* 3. 检查返回值是否合理 */ if (num_buffers > VB2_MAX_FRAME) num_buffers = VB2_MAX_FRAME; if (num_planes > VB2_MAX_PLANES) return -EINVAL; /* 4. 分配vb2_buffer结构数组 */ ret = __vb2_queue_alloc(q, memory, num_buffers, num_planes); if (ret < 0) return ret; /* 5. 返回实际分配的缓冲区数量 */ *count = q->num_buffers; /* 6. 保存内存类型 */ q->memory = memory; return 0; } EXPORT_SYMBOL_GPL(vb2_core_reqbufs);2.4 驱动实现示例:queue_setup回调
/** * @brief 驱动实现的queue_setup回调 * @param q vb2_queue指针 * @param num_buffers 缓冲区数量(可修改) * @param num_planes 平面数量(输出) * @param sizes 每个平面的大小数组(输出) * @param alloc_ctxs 分配上下文数组(输出) * @return 0成功,负值失败 * * @note 典型驱动实现模式 */ static int camera_queue_setup(struct vb2_queue *q, unsigned int *num_buffers, unsigned int *num_planes, unsigned int sizes[], struct device *alloc_ctxs[]) { struct camera_device *cam = vb2_get_drv_priv(q); /* 1. 设置平面数量(NV12格式需要2个平面)*/ *num_planes = 2; /* 2. 设置每个平面的大小 */ /* Y平面:宽度×高度 */ sizes[0] = cam->width * cam->height; /* UV平面:宽度×高度/2 */ sizes[1] = cam->width * cam->height / 2; /* 3. 确保至少有3个缓冲区 */ if (*num_buffers < 3) *num_buffers = 3; /* 4. 设置分配上下文(如果有)*/ if (alloc_ctxs[0]) alloc_ctxs[0] = cam->alloc_ctx; if (alloc_ctxs[1]) alloc_ctxs[1] = cam->alloc_ctx; dprintk(1, "%s: %u buffers, %u planes, sizes %u/%u\n", __func__, *num_buffers, *num_planes, sizes[0], sizes[1]); return 0; }第三部分:申请与回收流程
3.1 缓冲区状态机
videobuf2定义了完整的缓冲区状态机,每个缓冲区在生命周期中经历以下状态:
┌─────────────────────────────────┐ │ VB2_BUF_STATE_DEQUEUED │ │ (在用户空间控制下) │ └───────────────┬─────────────────┘ │ QBUF ▼ ┌─────────────────────────────────┐ │ VB2_BUF_STATE_PREPARING │ │ (准备中) │ └───────────────┬─────────────────┘ │ prepare完成 ▼ ┌─────────────────────────────────┐ │ VB2_BUF_STATE_PREPARED │ │ (已准备,等待入队) │ └───────────────┬─────────────────┘ │ buf_queue ▼ ┌─────────────────────────────────┐ │ VB2_BUF_STATE_QUEUED │ │ (已入队,等待硬件) │ └───────────────┬─────────────────┘ │ 提交给硬件 ▼ ┌─────────────────────────────────────────────────┐ │ VB2_BUF_STATE_ACTIVE │ │ (硬件正在使用,不可访问) │ └───┬─────────────────────────────┬───────────────┘ │ 硬件完成 │ 硬件出错 ▼ ▼ ┌───────────────────────┐ ┌───────────────────────┐ │ VB2_BUF_STATE_DONE │ │ VB2_BUF_STATE_ERROR │ │ (已完成,等待出队) │ │ (出错) │ └───────────┬───────────┘ └───────────┬───────────┘ │ DQBUF │ DQBUF └───────────────┬────────────────┘ ▼ ┌─────────────────────────┐ │ 返回VB2_BUF_STATE_DEQUEUED │ │ (循环完成) │ └─────────────────────────┘
3.2 缓冲区入队流程(QBUF)
/** * @brief 缓冲区入队核心函数 * @param q vb2_queue指针 * @param vb 要入队的缓冲区 * @return 0成功,负值失败 * * @note 源码位置:drivers/media/common/videobuf2/videobuf2-core.c */ int vb2_core_qbuf(struct vb2_queue *q, struct vb2_buffer *vb) { int ret; /* 1. 参数检查 */ if (!q || !vb) return -EINVAL; /* 2. 检查缓冲区状态(必须是DEQUEUED或PREPARED)*/ if (vb->state != VB2_BUF_STATE_DEQUEUED && vb->state != VB2_BUF_STATE_PREPARED) { dprintk(1, "invalid buffer state %d\n", vb->state); return -EINVAL; } /* 3. 如果尚未准备,调用prepare */ if (vb->state == VB2_BUF_STATE_DEQUEUED) { ret = __vb2_buf_prepare(vb); if (ret) return ret; } /* 4. 将缓冲区加入queued_list */ list_add_tail(&vb->queued_entry, &q->queued_list); q->queued_count++; vb->state = VB2_BUF_STATE_QUEUED; /* 5. 调用驱动的buf_queue回调(将缓冲区提交给硬件)*/ call_qop(q, buf_queue, vb); /* 6. 如果流尚未启动,可能需要启动 */ if (!q->start_streaming_called && q->queued_count >= q->min_buffers_needed) { ret = vb2_start_streaming(q); if (ret) { /* 启动失败,需要回滚 */ __vb2_queue_cancel(q); return ret; } } return 0; } EXPORT_SYMBOL_GPL(vb2_core_qbuf);3.3 缓冲区出队流程(DQBUF)
/** * @brief 缓冲区出队核心函数 * @param q vb2_queue指针 * @param vb 出队的缓冲区(输出) * @param nonblocking 是否非阻塞 * @return 0成功,负值失败 * * @note 源码位置:drivers/media/common/videobuf2/videobuf2-core.c */ int vb2_core_dqbuf(struct vb2_queue *q, struct vb2_buffer **vb, bool nonblocking) { int ret; unsigned long flags; /* 1. 检查队列状态 */ if (q->error) { dprintk(1, "fatal error occurred on queue\n"); return -EIO; } /* 2. 如果流已停止,没有缓冲区可出队 */ if (!q->streaming) { dprintk(1, "streaming off, no buffers to dequeue\n"); return -EINVAL; } /* 3. 等待缓冲区完成(阻塞或非阻塞)*/ retry: spin_lock_irqsave(&q->done_lock, flags); if (list_empty(&q->done_list)) { spin_unlock_irqrestore(&q->done_lock, flags); if (nonblocking) return -EAGAIN; /* 阻塞等待 */ ret = wait_event_interruptible(q->done_wq, !list_empty(&q->done_list)); if (ret < 0) return ret; goto retry; } /* 4. 从done_list取出第一个缓冲区 */ *vb = list_first_entry(&q->done_list, struct vb2_buffer, done_entry); list_del(&(*vb)->done_entry); spin_unlock_irqrestore(&q->done_lock, flags); /* 5. 调用驱动的buf_finish回调 */ ret = call_vb_qop(*vb, buf_finish, *vb); if (ret) return ret; /* 6. 更新缓冲区状态 */ (*vb)->state = VB2_BUF_STATE_DEQUEUED; return 0; } EXPORT_SYMBOL_GPL(vb2_core_dqbuf);3.4 硬件完成回调:vb2_buffer_done
当DMA传输完成,硬件产生中断,驱动在中断处理中调用此函数:
/** * @brief 通知videobuf2缓冲区已经处理完成 * @param vb 完成的缓冲区 * @param state 最终状态(VB2_BUF_STATE_DONE 或 VB2_BUF_STATE_ERROR) * * @note 通常在硬件中断处理程序中调用 */ void vb2_buffer_done(struct vb2_buffer *vb, enum vb2_buffer_state state) { struct vb2_queue *q = vb->vb2_queue; unsigned long flags; /* 1. 参数检查 */ if (WARN_ON(state != VB2_BUF_STATE_DONE && state != VB2_BUF_STATE_ERROR)) state = VB2_BUF_STATE_ERROR; /* 2. 调用驱动的buf_finish回调 */ call_vb_qop(vb, buf_finish, vb); /* 3. 更新缓冲区状态 */ vb->state = state; /* 4. 从queued_list移除 */ spin_lock_irqsave(&q->queued_list_lock, flags); list_del(&vb->queued_entry); q->queued_count--; spin_unlock_irqrestore(&q->queued_list_lock, flags); /* 5. 加入done_list */ spin_lock_irqsave(&q->done_lock, flags); list_add_tail(&vb->done_entry, &q->done_list); spin_unlock_irqrestore(&q->done_lock, flags); /* 6. 唤醒等待队列(通知应用程序)*/ wake_up(&q->done_wq); } EXPORT_SYMBOL_GPL(vb2_buffer_done);3.5 典型驱动中断处理示例
/** * @brief 摄像头DMA完成中断处理 * @param irq 中断号 * @param dev_id 设备私有数据 * @return IRQ处理状态 */ static irqreturn_t camera_irq_handler(int irq, void *dev_id) { struct camera_device *cam = dev_id; struct vb2_buffer *vb; u32 status; /* 1. 读取中断状态寄存器 */ status = readl(cam->regs + CAMERA_INT_STATUS); /* 2. 检查是否帧完成中断 */ if (status & CAMERA_INT_FRAME_DONE) { /* 清除中断 */ writel(CAMERA_INT_FRAME_DONE, cam->regs + CAMERA_INT_CLEAR); spin_lock(&cam->dma_lock); /* 3. 获取当前正在使用的缓冲区 */ vb = cam->active_buf; if (vb) { /* 4. 记录时间戳 */ vb->timestamp = ktime_get_ns(); vb->sequence = cam->frame_sequence++; /* 5. 设置实际数据大小(驱动需知道一帧数据有多大)*/ for (int i = 0; i < vb->num_planes; i++) { vb2_set_plane_payload(vb, i, cam->plane_sizes[i]); } /* 6. 通知videobuf2缓冲区完成 */ vb2_buffer_done(vb, VB2_BUF_STATE_DONE); /* 7. 提交下一个缓冲区(如果有)*/ if (!list_empty(&cam->vb2_vidq.queued_list)) { vb = list_first_entry(&cam->vb2_vidq.queued_list, struct vb2_buffer, queued_entry); camera_hw_submit_buffer(cam, vb); cam->active_buf = vb; } else { cam->active_buf = NULL; } } spin_unlock(&cam->dma_lock); return IRQ_HANDLED; } return IRQ_NONE; }第四部分:大数据流处理与DMA协同
4.1 完整数据流架构图
从摄像头硬件到用户空间应用程序的完整数据流:
┌─────────────────────────────────────────────────────────────────────────────┐ │ 硬件层 │ │ ┌────────────┐ ┌────────────┐ ┌────────────┐ ┌────────────┐ │ │ │ 摄像头Sensor │───▶│ CSI接口 │───▶│ DMA引擎 │───▶│ 内存控制器 │ │ │ │ (IMX185等) │ │ (MIPI) │ │ │ │ │ │ │ └────────────┘ └────────────┘ └────────────┘ └────────────┘ │ │ │ │ │ │ │ ▼ ▼ ▼ │ │ ┌──────────────────────────────────────┐ │ │ │ 系统内存(DMA缓冲区) │ │ │ │ ┌────────┐ ┌────────┐ ┌────────┐ │ │ │ │ │Buffer 1│ │Buffer 2│ │Buffer 3│...│ │ │ │ └────────┘ └────────┘ └────────┘ │ │ │ └──────────────────────────────────────┘ │ └─────────────────────────────────────────────────────────────────────────────┘ │ │ 内存映射(mmap) ▼ ┌─────────────────────────────────────────────────────────────────────────────┐ │ 内核层 │ │ ┌──────────────────────────────────────────────────────────────┐ │ │ │ videobuf2框架 │ │ │ │ ┌────────────┐ ┌────────────┐ ┌────────────┐ │ │ │ │ │ vb2_queue │───▶│vb2_buffer[]│───▶│ mem_ops │ │ │ │ │ └────────────┘ └────────────┘ └────────────┘ │ │ │ └──────────────────────────────────────────────────────────────┘ │ │ │ │ │ │ │ │ ┌──────────────────────────────────────────────────────────────┐ │ │ │ DMA API层 (dma-mapping.h) │ │ │ │ ┌────────────────────┐ ┌────────────────────┐ │ │ │ │ │dma_alloc_coherent │ │ dma_map_sg │ │ │ │ │ │dma_free_coherent │ │ dma_unmap_sg │ │ │ │ │ └────────────────────┘ └────────────────────┘ │ │ │ └──────────────────────────────────────────────────────────────┘ │ └─────────────────────────────────────────────────────────────────────────────┘ │ │ ▼ ┌─────────────────────────────────────────────────────────────────────────────┐ │ 用户层 │ │ ┌──────────────────────────────────────────────────────────────┐ │ │ │ 应用程序处理流程 │ │ │ │ │ │ │ │ 1. open("/dev/video0") │ │ │ │ 2. VIDIOC_REQBUFS → 分配4个缓冲区 │ │ │ │ 3. VIDIOC_QUERYBUF + mmap → 映射缓冲区到用户空间 │ │ │ │ 4. VIDIOC_QBUF → 将空缓冲区入队 │ │ │ │ 5. VIDIOC_STREAMON → 启动流 │ │ │ │ 6. while (1) { │ │ │ │ VIDIOC_DQBUF → 等待并获取完成的帧 │ │ │ │ process_frame(index); // 处理帧数据 │ │ │ │ VIDIOC_QBUF → 重新入队 │ │ │ │ } │ │ │ │ 7. VIDIOC_STREAMOFF → 停止流 │ │ │ │ 8. close() │ │ │ └──────────────────────────────────────────────────────────────┘ │ └─────────────────────────────────────────────────────────────────────────────┘4.2 DMA-SG内存分配器实现
/** * @brief DMA-SG缓冲区私有数据结构 * @note 源码位置:drivers/media/common/videobuf2/videobuf2-dma-sg.c */ struct vb2_dma_sg_buf { struct device *dev; /*!< DMA设备 */ void *vaddr; /*!< 内核虚拟地址 */ struct page **pages; /*!< 页面数组 */ int nr_pages; /*!< 页面数量 */ struct sg_table sg_table; /*!< 散列表(用于DMA)*/ size_t size; /*!< 总大小 */ atomic_t refcount; /*!< 引用计数 */ enum dma_data_direction dma_dir; /*!< DMA方向 */ bool mapped; /*!< 是否已映射 */ }; /** * @brief DMA-SG分配函数 */ static void *vb2_dma_sg_alloc(void *alloc_ctx, unsigned long size, enum dma_data_direction dma_dir, gfp_t gfp_flags) { struct vb2_dma_sg_buf *buf; struct sg_table *sgt; int ret; /* 1. 分配私有数据结构 */ buf = kzalloc(sizeof(*buf), GFP_KERNEL); if (!buf) return NULL; /* 2. 获取设备 */ buf->dev = vb2_dma_sg_get_device(alloc_ctx); buf->dma_dir = dma_dir; buf->size = size; /* 3. 计算所需页面数 */ buf->nr_pages = PAGE_ALIGN(size) / PAGE_SIZE; /* 4. 分配页面数组 */ buf->pages = kcalloc(buf->nr_pages, sizeof(struct page *), GFP_KERNEL); if (!buf->pages) goto fail_pages; /* 5. 分配物理页面 */ ret = alloc_pages_array(buf->pages, buf->nr_pages, gfp_flags); if (ret < 0) goto fail_alloc; /* 6. 创建散列表 */ sgt = &buf->sg_table; ret = sg_alloc_table_from_pages(sgt, buf->pages, buf->nr_pages, 0, size, GFP_KERNEL); if (ret) goto fail_sg; /* 7. 映射到内核虚拟地址 */ buf->vaddr = vmap(buf->pages, buf->nr_pages, VM_MAP, PAGE_KERNEL); if (!buf->vaddr) goto fail_vmap; atomic_set(&buf->refcount, 1); return buf; fail_vmap: sg_free_table(&buf->sg_table); fail_sg: free_pages_array(buf->pages, buf->nr_pages); fail_alloc: kfree(buf->pages); fail_pages: kfree(buf); return NULL; } /** * @brief 获取DMA地址 */ static dma_addr_t vb2_dma_sg_dma_addr(void *buf_priv, unsigned long plane) { struct vb2_dma_sg_buf *buf = buf_priv; struct sg_table *sgt = &buf->sg_table; /* 返回第一个sg entry的DMA地址 */ return sg_dma_address(sgt->sgl); }4.3 DMA-contig内存分配器
/** * @brief DMA-contig缓冲区私有数据结构 * @note 源码位置:drivers/media/common/videobuf2/videobuf2-dma-contig.c */ struct vb2_dc_buf { struct device *dev; /*!< DMA设备 */ void *vaddr; /*!< 内核虚拟地址 */ dma_addr_t dma_addr; /*!< DMA地址 */ unsigned long size; /*!< 大小 */ atomic_t refcount; /*!< 引用计数 */ void *cookie; /*!< dma_alloc_coherent返回的cookie */ }; /** * @brief DMA-contig分配函数 */ static void *vb2_dc_alloc(void *alloc_ctx, unsigned long size, enum dma_data_direction dma_dir, gfp_t gfp_flags) { struct vb2_dc_buf *buf; dma_addr_t dma_addr; void *vaddr; /* 1. 分配私有数据结构 */ buf = kzalloc(sizeof(*buf), GFP_KERNEL); if (!buf) return NULL; buf->dev = vb2_dc_get_device(alloc_ctx); /* 2. 使用dma_alloc_coherent分配物理和虚拟连续内存 */ vaddr = dma_alloc_coherent(buf->dev, size, &dma_addr, GFP_KERNEL); if (!vaddr) { dev_err(buf->dev, "dma_alloc_coherent of size %lu failed\n", size); kfree(buf); return NULL; } /* 3. 填充结构 */ buf->vaddr = vaddr; buf->dma_addr = dma_addr; buf->size = size; atomic_set(&buf->refcount, 1); return buf; }4.4 性能对比数据
根据实际硬件测试,三种内存模型的性能对比:
| 特性 | DMA-SG | vmalloc | DMA-contig |
|---|---|---|---|
| 物理地址连续性 | 不连续 | 不连续 | 连续 |
| 虚拟地址连续性 | 连续 | 连续 | 连续 |
| 支持DMA | 是(需硬件SG支持) | 否 | 是 |
| 内存碎片影响 | 小 | 小 | 大 |
| 分配速度 | 中等 | 快 | 慢(可能失败) |
| 适用硬件 | 支持SG-DMA的硬件 | 无DMA/软件处理 | 任何DMA硬件 |
| 典型帧率(1080p30) | 29-30fps | 5-10fps | 30fps稳定 |
4.5 DMA一致性处理
对于DMA传输,缓存一致性是关键问题:
/** * @brief 使DMA缓冲区与CPU缓存保持一致 * @param vaddr 虚拟地址 * @param size 大小 * @param direction DMA方向 * * @note 源码位置:arch/frv/mm/dma-alloc.c */ void consistent_sync(void *vaddr, size_t size, int direction) { unsigned long start = (unsigned long) vaddr; unsigned long end = start + size; switch (direction) { case DMA_NONE: BUG(); case DMA_FROM_DEVICE: /* 从设备到内存:只需使缓存失效 */ dma_cache_invalidate(start, end); break; case DMA_TO_DEVICE: /* 从内存到设备:需要写回缓存 */ dma_cache_writeback(start, end); break; case DMA_BIDIRECTIONAL: /* 双向:写回并使无效 */ dma_cache_writeback_invalidate(start, end); break; } } EXPORT_SYMBOL(consistent_sync);第五部分:模块化设计与扩展
5.1 内存操作函数表结构
/** * struct vb2_mem_ops - 内存操作函数表 * @brief 定义在 include/media/videobuf2-core.h * * 这是videobuf2框架最核心的抽象,允许不同的内存分配策略 */ struct vb2_mem_ops { /** 分配内存,返回分配器私有数据 */ void *(*alloc)(void *alloc_ctx, unsigned long size, enum dma_data_direction dma_dir, gfp_t gfp_flags); /** 释放内存 */ void (*put)(void *buf_priv); /** 获取DMA-BUF对象(用于DMABUF模式)*/ struct dma_buf *(*get_dmabuf)(void *buf_priv, unsigned long flags); /** USERPTR模式:获取用户空间指针 */ void *(*get_userptr)(void *alloc_ctx, unsigned long vaddr, unsigned long size, enum dma_data_direction dma_dir); /** USERPTR模式:释放 */ void (*put_userptr)(void *buf_priv); /** DMABUF模式:附加DMABUF */ void *(*attach_dmabuf)(void *alloc_ctx, struct dma_buf *dbuf, unsigned long size, enum dma_data_direction dma_dir); /** DMABUF模式:分离 */ void (*detach_dmabuf)(void *buf_priv); /** DMABUF模式:映射 */ int (*map_dmabuf)(void *buf_priv); /** DMABUF模式:取消映射 */ void (*unmap_dmabuf)(void *buf_priv); /** 缓存同步:准备(CPU->设备)*/ void (*prepare)(void *buf_priv); /** 缓存同步:完成(设备->CPU)*/ void (*finish)(void *buf_priv); /** 获取内核虚拟地址 */ void *(*vaddr)(void *buf_priv); /** 获取分配器cookie */ void *(*cookie)(void *buf_priv); /** 获取引用计数 */ unsigned int (*num_users)(void *buf_priv); /** mmap操作 */ int (*mmap)(void *buf_priv, struct vm_area_struct *vma); };5.2 三种内存操作表实例
/* DMA-SG内存操作表 */ const struct vb2_mem_ops vb2_dma_sg_memops = { .alloc = vb2_dma_sg_alloc, .put = vb2_dma_sg_put, .get_userptr = vb2_dma_sg_get_userptr, .put_userptr = vb2_dma_sg_put_userptr, .prepare = vb2_dma_sg_prepare, .finish = vb2_dma_sg_finish, .vaddr = vb2_dma_sg_vaddr, .cookie = vb2_dma_sg_cookie, .num_users = vb2_dma_sg_num_users, .mmap = vb2_dma_sg_mmap, }; EXPORT_SYMBOL_GPL(vb2_dma_sg_memops); /* DMA-contig内存操作表 */ const struct vb2_mem_ops vb2_dma_contig_memops = { .alloc = vb2_dc_alloc, .put = vb2_dc_put, .get_userptr = vb2_dc_get_userptr, .put_userptr = vb2_dc_put_userptr, .prepare = vb2_dc_prepare, .finish = vb2_dc_finish, .vaddr = vb2_dc_vaddr, .cookie = vb2_dc_cookie, .num_users = vb2_dc_num_users, .mmap = vb2_dc_mmap, }; EXPORT_SYMBOL_GPL(vb2_dma_contig_memops); /* vmalloc内存操作表 */ const struct vb2_mem_ops vb2_vmalloc_memops = { .alloc = vb2_vmalloc_alloc, .put = vb2_vmalloc_put, .get_userptr = vb2_vmalloc_get_userptr, .put_userptr = vb2_vmalloc_put_userptr, .prepare = vb2_vmalloc_prepare, .finish = vb2_vmalloc_finish, .vaddr = vb2_vmalloc_vaddr, .cookie = vb2_vmalloc_cookie, .num_users = vb2_vmalloc_num_users, .mmap = vb2_vmalloc_mmap, }; EXPORT_SYMBOL_GPL(vb2_vmalloc_memops);总结:设计思想与选择建议
设计思想总结
分层抽象:videobuf2将缓冲区管理与内存分配分离,通过
vb2_mem_ops接口实现策略模式状态机驱动:通过严格的缓冲区状态机,确保数据流的有序性
零拷贝设计:通过mmap实现用户空间与内核空间共享缓冲区,避免数据拷贝
DMA集成:通过DMA API与硬件协同,实现高效数据传输
内存模型选择建议
根据实际应用场景:
| 场景 | 推荐模型 | 理由 |
|---|---|---|
| 高性能嵌入式设备(如行车记录仪) | DMA-contig | 性能最好,帧率稳定 |
| 内存受限系统(如IPC摄像头) | DMA-SG | 避免内存碎片,分配成功率高 |
| 纯软件处理(如视频格式转换) | vmalloc | 实现简单,无需DMA支持 |
| 用户空间分配缓冲区 | DMA-SG | 适合zero-copy架构 |
| 硬件不支持SG-DMA | DMA-contig | 唯一选择 |
性能优化建议
缓冲区数量:至少3个(生产者-消费者模型),建议4-6个
预分配:在系统启动时预分配DMA缓冲区(使用CMA)
缓存一致性:根据硬件能力选择
dma-coherent设备树属性批量处理:对于大数据流,考虑使用DMA引擎的链表模式
参考资料:
Linux内核源码:
drivers/media/v4l2-core/Linux内核文档:
Documentation/driver-api/media/v4l2-videobuf2.rstCMA文档:
Documentation/devicetree/bindings/reserved-memory/reserved-memory.txt
