Framebuffer 框架深度剖析:从结构体到 mmap 机制

说实话,Framebuffer 这玩意儿,在 Linux 显示驱动里算是老前辈了。我刚开始接触内核驱动时,第一个正经写的显示驱动就是基于 fbdev 框架的。那时候 DRM 还没这么普及,大家都是在 fb_info 这个结构体上做文章。今天咱们就把它拆开看看,里面到底装了些什么。

fb_info:这个庞然大物到底长什么样?

fb_info 是整个 Framebuffer 驱动的核心数据结构。你可以把它想象成一个超级控制中心——所有和显示相关的信息、操作、状态都塞在里面。我习惯把它分成几个功能区来看:

字段区域 典型成员 作用
设备信息 struct fb_var_screeninfo var
struct fb_fix_screeninfo fix
可变参数(分辨率、位深、时序)
固定参数(显存地址、步长、类型)
操作接口 struct fb_ops *fbops 所有硬件操作的函数指针集合
显存管理 void *screen_base
unsigned long screen_size
显存虚拟地址和大小
设备模型 struct device *device
struct module *owner
内核设备模型挂载点、模块引用计数
辅助设施 struct fb_blit_cursor cursor
struct fb_pixmap pixmap
硬件光标加速、像素格式转换辅助

这里有个细节我想提一下——var 和 fix 的区别。我在项目中见过不少新手把这两个搞混。var 是用户态可以通过 FBIOPUT_VSCREENINFO 改的,比如你调分辨率、改刷新率,动的就是它。fix 是硬件决定的,用户只能读不能写。说白了,一个是「我想怎么显示」,一个是「硬件只能这么显示」。

fb_ops 操作集:驱动的心脏

fb_ops 就是驱动真正干活的函数表。每个函数指针对应一个硬件操作。我列一下最核心的几个:

  • fb_open / fb_release:设备打开和关闭时的钩子。我一般在这里做电源管理和引用计数。
  • fb_read / fb_write:直接读写显存。说实话,现在很少用了,但 legacy 代码里还有。
  • fb_setcoloreg:设置调色板。8 位色深时代必备,现在真彩色用得少,但兼容性不能丢。
  • fb_blank:开关显示。省电场景下很重要。
  • fb_pan_display:画面平移。双缓冲切换时用,我当年做视频播放器驱动就靠它实现无撕裂。
  • fb_mmap:这个最核心,后面单独讲。
我的习惯:写 fb_ops 时,先把 fb_fillrect、fb_copyarea、fb_imageblit 这三个加速函数实现好。用户态 GUI 库(比如 Qt、DirectFB)会大量调用它们。不实现的话,回退到软加速,性能直接掉一个数量级。

注册与注销:驱动的一生

流程其实不复杂,但顺序错了就会出问题。我吃过这个亏——有一次注销时先释放了显存,再注销设备,结果内核 panic 了。嗯,顺序很重要。

注册流程

  1. 分配 fb_info:framebuffer_alloc(),顺便带上你的私有数据大小。
  2. 填充固定参数:设置 fix 结构体,包括显存物理地址、步长、类型标志。
  3. 填充可变参数:设置 var 的默认分辨率、位深、时序。
  4. 挂载 fb_ops:把实现好的操作集指针赋给 fbops。
  5. 分配显存:用 dma_alloc_coherent() 或 ioremap(),把地址填进 screen_base。
  6. 注册设备:register_framebuffer(),这步之后用户就能看到 /dev/fbX 了。

注销流程

  1. 注销设备:unregister_framebuffer(),确保没有用户还在操作。
  2. 释放显存:根据分配方式调用 dma_free_coherent() 或 iounmap()。
  3. 释放 fb_info:framebuffer_release()。
我曾经踩过的坑:在 fb_open 里做了 try_module_get(),但 fb_release 里忘了 module_put()。结果模块卸载时引用计数不为零,死活卸载不掉。调试了一下午才发现。

mmap 机制:用户态直接操作显存

这是 Framebuffer 最精髓的地方。用户态程序通过 mmap 把显存映射到自己的地址空间,然后直接读写像素数据,不需要经过系统调用。性能就是这么来的。

驱动里要做的事情其实就一件——实现 fb_mmap 回调。内核提供了辅助函数 fb_deferred_io_mmap() 和 fb_mmap(),但如果你想自己控制,核心逻辑是:

static int my_fb_mmap(struct fb_info *info, struct vm_area_struct *vma)
{
    unsigned long start = info->fix.smem_start;
    unsigned long size = info->fix.smem_len;
    pgprot_t prot = vma->vm_page_prot;

    // 如果是 DMA 内存,需要设置非缓存属性
    prot = pgprot_writecombine(prot);

    // 建立物理地址到用户空间的映射
    return remap_pfn_range(vma, vma->vm_start,
                           start >> PAGE_SHIFT,
                           size, prot);
}

这里有个关键点——缓存一致性。显存通常是 DMA 内存,CPU 和显示控制器都会访问。如果 CPU 用了 cache,显示控制器读到的可能是 stale 数据。我一般用 pgprot_writecombine(),它允许写合并但禁止缓存,性能和正确性都能兼顾。

mmap 的典型应用场景:
  • 直接绘制像素数据(游戏、GUI 框架)
  • 双缓冲切换(通过 pan display 配合)
  • 视频帧渲染(ffmpeg 等直接输出到显存)

知识结构总览

下面这张图把 fb_info、fb_ops、注册注销、mmap 的关系串起来了。我画图时习惯把数据流和控制流分开,这样看起来更清楚。

Framebuffer 框架核心结构图 Framebuffer 框架核心结构图 struct fb_info struct fb_ops *fbops fb_open / fb_release fb_setcoloreg / fb_blank fb_pan_display / fb_mmap fb_fillrect / fb_copyarea 显存管理 screen_base (虚拟地址) screen_size (显存大小) fix.smem_start (物理地址) 注册:alloc → 填充 → register → /dev/fbX 注销:unregister → 释放显存 → framebuffer_release

你看,整个框架其实就围绕三个核心:数据结构(fb_info)、操作接口(fb_ops)、生命周期管理(注册/注销)。mmap 是连接用户态和内核态显存的关键桥梁。搞懂了这些,Framebuffer 驱动对你来说就没秘密了。

调试小技巧:注册成功后,在用户态执行 cat /sys/class/graphics/fb0/virtual_size 可以查看分辨率。如果显示异常,先检查 fix.line_length 是不是正确——步长算错了,画面会斜着撕裂,我遇到过两次。