V4L2到DRM全链路:从摄像头到屏幕的零拷贝之旅
导读:摄像头数据如何在Linux内核中走完"采集→处理→显示"全程?本文用一张全景图+一次采集追踪,讲清楚V4L2、DRM/DMABUF三者的关系。每个模块标注输入输出,5分钟搞懂Linux视频管线。
一、三层架构全景图
视频数据在Linux内核中流经三个子系统:V4L2负责采集,DRM负责显示,DMA-BUF负责零拷贝桥接。
flowchart TB
subgraph 采集层["① V4L2 采集层"]
direction TB
VDEV["video_device<br/>/dev/video0"]
VB2["vb2_queue<br/>缓冲区管理"]
end
subgraph 共享层["② DMA-BUF 零拷贝桥"]
DMA["文件描述符传递<br/>/dev/dma_buf fd"]
end
subgraph 显示层["③ DRM 显示层"]
direction TB
FB["Framebuffer<br/>像素缓冲区"]
PLANE["Plane 图层"]
CRTC["CRTC 扫描时序"]
CONN["Connector<br/>HDMI/eDP 输出"]
end
VDEV --> VB2
VB2 -->|"EXPBUF<br/>导出fd"| DMA
DMA -->|"PRIME导入<br/>复用同一帧"| FB
FB --> PLANE --> CRTC --> CONN
CONN --> 屏幕(["屏幕"])
style VDEV fill:#E3F2FD,stroke:#1976D2
style VB2 fill:#E3F2FD,stroke:#1976D2
style DMA fill:#FFF8E1,stroke:#F57C00
style FB fill:#E8F5E9,stroke:#388E3C
style PLANE fill:#E8F5E9,stroke:#388E3C
style CRTC fill:#E8F5E9,stroke:#388E3C
style CONN fill:#E8F5E9,stroke:#388E3C
三层职责速记:
| 层 | 核心组件 | 一句话 |
|---|---|---|
| ① V4L2 | /dev/video0 + vb2_queue |
把传感器数据送入内核缓冲区 |
| ② DMA-BUF | EXPBUF + PRIME |
把内核缓冲区"借"给显示驱动,不拷贝 |
| ③ DRM | FB → Plane → CRTC → Connector | 把像素数据扫描到屏幕 |
二、一次采集请求的完整旅程
摄像头按下录制键,数据走了一条怎样的路?追踪一帧图像从镜头到屏幕的完整路径。
来自 linuxros.cn · linuxROS
flowchart LR
SENSOR(["CMOS 图像传感器"]) --> CSI["MIPI CSI-2<br/>高速串行"]
CSI --> ISP["ISP 图像处理<br/>Bayer/去噪/WB"]
ISP --> VDEV["video_device<br/>/dev/video0"]
VDEV --> VB2["vb2_queue<br/>缓冲区入队"]
VB2 --> DQ["DQBUF<br/>出队取帧"]
DQ --> EXP["EXPBUF<br/>导出 DMA-BUF fd"]
EXP --> PRIME["DRM PRIME<br/>导入 fd"]
PRIME --> FB["Framebuffer<br/>像素写入"]
FB --> SCAN["CRTC 扫描<br/>HDMI 输出"]
SCAN --> 屏幕(["屏幕显示"])
style SENSOR fill:#FFEBEE,stroke:#D32F2F
style CSI fill:#FFEBEE,stroke:#D32F2F
style ISP fill:#FFEBEE,stroke:#D32F2F
style VDEV fill:#E3F2FD,stroke:#1976D2
style VB2 fill:#E3F2FD,stroke:#1976D2
style DQ fill:#E3F2FD,stroke:#1976D2
style EXP fill:#FFF8E1,stroke:#F57C00
style PRIME fill:#FFF8E1,stroke:#F57C00
style FB fill:#E8F5E9,stroke:#388E3C
style SCAN fill:#E8F5E9,stroke:#388E3C
关键数据流(按顺序):
- 镜头光信号 → CMOS 传感器 → MIPI CSI-2 串行传输
- ISP 完成 Bayer 解码/去噪/白平衡 → 原始图像
open("/dev/video0")→VIDIOC_S_FMT设置格式(1920×1080 NV12)VIDIOC_REQBUFS+VIDIOC_QBUF申请并入队缓冲区VIDIOC_STREAMON启动流 → 驱动开始填充帧VIDIOC_DQBUF出队 → 拿到已填充帧的句柄VIDIOC_EXPBUF导出 DMA-BUF fd → 零拷贝关键drmPrimeFDToHandle导入 fd → 创建 Framebuffer- CRTC 持续扫描 → HDMI 输出到屏幕
三、V4L2 三种缓冲区模式对比
| 模式 | 原理 | 适用场景 |
|---|---|---|
| MMAP | 内核分配缓冲区,mmap 直接访问 | 简单采集,最常用 |
| USERPTR | 用户空间分配内存,传地址给驱动 | 已有内存池复用 |
| DMABUF ⭐ | 内核缓冲区通过 fd 跨设备共享 | V4L2↔DRM 零拷贝 |
四、DMA-BUF 零拷贝 vs 传统拷贝路径
flowchart TB
subgraph 零拷贝["🟢 DMA-BUF 零拷贝(推荐)"]
Z1["V4L2 采集帧"] --> Z2["内核 DMA 缓冲区"]
Z2 --> Z3["导出 dma_buf_fd"]
Z3 --> Z4["DRM PRIME 导入"]
Z4 --> Z5["直接 scanout 到屏幕"]
end
subgraph 传统["🔴 传统拷贝(低效)"]
T1["V4L2 采集帧"] --> T2["mmap 到用户空间"]
T2 --> T3["CPU 拷贝/转换"]
T3 --> T4["写入 DRM dumb buffer"]
T4 --> T5["DRM 显示输出"]
end
Z1 -.->|"零次CPU拷贝"| Z5
T1 -.->|"≥2次CPU拷贝"| T5
style Z1 fill:#E8F5E9,stroke:#388E3C
style Z2 fill:#E8F5E9,stroke:#388E3C
style Z3 fill:#E8F5E9,stroke:#388E3C
style Z4 fill:#E8F5E9,stroke:#388E3C
style Z5 fill:#E8F5E9,stroke:#388E3C
style T1 fill:#FFEBEE,stroke:#D32F2F
style T2 fill:#FFEBEE,stroke:#D32F2F
style T3 fill:#FFEBEE,stroke:#D32F2F
style T4 fill:#FFEBEE,stroke:#D32F2F
style T5 fill:#FFEBEE,stroke:#D32F2F
五、总结
记一个口诀:采集入队→DQBUF取帧→EXPBUF导出→PRIME导入→CRTC扫描。
整个Linux视频管线的本质就是零拷贝传递:数据从摄像头到屏幕全程在DMA层面流转,不经过用户空间,不触发一次CPU拷贝。理解这个传递链,就理解了V4L2和DRM的协作方式。
下篇预告:libcamera 如何用一套API统一V4L2与Android Camera HAL,让摄像头应用一次编写、跨平台运行。
参考来源
- V4L2 API — Linux Media Subsystem
- DRM/KMS API — Linux GPU Driver Developer's Guide
- DMA-BUF Sharing — Linux Kernel Documentation
- V4L2 Buffer Management — Media Controller API