V4L2与零拷贝:从视频采集到DMA-BUF跨设备共享的全链路实。
摄像头采集一帧图像,传统做法要经过两次CPU拷贝才能显示到屏幕上——V4L2采集到内核缓冲区,CPU搬到用户空间,再CPU搬到GPU/DRM。080p@60fps下,每秒光拷贝就吃掉几百MB/s带宽。DMA-BUF就是来解决这个问题的:让V4L2、GPU、DRM共享同一块物理内存,零拷贝走完全程,基于Linux 6.8内核,从V4L2框架讲起,过videobuf2缓冲区管理,最后把DMA-BUF零拷贝机制和实战代码串起来。
一、V4L2框架概述
V4L2(Video for Linux 2)是Linux内核的视频设备标准接口,摄像头、TV采集卡、视频输出设备、SDR全归它管。用户空间通过/dev/videoX设备节点和标准ioctl跟驱动交互,不用管底层硬件差异。
架构分层
支持的设备类。
| 类型 | V4L2设备类型 | 设备节点 | 典型硬件 |
|---|---|---|---|
| 视频捕获 | V4L2_CAP_VIDEO_CAPTURE |
/dev/videoX |
USB摄像头、MIPI CSI Sensor |
| 视频输出 | V4L2_CAP_VIDEO_OUTPUT |
/dev/videoX |
HDMI输出、LCD显示 |
| VBI数据 | V4L2_CAP_VBI_CAPTURE |
/dev/vbiX |
TV场消隐期数据 |
| SDR | V4L2_CAP_SDR_CAPTURE |
/dev/swradioX |
软件定义无线电 |
| 元数据 | V4L2_CAP_META_CAPTURE |
/dev/videoX |
Sensor内嵌元数据 |
日常开发打交道最多的就是视频捕获,后面内容也以捕获为主。
二、V4L2核心数据结构
V4L2驱动的骨架就四个结构体:v4l2_device管设备实例,video_device管设备节点,v4l2_subdev管子设备,v4l2_ioctl_ops管ioctl回调试
v4l2_device:设备实。
// include/media/v4l2-device.h
struct v4l2_device {
struct device *dev; // 指向底层device
struct list_head subdevs; // 子设备链。 struct mutex lock; // 全局互斥。 char name[V4L2_DEVICE_NAME_SIZE]; // 设备。 struct v4l2_prio_state prio; // 优先级状态 struct kref ref; // 引用计数
void (*release)(struct v4l2_device *v4l2_dev); // 释放回调
};
v4l2_device是整个V4L2驱动的根,通常嵌入在驱动的私有结构体里,注册用v4l2_device_register,注销用v4l2_device_unregister。
v4l2_subdev:子设备
// include/media/v4l2-subdev.h
struct v4l2_subdev {
struct v4l2_device *v4l2_dev; // 指向父设。 const struct v4l2_subdev_ops *ops; // 子设备操作集
struct list_head list; // 挂在v4l2_device.subdevs链表。 u32 flags;
struct v4l2_subdev_platform_data *pdata;
};
子设备代表Sensor、ISP、Tuner等独立功能模块。Sensor驱动注册为subdev,ISP驱动也注册为subdev,V4L2核心层通过v4l2_device统一管理它们之间的连接关系。
video_device:注册到/dev/videoX
// include/media/v4l2-dev.h(关键字段)
struct video_device {
const struct v4l2_file_operations *fops; // 文件操作
const struct v4l2_ioctl_ops *ioctl_ops; // ioctl操作
struct v4l2_device *v4l2_dev; // 指向父设。 struct device dev; // 内嵌device
enum vfl_devnode_type vfl_type; // 设备类型
enum vfl_devnode_direction vfl_dir; // 输入/输出方向
int minor; // 次设备号
struct vb2_queue *queue; // 绑定的vb2队列
u32 device_caps; // 设备能力位图
};
video_device是用户空间能看到的实体——注册后会在/dev/下创建videoX节点。.8内核里video_device内嵌了device,完整融入设备模型。
v4l2_file_operations与v4l2_ioctl_ops
// 文件操作,大部分直接用V4L2提供的默认实。struct v4l2_file_operations {
struct module *owner;
int (*open)(struct video_device *vdev);
int (*release)(struct video_device *vdev);
ssize_t (*read)(struct video_device *vdev, char __user *buf,
size_t count, loff_t *ppos);
ssize_t (*write)(struct video_device *vdev, const char __user *buf,
size_t count, loff_t *ppos);
__poll_t (*poll)(struct file *file, struct poll_table_struct *wait);
long (*ioctl)(struct video_device *vdev, unsigned int cmd, void *arg);
int (*mmap)(struct video_device *vdev, struct vm_area_struct *vma);
};
// ioctl操作,驱动必须实现的核心回调
struct v4l2_ioctl_ops {
int (*vidioc_querycap)(struct file *file, void *fh,
struct v4l2_capability *cap);
int (*vidioc_enum_fmt_vid_cap)(struct file *file, void *fh,
struct v4l2_fmtdesc *f);
int (*vidioc_g_fmt_vid_cap)(struct file *file, void *fh,
struct v4l2_format *fmt);
int (*vidioc_s_fmt_vid_cap)(struct file *file, void *fh,
struct v4l2_format *fmt);
int (*vidioc_reqbufs)(struct file *file, void *fh,
struct v4l2_requestbuffers *b);
int (*vidioc_qbuf)(struct file *file, void *fh,
struct v4l2_buffer *b);
int (*vidioc_dqbuf)(struct file *file, void *fh,
struct v4l2_buffer *b);
int (*vidioc_streamon)(struct file *file, void *fh,
enum v4l2_buf_type type);
int (*vidioc_streamoff)(struct file *file, void *fh,
enum v4l2_buf_type type);
// ... 更多回调
};
v4l2_file_operations大部分可以直接用V4L2核心提供的v4l2_fops默认实现,驱动只需要关注open和release。真正需要驱动实现的是v4l2_ioctl_ops里的回调试
三、V4L2 ioctl操作流程
V4L2采集有一套固定的ioctl调用顺序,跳步或乱序都会报错。完整流程如下:
各步骤对应的ioctl和驱动回调
| 步骤 | 用户空间ioctl | 驱动回调 | 说明 |
|---|---|---|---|
| 查询能力 | VIDIOC_QUERYCAP |
vidioc_querycap |
返回设备能力位图,必须第一步调用 |
| 枚举格式 | VIDIOC_ENUM_FMT |
vidioc_enum_fmt_vid_cap |
遍历硬件支持的像素格式 |
| 设置格式 | VIDIOC_S_FMT |
vidioc_s_fmt_vid_cap |
设定分辨率、像素格式、场 |
| 申请缓冲区 | VIDIOC_REQBUFS |
vidioc_reqbufs |
指定缓冲区数量和内存类型 |
| 映射缓冲区 | mmap |
vb2_mmap |
把内核缓冲区映射到用户空间 |
| 缓冲区入队 | VIDIOC_QBUF |
vidioc_qbuf |
把空闲缓冲区交给驱动填充 |
| 启动采集 | VIDIOC_STREAMON |
vidioc_streamon |
通知驱动开始DMA传输 |
| 缓冲区出队 | VIDIOC_DQBUF |
vidioc_dqbuf |
取回已填充数据的缓冲区 |
| 停止采集 | VIDIOC_STREAMOFF |
vidioc_streamoff |
停止DMA,所有缓冲区回到用户 |
关键点:REQBUFS必须在QBUF之前,STREAMON必须在所有初始缓冲区入队之后。顺序搞反,ioctl直接返回EINVAL。
四、videobuf2缓冲区管理
videobuf2(简称vb2)是V4L2的缓冲区管理框架,缓冲区分配、映射、入队出队、DMA对接全由它管。驱动不直接操作缓冲区内存,全部走vb2接口。
vb2_queue:缓冲区队列
// include/media/videobuf2-core.h(关键字段)
struct vb2_queue {
enum v4l2_buf_type type; // 缓冲区类。V4L2_BUF_TYPE_VIDEO_CAPTURE)
enum vb2_memory memory; // 内存类型(VB2_MEMORY_MMAP/DMABUF/USERPTR)
struct mutex *lock; // 队列。 const struct vb2_ops *ops; // 驱动回调
const struct vb2_mem_ops *mem_ops;// 内存操作
u32 min_buffers_needed; // 最少需要的缓冲区数
u32 buf_struct_size; // 驱动私有缓冲区结构大。 struct vb2_buffer *bufs[VB2_MAX_FRAME]; // 缓冲区数。 unsigned int num_buffers; // 已分配缓冲区。};
vb2_ops:驱动回调
// 驱动需要实现的vb2操作
struct vb2_ops {
int (*queue_setup)(struct vb2_queue *q,
unsigned int *num_buffers,
unsigned int *num_planes,
unsigned int sizes[],
struct device *alloc_devs[]);
void (*buf_queue)(struct vb2_buffer *vb); // 缓冲区入队时调用
int (*buf_prepare)(struct vb2_buffer *vb); // 缓冲区准备(可选)
void (*buf_finish)(struct vb2_buffer *vb); // 缓冲区完成(可选)
int (*start_streaming)(struct vb2_queue *q, unsigned int count);
void (*stop_streaming)(struct vb2_queue *q);
void (*buf_cleanup)(struct vb2_buffer *vb); // 缓冲区清理(可选)
};
queue_setup在REQBUFS时被调用,驱动可以调整缓冲区数量和大小。buf_queue在每次QBUF时被调用,驱动在这里把缓冲区交给硬件DMA引擎。start_streaming/stop_streaming对应STREAMON/STREAMOFF。
vb2_mem_ops:内存操。
// 三种内存操作集,驱动选择一。struct vb2_mem_ops {
void *(*alloc)(struct vb2_buffer *vb, struct device *dev,
unsigned long size);
void (*put)(void *buf_priv);
void *(*get_userptr)(struct vb2_buffer *vb, struct device *dev,
unsigned long vaddr, unsigned long size);
void (*put_userptr)(void *buf_priv);
struct dma_buf *(*get_dmabuf)(void *buf_priv, unsigned long flags);
int (*mmap)(void *buf_priv, struct vm_area_struct *vma);
// ...
};
三种内存模式
| 模式 | mem_ops | 分配方式 | 适用场景 |
|---|---|---|---|
| VMALLOC | vb2_vmalloc_memops |
vmalloc分配虚拟连续内存 |
调试、USB摄像头 |
| DMA-contiguous | vb2_dma_contig_memops |
dma_alloc_coherent分配物理连续内存 |
嵌入式平台ISP/DMA引擎 |
| DMA-scatter/gather | vb2_dma_sg_memops |
dma_map_sg映射分散页面 |
高端摄像头、大分辨率 |
嵌入式平台最常用DMA-contiguous模式,因为很多SoC的DMA引擎要求物理地址连续。dma_alloc_coherent分配的内存物理连续且cache一致,硬件DMA直接拿返回的DMA地址搬数据就行。
DMA-contiguous模式的关键代码:
// 初始化vb2_queue时指定DMA-contiguous
q->mem_ops = vb2_dma_contig_memops;
// vb2内部通过dma_alloc_coherent分配
// drivers/media/common/videobuf2/videobuf2-dma-contig.c
static void *vb2_dc_alloc(struct vb2_buffer *vb, struct device *dev,
unsigned long size)
{
struct vb2_dc_buf *buf;
dma_addr_t dma_addr;
buf = kzalloc(sizeof(*buf), GFP_KERNEL);
// 分配物理连续、cache一致的DMA缓冲。 buf->vaddr = dma_alloc_coherent(dev, size, &dma_addr,
GFP_KERNEL | vb->dma_attrs);
buf->dma_addr = dma_addr;
buf->size = size;
buf->dev = dev;
return buf;
}
五、简单V4L2驱动示例
下面写一个虚拟摄像头驱动,基于vb2框架,生成彩色测试图案。代码结构参考内核的vivid驱动,但大幅简化,只保留核心逻辑。
// v4l2_dummy_cam.c - 虚拟摄像头驱动(Linux 6.8。#include <linux/module.h>
#include <linux/platform_device.h>
#include <media/v4l2-device.h>
#include <media/v4l2-ioctl.h>
#include <media/v4l2-fh.h>
#include <media/videobuf2-v4l2.h>
#include <media/videobuf2-vmalloc.h>
#define DUMMY_CAM_WIDTH 640
#define DUMMY_CAM_HEIGHT 480
#define DUMMY_CAM_PIXFMT V4L2_PIX_FMT_YUYV
#define DUMMY_CAM_NBUFS 4
struct dummy_cam_dev {
struct v4l2_device v4l2_dev;
struct video_device vdev;
struct vb2_queue queue;
struct mutex lock;
/* 当前帧计数,用于生成测试图案 */
u32 frame_count;
};
/* ---- vb2_ops ---- */
static int dummy_cam_queue_setup(struct vb2_queue *q,
unsigned int *nbuffers,
unsigned int *nplanes,
unsigned int sizes[],
struct device *alloc_devs[])
{
/* YUYV格式,每像素2字节 */
sizes[0] = DUMMY_CAM_WIDTH * DUMMY_CAM_HEIGHT * 2;
*nplanes = 1;
return 0;
}
static int dummy_cam_buf_prepare(struct vb2_buffer *vb)
{
struct vb2_v4l2_buffer *vbuf = to_vb2_v4l2_buffer(vb);
unsigned long size = DUMMY_CAM_WIDTH * DUMMY_CAM_HEIGHT * 2;
if (vb2_plane_size(vb, 0) < size)
return -EINVAL;
vb2_set_plane_payload(vb, 0, size);
vbuf->field = V4L2_FIELD_NONE;
return 0;
}
static void dummy_cam_buf_queue(struct vb2_buffer *vb)
{
struct dummy_cam_dev *dev = vb2_get_drv_priv(vb->vb2_queue);
struct vb2_v4l2_buffer *vbuf = to_vb2_v4l2_buffer(vb);
/* 模拟硬件填充:生成YUYV测试图案 */
void *vaddr = vb2_plane_vaddr(vb, 0);
if (vaddr) {
u8 *p = vaddr;
u32 frame = dev->frame_count++;
/* 简单渐变色图案,Y分量随帧号变更*/
memset(p, (frame * 3) & 0xFF,
DUMMY_CAM_WIDTH * DUMMY_CAM_HEIGHT * 2);
}
/* 立即完成,模拟硬件中断回调*/
vbuf->sequence = dev->frame_count;
vbuf->vb2_buf.timestamp = ktime_get_ns();
vb2_buffer_done(&vbuf->vb2_buf, VB2_BUF_STATE_DONE);
}
static int dummy_cam_start_streaming(struct vb2_queue *q, unsigned int count)
{
struct dummy_cam_dev *dev = vb2_get_drv_priv(q);
dev->frame_count = 0;
return 0;
}
static void dummy_cam_stop_streaming(struct vb2_queue *q)
{
struct dummy_cam_dev *dev = vb2_get_drv_priv(q);
/* 清除所有未完成的缓冲区 */
struct vb2_buffer *vb;
while ((vb = vb2_done_list_del(q)))
vb2_buffer_done(vb, VB2_BUF_STATE_ERROR);
}
static const struct vb2_ops dummy_cam_vb2_ops = {
.queue_setup = dummy_cam_queue_setup,
.buf_prepare = dummy_cam_buf_prepare,
.buf_queue = dummy_cam_buf_queue,
.start_streaming = dummy_cam_start_streaming,
.stop_streaming = dummy_cam_stop_streaming,
};
/* ---- v4l2_ioctl_ops ---- */
static int dummy_cam_querycap(struct file *file, void *fh,
struct v4l2_capability *cap)
{
strscpy(cap->driver, "dummy_cam", sizeof(cap->driver));
strscpy(cap->card, "Dummy Camera", sizeof(cap->card));
strscpy(cap->bus_info, "platform:dummy_cam", sizeof(cap->bus_info));
cap->device_caps = V4L2_CAP_VIDEO_CAPTURE | V4L2_CAP_STREAMING;
cap->capabilities = cap->device_caps | V4L2_CAP_DEVICE_CAPS;
return 0;
}
static int dummy_cam_enum_fmt(struct file *file, void *fh,
struct v4l2_fmtdesc *f)
{
if (f->index > 0)
return -EINVAL;
f->pixelformat = DUMMY_CAM_PIXFMT;
return 0;
}
static int dummy_cam_g_fmt(struct file *file, void *fh,
struct v4l2_format *fmt)
{
fmt->fmt.pix.width = DUMMY_CAM_WIDTH;
fmt->fmt.pix.height = DUMMY_CAM_HEIGHT;
fmt->fmt.pix.pixelformat = DUMMY_CAM_PIXFMT;
fmt->fmt.pix.field = V4L2_FIELD_NONE;
fmt->fmt.pix.bytesperline = DUMMY_CAM_WIDTH * 2;
fmt->fmt.pix.sizeimage = DUMMY_CAM_WIDTH * DUMMY_CAM_HEIGHT * 2;
fmt->fmt.pix.colorspace = V4L2_COLORSPACE_SRGB;
return 0;
}
static int dummy_cam_s_fmt(struct file *file, void *fh,
struct v4l2_format *fmt)
{
/* 虚拟设备只支持一种格式,直接返回固定*/
return dummy_cam_g_fmt(file, fh, fmt);
}
static const struct v4l2_ioctl_ops dummy_cam_ioctl_ops = {
.vidioc_querycap = dummy_cam_querycap,
.vidioc_enum_fmt_vid_cap = dummy_cam_enum_fmt,
.vidioc_g_fmt_vid_cap = dummy_cam_g_fmt,
.vidioc_s_fmt_vid_cap = dummy_cam_s_fmt,
.vidioc_reqbufs = vb2_ioctl_reqbufs,
.vidioc_create_bufs = vb2_ioctl_create_bufs,
.vidioc_qbuf = vb2_ioctl_qbuf,
.vidioc_dqbuf = vb2_ioctl_dqbuf,
.vidioc_streamon = vb2_ioctl_streamon,
.vidioc_streamoff = vb2_ioctl_streamoff,
.vidioc_expbuf = vb2_ioctl_expbuf,
.vidioc_mmap = vb2_ioctl_mmap,
};
/* ---- v4l2_file_operations ---- */
static int dummy_cam_open(struct file *file)
{
struct dummy_cam_dev *dev = video_drvdata(file);
return v4l2_fh_open(file);
}
static int dummy_cam_release(struct file *file)
{
return vb2_fop_release(file);
}
static const struct v4l2_file_operations dummy_cam_fops = {
.owner = THIS_MODULE,
.open = dummy_cam_open,
.release = dummy_cam_release,
.read = vb2_fop_read,
.poll = vb2_fop_poll,
.mmap = vb2_fop_mmap,
.unlocked_ioctl = video_ioctl2,
};
/* ---- probe / remove ---- */
static int dummy_cam_probe(struct platform_device *pdev)
{
struct dummy_cam_dev *dev;
int ret;
dev = devm_kzalloc(&pdev->dev, sizeof(*dev), GFP_KERNEL);
if (!dev)
return -ENOMEM;
mutex_init(&dev->lock);
/* 注册v4l2_device */
ret = v4l2_device_register(&pdev->dev, &dev->v4l2_dev);
if (ret)
return ret;
/* 初始化vb2_queue */
dev->queue.type = V4L2_BUF_TYPE_VIDEO_CAPTURE;
dev->queue.io_modes = VB2_MMAP | VB2_DMABUF | VB2_READ;
dev->queue.dev = &pdev->dev;
dev->queue.ops = &dummy_cam_vb2_ops;
dev->queue.mem_ops = &vb2_vmalloc_memops;
dev->queue.buf_struct_size = sizeof(struct vb2_v4l2_buffer);
dev->queue.lock = &dev->lock;
dev->queue.min_buffers_needed = 1;
ret = vb2_queue_init(&dev->queue);
if (ret)
goto err_v4l2_unregister;
/* 初始化video_device */
dev->vdev = video_device_alloc();
if (!dev->vdev) {
ret = -ENOMEM;
goto err_v4l2_unregister;
}
dev->vdev->v4l2_dev = &dev->v4l2_dev;
dev->vdev->fops = &dummy_cam_fops;
dev->vdev->ioctl_ops = &dummy_cam_ioctl_ops;
dev->vdev->queue = &dev->queue;
dev->vdev->lock = &dev->lock;
dev->vdev->device_caps = V4L2_CAP_VIDEO_CAPTURE | V4L2_CAP_STREAMING;
video_set_drvdata(dev->vdev, dev);
ret = video_register_device(dev->vdev, VFL_TYPE_VIDEO, -1);
if (ret)
goto err_vdev_release;
platform_set_drvdata(pdev, dev);
dev_info(&pdev->dev, "dummy camera registered as /dev/video%d\n",
dev->vdev->num);
return 0;
err_vdev_release:
video_device_release(dev->vdev);
err_v4l2_unregister:
v4l2_device_unregister(&dev->v4l2_dev);
return ret;
}
static void dummy_cam_remove(struct platform_device *pdev)
{
struct dummy_cam_dev *dev = platform_get_drvdata(pdev);
video_unregister_device(dev->vdev);
v4l2_device_unregister(&dev->v4l2_dev);
mutex_destroy(&dev->lock);
}
static struct platform_driver dummy_cam_driver = {
.probe = dummy_cam_probe,
.remove = dummy_cam_remove,
.driver = {
.name = "dummy_cam",
},
};
module_platform_driver(dummy_cam_driver);
MODULE_LICENSE("GPL");
MODULE_DESCRIPTION("Dummy V4L2 camera driver for testing");
几点说明。
- buf_queue里直接调用vb2_buffer_done完成缓冲区,真实驱动是在中断处理函数里调试- vidioc_reqbufs/qbuf/dqbuf/streamon/streamoff/mmap这些直接用vb2提供的vb2_ioctl_*默认实现,不需要驱动重启- 内存操作用了vb2_vmalloc_memops,真实嵌入式平台换成vb2_dma_contig_memops
六、用户空间V4L2应用
V4L2采集程序
// v4l2_capture.c - 用户空间V4L2采集示例
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <fcntl.h>
#include <unistd.h>
#include <sys/ioctl.h>
#include <sys/mman.h>
#include <errno.h>
#include <linux/videodev2.h>
#define NBUFS 4
struct buffer {
void *start;
size_t length;
};
int main(int argc, char **argv)
{
const char *dev_name = "/dev/video0";
int fd = -1;
struct buffer buffers[NBUFS];
int i;
/* 1. 打开设备 */
fd = open(dev_name, O_RDWR | O_NONBLOCK);
if (fd < 0) {
perror("open");
return -1;
}
/* 2. 查询设备能力 */
struct v4l2_capability cap;
if (ioctl(fd, VIDIOC_QUERYCAP, &cap) < 0) {
perror("VIDIOC_QUERYCAP");
goto out;
}
if (!(cap.capabilities & V4L2_CAP_VIDEO_CAPTURE)) {
fprintf(stderr, "不是视频捕获设备\n");
goto out;
}
/* 3. 设置格式 */
struct v4l2_format fmt;
memset(&fmt, 0, sizeof(fmt));
fmt.type = V4L2_BUF_TYPE_VIDEO_CAPTURE;
fmt.fmt.pix.width = 640;
fmt.fmt.pix.height = 480;
fmt.fmt.pix.pixelformat = V4L2_PIX_FMT_YUYV;
fmt.fmt.pix.field = V4L2_FIELD_NONE;
if (ioctl(fd, VIDIOC_S_FMT, &fmt) < 0) {
perror("VIDIOC_S_FMT");
goto out;
}
printf("实际格式: %dx%d, pixfmt=0x%08x\n",
fmt.fmt.pix.width, fmt.fmt.pix.height,
fmt.fmt.pix.pixelformat);
/* 4. 申请缓冲*/
struct v4l2_requestbuffers req;
memset(&req, 0, sizeof(req));
req.count = NBUFS;
req.type = V4L2_BUF_TYPE_VIDEO_CAPTURE;
req.memory = V4L2_MEMORY_MMAP;
if (ioctl(fd, VIDIOC_REQBUFS, &req) < 0) {
perror("VIDIOC_REQBUFS");
goto out;
}
/* 5. mmap映射 + QBUF入队 */
for (i = 0; i < NBUFS; i++) {
struct v4l2_buffer buf;
memset(&buf, 0, sizeof(buf));
buf.type = V4L2_BUF_TYPE_VIDEO_CAPTURE;
buf.memory = V4L2_MEMORY_MMAP;
buf.index = i;
if (ioctl(fd, VIDIOC_QUERYBUF, &buf) < 0) {
perror("VIDIOC_QUERYBUF");
goto out;
}
buffers[i].length = buf.length;
buffers[i].start = mmap(NULL, buf.length,
PROT_READ | PROT_WRITE,
MAP_SHARED, fd, buf.m.offset);
if (buffers[i].start == MAP_FAILED) {
perror("mmap");
goto out;
}
/* 入队 */
if (ioctl(fd, VIDIOC_QBUF, &buf) < 0) {
perror("VIDIOC_QBUF");
goto out;
}
}
/* 6. 启动采集 */
enum v4l2_buf_type type = V4L2_BUF_TYPE_VIDEO_CAPTURE;
if (ioctl(fd, VIDIOC_STREAMON, &type) < 0) {
perror("VIDIOC_STREAMON");
goto out;
}
/* 7. 采集循环 */
int frame_count = 0;
while (frame_count < 30) {
struct v4l2_buffer buf;
memset(&buf, 0, sizeof(buf));
buf.type = V4L2_BUF_TYPE_VIDEO_CAPTURE;
buf.memory = V4L2_MEMORY_MMAP;
/* 出队(阻塞等待) */
if (ioctl(fd, VIDIOC_DQBUF, &buf) < 0) {
if (errno == EAGAIN)
continue;
perror("VIDIOC_DQBUF");
break;
}
printf("。%d: index=%d, bytesused=%u, seq=%u\n",
frame_count, buf.index, buf.bytesused, buf.sequence);
/* 此处可处理buffers[buf.index].start中的图像数据 */
/* 重新入队 */
if (ioctl(fd, VIDIOC_QBUF, &buf) < 0) {
perror("VIDIOC_QBUF");
break;
}
frame_count++;
}
/* 8. 停止采集 */
ioctl(fd, VIDIOC_STREAMOFF, &type);
out:
/* 9. 清理 */
for (i = 0; i < NBUFS; i++) {
if (buffers[i].start && buffers[i].start != MAP_FAILED)
munmap(buffers[i].start, buffers[i].length);
}
if (fd >= 0)
close(fd);
return 0;
}
v4l2-ctl命令行工。
调试V4L2设备最方便的工具是v4l2-ctl,来自v4l-utils包:
# 查看设备信息
v4l2-ctl --device=/dev/video0 --all
# 列出支持的格。v4l2-ctl --device=/dev/video0 --list-formats-ext
# 设置格式并采集一。v4l2-ctl --device=/dev/video0 \
--set-fmt-video=width=640,height=480,pixelformat=YUYV \
--set-parm=30 \
--stream-mmap --stream-to=frame.yuyv --stream-count=1
# 连续采集10。v4l2-ctl --device=/dev/video0 \
--stream-mmap --stream-to=video.yuyv --stream-count=10
七、DMA-BUF零拷贝机。
传统拷贝的瓶。
V4L2采集到显示的传统数据流:
V4L2采集 。[内核DMA缓冲区] 。CPU拷贝 。[用户空间缓冲区] 。CPU拷贝 。[GPU缓冲区] 。渲染显示
1080p@60fps的YUYV图像,每帧约2MB,每。20MB。两次CPU拷贝意味着240MB/s的内存带宽浪费,还占CPU。在4K或HDR场景下更夸张。
DMA-BUF的解。
DMA-BUF让多个设备共享同一块物理内存,通过文件描述符(fd)在进程和设备之间传递:
核心思路:V4L2驱动分配的DMA缓冲区,通过dma_buf_export导出为dma_buf对象,拿到一个fd;GPU和DRM通过dma_buf_get导入这个fd,直接DMA访问同一块物理内存。CPU全程不碰数据。
dma_buf_export / dma_buf_get
// 导出端(V4L2驱动。// include/linux/dma-buf.h
struct dma_buf_export_info {
const char *exp_name; // 导出者名。 const struct dma_buf_ops *ops; // 操作回调
size_t size; // 缓冲区大。 int flags; // O_RDWR等标志 void *priv; // 私有数据
};
// 导出dma_buf,返回fd
struct dma_buf *dma_buf_export(const struct dma_buf_export_info *exp_info);
// 导入端(GPU/DRM。// 通过fd获取dma_buf
struct dma_buf *dma_buf_get(int fd);
// 使用完毕后释。void dma_buf_put(struct dma_buf *dmabuf);
V4L2侧不用驱动手动调dma_buf_export,vb2框架已经封装好了。用户空间通过VIDIOC_EXPBUFioctl导出缓冲区对应的fd。
// 用户空间:从V4L2缓冲区导出DMA-BUF fd
struct v4l2_exportbuffer expbuf;
memset(&expbuf, 0, sizeof(expbuf));
expbuf.type = V4L2_BUF_TYPE_VIDEO_CAPTURE;
expbuf.index = 0; // 缓冲区索。expbuf.flags = O_RDWR;
if (ioctl(fd, VIDIOC_EXPBUF, &expbuf) < 0) {
perror("VIDIOC_EXPBUF");
return -1;
}
// expbuf.fd 就是DMA-BUF文件描述。printf("DMA-BUF fd = %d\n", expbuf.fd);
传统拷贝 vs DMA-BUF零拷。
| 对比 | 传统mmap拷贝 | DMA-BUF零拷贝 |
|---|---|---|
| CPU参与 | 2次拷贝(内核→用户→GPU) | 0次拷贝 |
| 内存带宽 | 3倍帧大小(读+写) | 1倍帧大小(仅DMA写入) |
| 延迟 | 微秒级拷贝开销 | 纯DMA传输延迟 |
| 内存占用 | 多份副本同时存在 | 一份物理内存共享 |
| 编程复杂度 | 低(标准mmap) | 中(需协调fd传递) |
| 适用场景 | 低帧率/调试 | 高帧率/嵌入式实战 |
| 前提条件 | 设备在同一IOMMU域或物理地址空间 |
八、V4L2 + DMA-BUF实战
V4L2输出DMA-BUF fd
驱动侧需要做两件事:vb2_queue支持VB2_DMABUF模式,ioctl_ops注册vidioc_expbuf。
/* 驱动初始化时允许DMABUF模式 */
dev->queue.io_modes = VB2_MMAP | VB2_DMABUF | VB2_READ;
/* ioctl_ops中注册expbuf */
static const struct v4l2_ioctl_ops dummy_cam_ioctl_ops = {
/* ... 其他回调 ... */
.vidioc_expbuf = vb2_ioctl_expbuf, // vb2默认实现
};
用户空间导出DMA-BUF fd的完整流程:
// v4l2_dmabuf_export.c - 从V4L2导出DMA-BUF fd
#include <stdio.h>
#include <fcntl.h>
#include <unistd.h>
#include <sys/ioctl.h>
#include <linux/videodev2.h>
int main(void)
{
int fd = open("/dev/video0", O_RDWR);
if (fd < 0) { perror("open"); return -1; }
/* 申请缓冲区,使用DMABUF模式 */
struct v4l2_requestbuffers req = {
.count = 4,
.type = V4L2_BUF_TYPE_VIDEO_CAPTURE,
.memory = V4L2_MEMORY_MMAP, // 内核侧仍用mmap分配
};
if (ioctl(fd, VIDIOC_REQBUFS, &req) < 0) {
perror("REQBUFS");
goto out;
}
/* 导出缓冲。的DMA-BUF fd */
struct v4l2_exportbuffer expbuf = {
.type = V4L2_BUF_TYPE_VIDEO_CAPTURE,
.index = 0,
.flags = O_RDWR,
};
if (ioctl(fd, VIDIOC_EXPBUF, &expbuf) < 0) {
perror("EXPBUF");
goto out;
}
printf("DMA-BUF fd = %d\n", expbuf.fd);
/* 将expbuf.fd传给GPU/DRM消费者使*/
/* ... */
close(expbuf.fd);
out:
close(fd);
return 0;
}
GPU/DRM导入DMA-BUF
GPU端(以EGL/DRM为例)导入DMA-BUF fd。
// GPU端导入DMA-BUF(EGL方式。#include <EGL/egl.h>
#include <EGL/eglext.h>
// 从DMA-BUF fd创建EGLImage
EGLImage create_eglimage_from_dmabuf(int dmabuf_fd, int width, int height)
{
EGLint attribs[] = {
EGL_WIDTH, width,
EGL_HEIGHT, height,
EGL_LINUX_DRM_FOURCC_EXT, DRM_FORMAT_YUYV,
EGL_DMA_BUF_PLANE0_FD_EXT, dmabuf_fd,
EGL_DMA_BUF_PLANE0_OFFSET_EXT, 0,
EGL_DMA_BUF_PLANE0_PITCH_EXT, width * 2,
EGL_NONE,
};
return eglCreateImageKHR(eglGetCurrentDisplay(),
EGL_NO_CONTEXT,
EGL_LINUX_DMA_BUF_EXT,
(EGLClientBuffer)NULL,
attribs);
}
DRM显示端导入DMA-BUF fd。
// DRM端导入DMA-BUF(drm_prime_handle方式。#include <drm/drm.h>
#include <xf86drm.h>
int import_dmabuf_to_drm(int drm_fd, int dmabuf_fd)
{
struct drm_prime_handle prime = {
.fd = dmabuf_fd,
.flags = 0,
};
/* 将DMA-BUF fd转为DRM GEM handle */
if (drmIoctl(drm_fd, DRM_IOCTL_PRIME_FD_TO_HANDLE, &prime) < 0) {
perror("PRIME_FD_TO_HANDLE");
return -1;
}
printf("DRM GEM handle = %u\n", prime.handle);
/* 后续用handle做fb创建和显*/
return prime.handle;
}
内核侧跨设备共享的关键路径
// 消费者设备(如GPU驱动)通过dma_buf_get导入
struct dma_buf *dmabuf = dma_buf_get(fd);
if (IS_ERR(dmabuf))
return PTR_ERR(dmabuf);
// 获取dma_buf_attachment,建立设备间的映射关。struct dma_buf_attachment *attach;
attach = dma_buf_attach(dmabuf, dev); // dev是消费者设。if (IS_ERR(attach)) {
dma_buf_put(dmabuf);
return PTR_ERR(attach);
}
// 获取scatter-gather表,用于DMA传输
struct sg_table *sgt;
sgt = dma_buf_map_attachment(attach, DMA_BIDIRECTIONAL);
if (IS_ERR(sgt)) {
dma_buf_detach(dmabuf, attach);
dma_buf_put(dmabuf);
return PTR_ERR(sgt);
}
// 此时sgt->sgl包含物理页面信息,消费者设备的DMA引擎
// 可以直接通过sg_table访问V4L2分配的同一块物理内存
// 使用完毕后释。dma_buf_unmap_attachment(attach, sgt, DMA_BIDIRECTIONAL);
dma_buf_detach(dmabuf, attach);
dma_buf_put(dmabuf);
dma_buf_attach建立设备间连接,dma_buf_map_attachment拿到scatter-gather表,消费者DMA引擎通过sg_table直接访问原始缓冲区。.8内核里,两个设备在同一个IOMMU域的话,dma_buf_map_attachment会自动建IOMMU映射,不用物理地址连续。
九、常见问题
Q1:VIDIOC_DQBUF一直超时,拿不到帧。
检查清单:
- 是否调用了STREAMON——忘了
VIDIOC_STREAMON,DMA根本没启动,当然没有。2. 是否有缓冲区入队——STREAMON之前至少要QBUF一个缓冲区,否则硬件没地方写数。3. 中断是否正常——真实驱动里vb2_buffer_done在中断处理函数中调用,中断没注册或没触发就不会完成缓冲区 - *非阻塞模块——
open时加了O_NONBLOCK,DQBUF没有就绪帧时立即返回EAGAIN,需要用poll/select等待
# 用v4l2-ctl验证设备是否能正常采。v4l2-ctl --device=/dev/video0 --stream-mmap --stream-count=5
Q2:mmap失败返回EINVAL。
常见原因。
1. 没有先REQBUFS——必须先VIDIOC_REQBUFS分配缓冲区,再QUERYBUF+mmap
2. 内存不足——嵌入式平台内存紧张。。080p缓冲区约32MB,可能分配失。3. 缓冲区类型不匹配——REQBUFS和QUERYBUF的type字段必须一。
Q3:DMA-BUF导入失败。
三个排查方向。
1. IOMMU域不一*——V4L2和GPU/DRM不在同一个IOMMU域,DMA地址无法互通。嵌入式SoC通常共享一个IOMMU,问题不大;PCIe设备可能各自有独立IOMMU
2. EXPBUF未注——驱动v4l2_ioctl_ops里没注册vidioc_expbuf回调,或vb2_queue没声明VB2_DMABUF模式
3. fd生命周期*——DMA-BUF fd在消费者使用期间不能关闭,否则底层dma_buf被释放,消费者访问会触发页错。
# 检查IOMMU分组
ls /sys/kernel/iommu_groups/
# 检查设备是否在同一。cat /sys/kernel/iommu_groups/*/devices
十、总结
V4L2 + DMA-BUF速查看
| 操作 | API / ioctl | 说明 |
|---|---|---|
| 注册V4L2设备 | v4l2_device_register |
初始化设备实例 |
| 注册子设备 | v4l2_device_register_subdev |
Sensor/ISP |
| 创建设备节点 | video_register_device |
生成/dev/videoX |
| 初始化vb2队列 | vb2_queue_init |
绑定ops和mem_ops |
| 查询能力 | VIDIOC_QUERYCAP |
第一步必须调用 |
| 设置格式 | VIDIOC_S_FMT |
分辨率+像素格式 |
| 申请缓冲区 | VIDIOC_REQBUFS |
指定数量和内存类型 |
| 映射缓冲区 | mmap |
MMAP模式必须 |
| 缓冲区入队 | VIDIOC_QBUF |
交给驱动填充 |
| 启动采集 | VIDIOC_STREAMON |
开始DMA传输 |
| 缓冲区出队 | VIDIOC_DQBUF |
取回已填充帧 |
| 导出DMA-BUF | VIDIOC_EXPBUF |
获取零拷贝fd |
| 导入DMA-BUF | dma_buf_get |
消费者通过fd导入 |
| 建立映射 | dma_buf_map_attachment |
获取sg_table |
| 停止采集 | VIDIOC_STREAMOFF |
停止DMA |
vb2内存模式选择
| 场景 | 推荐模式 | 原因 |
|---|---|---|
| USB摄像头 | VMALLOC | USB传输已做一次拷贝,物理连续无意义 |
| SoC内置ISP | DMA-contiguous | ISP DMA引擎要求物理连续 |
| 高分辨率Sensor | DMA-sg | 大缓冲区难以分配物理连续内存 |
| 零拷贝到GPU | DMA-contiguous + EXPBUF | DMA-BUF共享需要DMA地址 |
零拷贝适用场景判断
- 帧率。0fps且分辨率。080p 。必须零拷贝,CPU拷贝扛不。- 多消费者(采集→AI推理→编码→显示)→ 零拷贝避免多份拷。- 低帧率调试场。。mmap够用,别给自己找麻烦
本文首发于linuxros.cn,转载请注明出处。