Contents

V4l2loopback实现笔记(六):基本视频回环

本模块实现核心的视频回环功能,是 v4l2loopback 的核心机制演示。

实现的功能

  • 生产者-消费者模式
  • 双 VB2 队列 (OUTPUT + CAPTURE)
  • 内部帧缓冲区传递数据
  • 等待队列同步
  • 第一个打开者为生产者,后续为消费者

学习要点

  1. 理解生产者-消费者模式在内核中的实现
  2. 理解等待队列 (wait_queue_head_t) 的使用
  3. 理解双队列系统的设计
  4. 理解缓冲区共享和同步的挑战
  5. 理解打开者角色分配的策略

核心架构设计

这个模块采用了 Shared Memory via Kernel Copy(内核拷贝共享内存) 的模式。

核心组件

  1. 全局设备对象 (struct loop_device):

    • 这是“中央枢纽”,所有的状态都保存在这里。

    • 关键成员: frame_buffer (中间缓冲区)。这是生产者和消费者交换数据的“中转站”。

  2. 双向 VB2 队列:

    • OUTPUT 队列 (生产者): 负责接收用户空间的视频帧,拷贝到内核的 frame_buffer。

    • CAPTURE 队列 (消费者): 负责从内核的 frame_buffer 读取数据,拷贝回用户空间。

  3. 角色分配机制:

    • 代码逻辑非常简单粗暴:第一个打开设备的是生产者 (OUTPUT),后续打开的是消费者 (CAPTURE)。

架构图解

[ App A: ffmpeg ]      [ Kernel Space: basic_loopback_v4l2 ]       [ App B: ffplay ]
(生产者/Producer)                                                   (消费者/Consumer)
       |                                                                   ^
       v                                                                   |
[ User Buffer ] ---> [ loop_out_buf_queue ] ---> [ frame_buffer ] ---> [ loop_cap_buf_queue ] ---> [ User Buffer ]
   (用户空间)             (VB2 Output)             (vmalloc)              (VB2 Capture)            (用户空间)
                               |
                        wake_up (唤醒)

关键代码解析

一、数据结构定义

1. 缓冲区封装

/* 缓冲区封装 */
struct loop_buffer {
    struct vb2_v4l2_buffer vb;
    struct list_head list;
};

2. 主设备结构体

所有打开这个设备文件的进程,都共享这一个结构体实例。它起到了“共享内存”和“状态同步”的作用。

/* 主设备结构体 */
struct loop_device {
    /* V4L2 核心 */
    struct v4l2_device v4l2_dev;
    struct video_device vdev;

    /* 同步 */
    struct mutex lock;
    spinlock_t qlock;

    /* 等待队列 - 用于消费者等待数据 */
    wait_queue_head_t wait_queue;

    /* 格式 */
    struct v4l2_pix_format pix_format;

    /*
     * 内部缓冲区 - 存储最新的帧数据
     * 这是回环的核心:生产者写入,消费者读取
     */
    void *frame_buffer;
    size_t frame_size;
    bool frame_ready;           /* 是否有新帧可用 */
    unsigned int write_seq;     /* 写入序号 */
    unsigned int read_seq;      /* 读取序号 */

    /* 流状态 */
    bool output_streaming;      /* 输出流是否活动 */
    bool capture_streaming;     /* 捕获流是否活动 */
    int output_opener;          /* 输出打开者计数 */
    int capture_opener;         /* 捕获打开者计数 */
};

void *frame_buffer: 这是内核中的中间缓冲区,生产者将数据写入这里,消费者从这里读取数据。

3. 打开者私有数据

/* 每个打开者的私有数据 */
struct loop_opener {
    struct v4l2_fh fh;
    struct loop_device *dev;
    enum v4l2_buf_type type;    /* OUTPUT 或 CAPTURE */
    struct vb2_queue vb2_queue;
    struct list_head buf_list;
    unsigned int buf_count;
    unsigned int sequence;
};

每当应用程序调用一次 open("/dev/videoX"),内核就会 kmalloc 一个 struct loop_opener。它代表了当前进程与驱动的一次会话。

二、V4L2 文件操作

1. 打开设备

当用户空间的应用程序(如 ffmpeg 或 ffplay)调用 open("/dev/videoX", …) 时,内核就会执行这个 loop_open 函数。它的核心任务是:建立会话(Session),分配角色(生产者 vs 消费者),并初始化缓冲区队列(VB2 Queue)

