3. Linux显示子系统概览
在Linux图形与显示栈中,fbdev、DRM、KMS、GEM、V4L2并不是彼此孤立的技术名词,而是不同历史阶段、不同抽象层次、不同职责边界共同构成的显示子系统。理解它们的关系,关键在于抓住两条主线:一条是“帧缓冲如何被用户空间访问”,另一条是“显示控制器、内存与视频采集如何被统一管理”。fbdev代表早期简单帧缓冲抽象,DRM/KMS代表现代显示管线抽象,GEM负责显存对象管理,V4L2则主要面向视频采集与输出设备,但在显示链路中常与DRM通过DMA-BUF协作。
一、核心原理:从“一块内存”到“一条显示管线”
fbdev的核心思想非常直接:内核暴露一个/dev/fb0字符设备,用户空间通过mmap映射一块连续或伪连续的帧缓冲内存,向其中写入像素数据,显示控制器便按固定时序扫描输出。它适合早期单图层、无硬件加速、无复杂合成需求的场景。其典型操作如下:
# 查看帧缓冲信息
fbset -i
# 向 fb0 写入测试图像
cat /dev/urandom > /dev/fb0
# 用户空间 mmap 伪代码
fd = open("/dev/fb0", O_RDWR);
fb = mmap(NULL, screensize, PROT_READ | PROT_WRITE, MAP_SHARED, fd, 0);
但fbdev的局限也很明显:它通常只描述一个默认帧缓冲,难以表达多图层、多CRTC、多连接器、页面翻转、垂直同步、原子提交等现代显示能力。于是DRM(Direct Rendering Manager)逐步成为Linux显示驱动的主流框架。DRM最初解决GPU直接渲染与内核态内存管理问题,后来与KMS(Kernel Mode Setting)结合,形成完整的显示模式设置与输出管理能力。
KMS负责“模式设置”:选择分辨率、刷新率、像素格式、输出接口、连接器状态,并把framebuffer绑定到CRTC与plane上。GEM(Graphics Execution Manager)则负责“显存对象”的分配、共享、同步与生命周期管理。现代DRM驱动中,KMS与GEM通常协同工作:GEM创建buffer object,KMS把buffer object包装成framebuffer,再通过plane提交到CRTC,最终由encoder和connector输出到物理接口。
二、方法流程:一次典型DRM/KMS显示提交
从用户空间看,一次完整的显示更新通常遵循“打开设备→获取资源→创建framebuffer→设置模式→页面翻转”的流程。以libdrm为例,其简化路径如下:
drmOpen("/dev/dri/card0");
drmModeGetResources(fd);
drmModeGetConnector(fd, connector_id);
drmModeGetEncoder(fd, encoder_id);
drmModeGetCrtc(fd, crtc_id);
// 创建 dumb buffer 并映射
drmModeCreateDumbBuffer(fd, width, height, bpp, 0, &create);
drmModeMapDumbBuffer(fd, create.handle, &map);
memset(map, 0x00, create.size);
// 添加 framebuffer
drmModeAddFB(fd, width, height, 24, 32, create.pitch,
create.handle, &fb_id);
// 设置 CRTC 并提交
drmModeSetCrtc(fd, crtc_id, fb_id, 0, 0,
&connector_id, 1, &mode);
在这个过程中,GEM对象是底层内存载体,KMS对象是显示拓扑与提交单元。若使用原子提交(Atomic Modeset),还会引入drmModeAtomicReq,把plane、CRTC、connector的属性一次性提交,避免中间状态闪烁。原子提交是现代DRM驱动推荐的方式,也是Wayland合成器、Xorg modesetting驱动、KMS屏显应用的基础。
三、V4L2与显示子系统的关系
V4L2(Video for Linux 2)主要面向视频采集、摄像头、视频输出、编解码等设备,其设备节点通常为/dev/video0。它并不直接替代DRM/KMS,但在实际系统中,摄像头采集的数据往往需要显示到屏幕上,或者显示内容需要被视频编码器读取。这时,DMA-BUF成为关键桥梁:V4L2导出DMA-BUF fd,DRM导入该fd并创建framebuffer,或者反过来由DRM导出、V4L2导入。这样,采集与显示之间可以实现零拷贝共享。
因此,V4L2与DRM/KMS的关系可以概括为:V4L2负责“视频流的生产与消费”,DRM/KMS负责“显示管线的组织与输出”,GEM/DMA-BUF负责“跨设备缓冲区共享”。fbdev则更多作为兼容层存在,现代内核中常用drm_fbdev_generic或fbdev emulation为旧用户空间提供/dev/fb0接口。
四、案例与场景:摄像头预览到屏幕显示
假设一个嵌入式设备需要把摄像头画面实时显示到LCD上。传统做法可能是V4L2采集到用户空间,再通过fbdev写入/dev/fb0,这会产生一次甚至多次内存拷贝。现代做法则是:V4L2驱动导出DMA-BUF fd,DRM驱动导入该fd并创建framebuffer,然后通过KMS plane直接扫描输出。这样,摄像头传感器→CSI→V4L2→DMA-BUF→DRM/KMS→LCD控制器形成一条零拷贝显示链路。其关键伪代码如下:
// V4L2 侧导出 DMA-BUF
struct v4l2_exportbuffer expbuf = {0};
expbuf.type = V4L2_BUF_TYPE_VIDEO_CAPTURE;
expbuf.index = 0;
expbuf.flags = O_CLOEXEC;
ioctl(v4l2_fd, VIDIOC_EXPBUF, &expbuf);
int dma_fd = expbuf.fd;
// DRM 侧导入 DMA-BUF 并创建 framebuffer
drmPrimeFDToHandle(drm_fd, dma_fd, &handle);
drmModeAddFB2(drm_fd, width, height, DRM_FORMAT_NV12,
handles, pitches, offsets, &fb_id, 0);
// 提交到 plane
drmModeSetPlane(drm_fd, plane_id, crtc_id, fb_id, 0,
0, 0, width, height,
0, 0, width << 16, height << 16);
这个案例说明,V4L2与DRM/KMS并非竞争关系,而是通过DMA-BUF形成协作。fbdev在这个链路中通常不再参与,除非需要兼容旧应用。
五、误区与复盘
常见误区一:认为DRM就是KMS。实际上DRM是更大的框架,KMS是DRM中的显示模式设置部分,GEM是内存管理部分。
常见误区二:认为V4L2只能做摄像头。V4L2也包含视频输出设备、编解码设备、radio等,只是显示输出主路径通常由DRM/KMS承担。
常见误区三:认为fbdev已经完全消失。现代内核仍提供fbdev模拟层,用于兼容旧用户空间和简单控制台显示。
| 子系统 | 主要职责 | 典型设备节点 | 与现代显示的关系 |
|---|---|---|---|
| fbdev | 简单帧缓冲抽象,mmap直接写像素 | /dev/fb0 | 兼容层,逐步被DRM fbdev模拟替代 |
| DRM | 直接渲染管理、设备与内存统一模型 | /dev/dri/card0 | 现代显示驱动核心框架 |
| KMS | 模式设置、plane/CRTC/encoder/connector管理 | 通过DRM设备访问 | 显示管线与输出控制核心 |
| GEM | 显存对象分配、共享、同步 | 通过DRM ioctl访问 | 与DMA-BUF共同实现跨设备零拷贝 |
| V4L2 | 视频采集、输出、编解码 | /dev/video0 | 通过DMA-BUF与DRM/KMS协作 |
学习建议:先理解“fbdev是单缓冲直写,DRM/KMS是多对象管线,GEM是内存对象,V4L2是视频流”,再通过modetest、kmscube、v4l2-ctl等工具观察实际设备节点与属性,就能把抽象概念落到具体驱动行为上。