static int loop_open(struct file *file)
{
    struct loop_opener *opener;
    struct vb2_queue *q;
    int ret;

    printk("loop open\n");

    opener = kzalloc(sizeof(*opener), GFP_KERNEL);
    if (!opener)
        return -ENOMEM;

    opener->dev = loop_dev;
    INIT_LIST_HEAD(&opener->buf_list);

    /* 初始化 V4L2 文件句柄 */
    v4l2_fh_init(&opener->fh, &loop_dev->vdev);
    file->private_data = opener;
    v4l2_fh_add(&opener->fh);

    /*
     * 根据打开顺序决定是输出还是捕获
     * 第一个打开者是输出(生产者),后续是捕获(消费者)
     */
    if (loop_dev->output_opener == 0) {
        opener->type = V4L2_BUF_TYPE_VIDEO_OUTPUT;
        loop_dev->output_opener++;
        printk("opened as OUTPUT (producer)\n");
    } else {
        opener->type = V4L2_BUF_TYPE_VIDEO_CAPTURE;
        loop_dev->capture_opener++;
        printk("opened as CAPTURE (consumer)\n");
    }

    /* 初始化 VB2 队列 */
    q = &opener->vb2_queue;
    q->type = opener->type;
    q->io_modes = VB2_MMAP | VB2_USERPTR | VB2_READ | VB2_WRITE;
    q->drv_priv = opener;
    q->buf_struct_size = sizeof(struct loop_buffer);
    q->mem_ops = &vb2_vmalloc_memops;
    q->timestamp_flags = V4L2_BUF_FLAG_TIMESTAMP_MONOTONIC;
    q->lock = &loop_dev->lock;
    q->min_queued_buffers = LOOP_MIN_BUFFERS;

    if (opener->type == V4L2_BUF_TYPE_VIDEO_OUTPUT) {
        q->ops = &loop_out_qops;
    } else {
        q->ops = &loop_cap_qops;
    }

    ret = vb2_queue_init(q);
    if (ret) {
        printk(KERN_ERR LOOP_MODULE_NAME ": failed to init vb2 queue\n");
        v4l2_fh_del(&opener->fh);
        v4l2_fh_exit(&opener->fh);
        kfree(opener);
        return ret;
    }

    return 0;
}
  1. 分配会话私有数据并挂载v4l2文件句柄

    • kzalloc 分配 struct loop_opener,用于保存当前打开者的状态。
    • v4l2_fh_initv4l2_fh_add 初始化并注册 V4L2 文件句柄以支持事件通知(Events)和优先级控制等标准功能。
  2. 角色分配

    • 通过检查 loop_dev->output_opener 计数器,决定当前打开者是生产者(OUTPUT)还是消费者(CAPTURE)。
    • 第一个打开者被标记为 OUTPUT,后续的都被标记为 CAPTURE。
  3. 初始化 VB2 队列

V4L2 子系统使用 videobuf2 (VB2) 框架来管理视频内存,所以每个打开者都需要初始化一个 VB2 队列。队列类型必须与 open 的类型一致。队列的IO模式设置为支持内存映射(MMAP)、用户指针(USERPTR)、读写操作。mem_ops = &vb2_vmalloc_memops指定使用 vmalloc 分配内存。这意味着视频帧在内核中是使用虚拟连续内存存储的。

  1. 挂载队列操作函数

根据打开者的类型,挂载不同的队列操作函数(loop_out_qopsloop_cap_qops)。这些操作函数定义了如何处理缓冲区的请求、队列启动/停止等操作。

2. 释放设备

当用户空间的应用程序调用 close(fd) 时,内核会执行这个 loop_release 函数。它的核心任务是:清理会话资源,更新设备状态。

三、生产者 VB2 队列操作

四、内存空间分布

这张图展示了内存空间的物理/逻辑分布。

在这个驱动中,内存被分为了三个明确的独立区域。它们都在 内核空间(Kernel Space) 中分配(因为使用了 vb2_vmalloc_memops),但归属权不同。

内存架构图解

+-----------------------------------------------------------------------+
|                        用户空间 (User Space)                          |
+-----------------------------------------------------------------------+
|  [ 生产者 App (ffmpeg) ]                  [ 消费者 App (ffplay) ]     |
|          | (mmap)                                 ^ (mmap)            |
|          v                                        |                   |
+-----------------------------------------------------------------------+
|                        内核空间 (Kernel Space)                        |
+--------------------------+-----------------+--------------------------+
|  区域 A: 生产者队列内存  |  区域 B: 中转   |  区域 C: 消费者队列内存  |
|  (Output Queue Buffers)  |  (Global Buf)   |  (Capture Queue Buffers) |
+--------------------------+-----------------+--------------------------+
|                          |                 |                          |
|   +------------------+   |    memcpy 1     |   +------------------+   |
|   | Buffer [0]       |---|-------------\   |   | Buffer [0]       |   |
|   | (vb2_vmalloc)    |   |             |   |   | (vb2_vmalloc)    |   |
|   +------------------+   |             v   |   +------------------+   |
|                          |                 |                          |
|   +------------------+   | +-------------+ |   +------------------+   |
|   | Buffer [1]       |   | |             | |   | Buffer [1]       |   |
|   | (当前正在处理)   |-->| | frame_buffer| |-->| (当前正在填充)   |   |
|   +------------------+   | | (vzalloc)   | |   +------------------+   |
|                          | +-------------+ |                          |
|   +------------------+   |                 |   +------------------+   |
|   | Buffer [2]       |   |    memcpy 2     |   | Buffer [2]       |   |
|   | (vb2_vmalloc)    |   |             ^   |   | (vb2_vmalloc)    |   |
|   +------------------+   |             |   |   +------------------+   |
|                          |-------------/   |                          |
|                          |                 |                          |
+--------------------------+-----------------+--------------------------+

三大内存区域详解

1. 区域 A:生产者私有队列 (Output Queue)
  • 归属:属于生产者的 struct loop_opener -> vb2_queue
  • 分配方式:当 ffmpeg 调用 REQBUFS 时,由 VB2 框架调用 vmalloc 分配一组 buffer(通常是 3-4 个,构成一个环形队列)。
  • 内容:这里存放的是源视频数据
  • 操作
  • 用户空间通过 mmap 直接往这里写数据。
  • 驱动程序的 loop_out_buf_queue 函数读取这里的地址作为 memcpy源地址 (src)
2. 区域 B:全局共享中转站 (Internal Frame Buffer)
  • 归属:属于全局唯一的 struct loop_device
  • 分配方式:在驱动初始化或 S_FMT(设置格式)时,通过 vzalloc 仅仅分配 1块 内存。
  • 内容:这里存放的是当前最新的一帧画面
  • 作用:它是连接左右两边的桥梁,也是数据的“持久化”地点。
  • 即使生产者把 Buffer [1] 收回去了,这一帧画面依然保存在这里,等待消费者来取。
3. 区域 C:消费者私有队列 (Capture Queue)
  • 归属:属于消费者的 struct loop_opener -> vb2_queue
  • 分配方式:当 ffplay 调用 REQBUFS 时分配一组 buffer。
  • 内容:这里存放的是即将给用户显示的视频数据
  • 操作
  • 驱动程序的 loop_cap_buf_queue 函数将中转站的数据 memcpy 到这里(作为目标地址 dst)。
  • 用户空间通过 mmap 直接读取这里的数据进行渲染。

数据的生命周期 (Data Lifecycle)

  1. 产生:数据首先出现在左边的 区域 A (Buffer 1) 中。
  2. 备份:驱动执行 memcpy,数据被复制到中间的 区域 B (frame_buffer)。此时,左边的 Buffer 1 任务完成,可以被覆盖写下一帧了。
  3. 分发:当消费者请求数据时,驱动执行 memcpy,数据从 区域 B 复制到右边的 区域 C (Buffer 1)
  4. 消费:右边的 Buffer 1 交给用户空间,此时数据完成了穿越。

这种内存设计的优缺点

  • 优点(解耦)

  • 生产者和消费者不需要拥有相同数量的 buffer。

  • 异步性:如果消费者处理得慢,生产者可以继续写(只要不断更新中间的 frame_buffer 即可)。消费者读到的永远是“最新存入的那一帧”,虽然会发生丢帧(旧的被覆盖了),但不会阻塞生产者。

  • 缺点(性能)

  • 这就是所谓的 Double Copy(双重拷贝)

  • 数据路径:RAM -> CPU寄存器 -> RAM (中间) -> CPU寄存器 -> RAM (目标)。

  • 对于高分辨率视频,这会消耗大量的内存带宽和 CPU 缓存。

用户空间交互流程

1. 生产者 (ffmpeg) 流程

ffmpeg -re -f lavfi -i testsrc=size=640x480:rate=30 -vcodec rawvideo -pix_fmt yuv420p -f v4l2 /dev/videoX

关键步骤

  1. 打开设备:调用 fd = open("/dev/video0", O_RDWR),虚拟文件系统查找设备,调用 loop_fops->open,内核执行 loop_open(),分配为 OUTPUT 角色。初始化 vb2_queue,挂载 loop_out_qops

  2. 设置格式:调用 ioctl(fd, VIDIOC_S_FMT, &fmt),内核配置 pix_format 和分配 frame_buffer

  3. 请求缓冲区:调用 ioctl(fd, VIDIOC_REQBUFS, &req),内核通过 VB2 框架分配 OUTPUT 队列的缓冲区(区域 A)。

  4. 核心循环:填数据并入队:用户空间可以通过不同的方式将数据写入缓冲区(mmap/userptr/read/write)。通过 ioctl(fd, VIDIOC_QBUF, &buf) 或者直接调用 write(fd, buf, size) 都可以把缓冲区入队。当使用ioctl时,其内部逻辑为loop_qbuf -> vb2_qbuf -> vb2_core_qbuf;当使用write时,其内部逻辑为loop_write -> vb2_write -> (内部模拟逻辑) -> vb2_core_qbuf。最终,都通过vb2_core_qbuf调用驱动的 loop_out_buf_queue 函数,执行 memcpy 将数据从区域 A 复制到区域 B(中转站),然后标记 frame_ready = true 并唤醒等待队列。

User Space
       [ 路径 A: 标准推流 (MMAP) ]              [ 路径 B: 传统写入 (Write) ]
               |                                       |
  ioctl(fd, VIDIOC_QBUF, ...)                   write(fd, data, len)
               |                                       |
               v                                       v
Kernel VFS     |                                       |
  loop_fops.unlocked_ioctl                        loop_fops.write
     (即 video_ioctl2)                                   |
               |                                       |
               v                                       v
V4L2 / Driver  |                                Driver: loop_write
  loop_ioctl_ops.vidioc_qbuf                           |
               |                                       |
       Driver: loop_qbuf                       调用 vb2_write(...)
               |                                       |
       调用 vb2_qbuf(...)                              |
               |                                       |
               |                        +----------------------------------+
               |                        | VB2 Core (vb2_write 内部逻辑)    |
               |                        | 1. 若未启动,自动 REQBUFS/STREAMON|
               |                        | 2. 找一个空闲 Buffer             |
               |                        | 3. copy_from_user() [数据拷贝#1] |
               |                        | 4. 自动发起入队操作               |
               |                        +----------------------------------+
               |                                       |
               +-------------------+-------------------+
                                   |
                                   v
                     VB2 Core (vb2_core_qbuf)  <--- 【汇流点】
                                   |
                       1. 校验 buffer 状态
                       2. 调用 q->ops->buf_prepare  ----> [loop_out_buf_prepare]
                       3. 将 buffer 状态设为 QUEUED
                       4. 调用 q->ops->buf_queue    ----> [loop_out_buf_queue]
                                   |
                                   v
                     Driver (loop_out_buf_queue)   <--- 【最终干活的地方】
                                   |
                       memcpy() [数据拷贝#2] (QueueBuf -> Global)
                       wake_up() 唤醒消费者
                       vb2_buffer_done() 标记完成

传统方式(write/read)生产者-消费者数据流转全景图

在这个场景中,我们将核心关注点放在 Buffer 的选择逻辑数据的四次搬运 上。

核心概念图例

  • User Buffer: 用户自己 malloc 的临时内存(ffmpeg/ffplay 各 1 个)。
  • Kernel Array: vb2_queue 中的 bufs[] 指针数组(仓库货架)。
  • Kernel Buffer: 实际的内核内存(vmalloc 分配)。
  • Global Buffer: 驱动的中间暂存区。

第一阶段:初始化 (REQBUFS) —— “建立仓库”

在推流和拉流开始前,内核会先建立好“仓库”和“货架”。

[1] ffmpeg 调用 ioctl(REQBUFS, count=3)
       |
       v
   内核 VB2 (Output Queue):
   1. 申请指针数组: bufs[3]
   2. 申请 3 块内核内存 (vmalloc)
   3. 填充数组:
      bufs[0] -> 指向 Kernel_Buf_A (状态: FREE)
      bufs[1] -> 指向 Kernel_Buf_B (状态: FREE)
      bufs[2] -> 指向 Kernel_Buf_C (状态: FREE)

---------------------------------------------------

[2] ffplay 调用 ioctl(REQBUFS, count=3)
       |
       v
   内核 VB2 (Capture Queue):
   1. 申请指针数组: bufs[3]
   2. 申请 3 块内核内存 (vmalloc)
   3. 填充数组:
      bufs[0] -> 指向 Kernel_Buf_X (状态: FREE)
      bufs[1] -> 指向 Kernel_Buf_Y (状态: FREE)
      bufs[2] -> 指向 Kernel_Buf_Z (状态: FREE)

第二阶段:完整数据流转图 (write / read 模式)

这是一张合并了推流和拉流的动态流程图。

文字版详细流程解析

1. 推流侧 (ffmpeg write)

  1. 发起: ffmpeg 用户程序的临时 Buffer 填满数据,调用 write()
  2. 选择 (Selection):
  • 内核 vb2_write 函数查看 vb2_queue->bufs[] 数组。
  • 发现 bufs[0] (Buf A) 的状态是 DEQUEUED (空闲)。
  • 决定使用 Buf A
  1. 拷贝 #1 (User->Kernel):
  • 内核调用 copy_from_user,将 ffmpeg Buffer 的内容复制到 Kernel Buf A。
  1. 入队 (Driver Queue):
  • VB2 调用驱动函数 loop_out_buf_queue(Buf A)
  1. 拷贝 #2 (Kernel->Global):
  • 驱动执行 memcpy(Global_Buf, Buf A)
  • 此时,中间仓库有了最新数据。
  1. 归还:
  • 驱动调用 vb2_buffer_done(Buf A)
  • VB2 将 Buf A 的状态重置为 DEQUEUED
  • write() 函数返回,ffmpeg 可以复用它的临时 Buffer 填下一帧了。

2. 拉流侧 (ffplay read)

  1. 发起: ffplay 用户程序准备一个空的临时 Buffer,调用 read()
  2. 选择 (Selection):
  • 内核 vb2_read 函数查看 vb2_queue->bufs[] 数组。
  • 发现 bufs[0] (Buf X) 的状态是 DEQUEUED (空闲)。
  • 决定使用 Buf X
  1. 入队 (Driver Queue):
  • VB2 调用驱动函数 loop_cap_buf_queue(Buf X)
  1. 拷贝 #3 (Global->Kernel):
  • 驱动执行 memcpy(Buf X, Global_Buf)
  • Buf X 拿到了最新的画面。
  1. 完成信号:
  • 驱动调用 vb2_buffer_done(Buf X)
  1. 拷贝 #4 (Kernel->User):
  • 内核调用 copy_to_user,将 Kernel Buf X 的内容复制到 ffplay Buffer。
  1. 归还:
  • VB2 将 Buf X 的状态重置为 DEQUEUED
  • read() 函数返回,ffplay 拿到数据开始渲染。

总结关键点

  1. 选择逻辑:非常简单粗暴,“谁闲着就用谁”(通常是顺序轮询)。
  2. 解耦:ffmpeg 的 Buffer 和 Kernel Buf A 没有永久绑定关系,只是在那一次 write 调用中临时发生了数据搬运。
  3. 瓶颈:你可以清楚地看到图中的 4 条虚线箭头(数据搬运),这就是为什么 read/write 模式慢的原因。如果换成 MMAP,ffmpeg 直接往 K_Buf_A 写,ffplay 直接读 K_Buf_X,中间只剩 Global 那一次中转。

MMAP数据流转图解

这是一个关于 MMAP 机制 核心逻辑的问题。

在 MMAP 模式下,缓冲区的管理方式与 Read/Write 模式完全不同。控制权发生了反转:在 Read/Write 模式下,内核帮你选 Buffer;但在 MMAP 模式下,FFmpeg 必须自己管理 Buffer 的索引,并且通过“借用”和“归还”的方式与驱动交互。


1. FFmpeg 应该用几个 Buffer?

答案:这是“协商”出来的,通常是 2 到 4 个。

FFmpeg 不会凭空决定用几个,它会走以下流程:

  1. 默认期望:FFmpeg 的 V4L2 输出模块通常默认请求 4 个 Buffer(为了保证足够的平滑度,防止丢帧)。
  2. 驱动限制:FFmpeg 调用 ioctl(VIDIOC_REQBUFS),传入它想要的数量(例如 4)。
  3. 最终决定
  • 你的驱动(basic_loopback_v4l2.c)在 queue_setup 函数里有话语权:
if (*nbuffers < LOOP_MIN_BUFFERS)
    *nbuffers = LOOP_MIN_BUFFERS; // 你的代码强行限制最少 2 个
  • 如果 FFmpeg 请求 4 个,驱动允许,那就用 4 个。
  • 如果 FFmpeg 请求 1 个,驱动强行改为 2 个,FFmpeg 必须接受用 2 个。

最佳实践: 通常推荐 3 个 (Triple Buffering)

  • Buffer A: 驱动正在读取(显示)。
  • Buffer B: FFmpeg 正在填充(准备)。
  • Buffer C: 备用(缓冲抖动)。

2. 填充时,FFmpeg 怎么选择 Free 的 Buffer?

在 MMAP 模式下,FFmpeg 不能随意选择往哪个 Buffer 里写数据。它必须严格遵循 “出列 (DQBUF) -> 填充 -> 入列 (QBUF)” 的循环机制。

我们可以把它想象成 “借盘子” 的游戏。

阶段一:启动阶段 (Priming) —— 所有的盘子都在 FFmpeg 手里

刚开始 (REQBUFS 之后),所有的 Buffer (比如索引 0, 1, 2) 都在用户空间手里,都是 Free 的。

  1. FFmpeg 只有本地记录,它知道 0, 1, 2 都是空的。
  2. FFmpeg 逻辑
  • 选中 Index 0 -> 填满数据 -> ioctl(QBUF, index=0)(把盘子递给内核)。
  • 选中 Index 1 -> 填满数据 -> ioctl(QBUF, index=1)
  • 选中 Index 2 -> 填满数据 -> ioctl(QBUF, index=2)
  1. 此时:FFmpeg 手里没有盘子了!所有 Buffer 都在内核队列里排队。

阶段二:循环阶段 (Steady State) —— 找内核要空盘子

FFmpeg 继续要推流,但手里没 Buffer 了。它必须问内核要:

  1. 索取 (DQBUF): FFmpeg 调用 ioctl(VIDIOC_DQBUF)
  • 如果驱动还没处理完:FFmpeg 阻塞 (Sleep),等待。
  • 如果驱动处理完了(比如 Buffer 0 的数据被拷走了):内核唤醒 FFmpeg,并告诉它:“Index 0 现在空闲了,还给你”
  1. 填充 (Fill): FFmpeg 拿到返回的 Index 0。它根据之前 mmap 得到的地址,往 Index 0 的内存区域写入新的一帧画面。
  2. 归还 (QBUF): FFmpeg 调用 ioctl(VIDIOC_QBUF, index=0),告诉内核:“Index 0 我又填好了,你拿去用吧”。

3. MMAP 完整 Buffer 流转图

假设协商使用了 3 个 Buffer (Index 0, 1, 2)。

4. 关键区别总结

特性Read/Write 模式MMAP 模式
谁分配内存?用户自己 malloc 一个临时的内核分配,用户映射 (map) 过来用
谁决定用哪个?内核决定vb2_write 内部自己找空闲的。协商决定。用户通过 DQBUF 询问内核哪个能用。
数据怎么进去?copy_from_user (拷贝)直接写内存指针 (零拷贝到内核区)
Buffer 状态用户不可见,黑盒。用户通过 QBUF/DQBUF 显式切换所有权。

5. 你的驱动需要改什么吗?

不需要。

  • 你的驱动代码(vb2_opsioctl 接口)已经完全支持这套逻辑了。
  • 只要你按照之前的修复(去掉 M2M 标志,或者禁用 RW 强制 MMAP),FFmpeg 就会自动按照上面的流程图工作:
  1. REQBUFS (申请 2-4 个)。
  2. MMAP (拿到地址)。
  3. 循环 DQBUF -> 写内存 -> QBUF

这就是为什么 MMAP 高效的原因:FFmpeg 知道 Buffer 在哪(Index),直接往那个地址写,写完通知驱动。少了一次 write() 系统调用带来的内存拷贝。

问题

  1. 用户空间->内核空间的数据拷贝有哪些方式?read/write vs mmap vs userptr vs ioctl(QBUF) 有什么区别?分别怎么实现?

  2. 两个队列的内存分布分别是怎样的?推流和拉流的内存路径是怎样的?

  3. 双队列设计的优缺点?如果只用单队列会怎么样?有哪些挑战?