4. mmap 映射帧缓冲:将帧缓冲设备文件映射到用户空间

帧缓冲设备(/dev/fb0)在 Linux 内核中本质是一段连续的物理内存区域。用户空间程序无法直接访问内核态物理地址,必须通过 mmap() 系统调用将这段内核缓冲区映射到进程的虚拟地址空间。本章详细拆解映射前的参数准备、映射大小计算以及关键配置项。

4.1 映射前的三要素准备

在调用 mmap() 之前,必须从帧缓冲设备获取三个核心参数:屏幕分辨率、每像素位数(bpp) 和 行跨度(stride/line_length)。这些参数通过 ioctl() 读取 fb_var_screeninfo 和 fb_fix_screeninfo 结构体获得。

关键步骤: 先 open 设备 → ioctl 获取可变/固定信息 → 计算映射大小 → mmap 建立映射。
参数 来源结构体 说明
xres / yres fb_var_screeninfo 可见分辨率宽度和高度(像素)
bits_per_pixel fb_var_screeninfo 每像素位数,常见 16、24、32
line_length fb_fix_screeninfo 一行实际占用的字节数(含对齐填充)
smem_len fb_fix_screeninfo 帧缓冲总物理内存大小(字节)
性能提示: 不要用 xres * yres * (bpp / 8) 直接计算映射大小。由于硬件行对齐(stride),实际 line_length 可能大于 xres * (bpp / 8),使用 line_length * yres 更准确。

4.2 映射大小计算:两种策略对比

映射大小直接影响可访问的显存范围和性能。以下是两种主流计算策略:

  • 策略 A(精确映射): size = line_length * yres。仅映射可见显示区域,节省虚拟地址空间,适合单缓冲场景。
  • 策略 B(全缓冲映射): size = smem_len。映射整个帧缓冲物理内存,包含可能存在的硬件光标、叠加层或双缓冲区域,适合复杂图形场景。
// 策略 A 示例代码
#include <linux/fb.h>
#include <sys/mman.h>
#include <fcntl.h>
#include <unistd.h>

int fb_fd;
struct fb_var_screeninfo var;
struct fb_fix_screeninfo fix;
unsigned char *fb_ptr;
long screensize;

// 1. 打开设备
fb_fd = open("/dev/fb0", O_RDWR);
if (fb_fd == -1) { /* 错误处理 */ }

// 2. 获取可变信息(分辨率、bpp)
ioctl(fb_fd, FBIOGET_VSCREENINFO, &var);
// 3. 获取固定信息(line_length、smem_len)
ioctl(fb_fd, FBIOGET_FSCREENINFO, &fix);

// 4. 计算映射大小(策略 A:精确映射)
screensize = fix.line_length * var.yres;

// 5. mmap 映射
fb_ptr = mmap(NULL, screensize,
              PROT_READ | PROT_WRITE,
              MAP_SHARED,
              fb_fd, 0);

if (fb_ptr == MAP_FAILED) { /* 错误处理 */ }

// 使用 fb_ptr 进行像素操作...

// 6. 解除映射
munmap(fb_ptr, screensize);
close(fb_fd);
常见错误: 使用 MAP_PRIVATE 代替 MAP_SHARED。MAP_PRIVATE 会触发写时复制(COW),写入操作不会同步到帧缓冲硬件,屏幕无变化且浪费内存。帧缓冲映射必须使用 MAP_SHARED。

4.3 mmap 参数配置深度解析

mmap() 原型为 void *mmap(void *addr, size_t length, int prot, int flags, int fd, off_t offset)。针对帧缓冲场景,各参数配置如下:

参数 推荐值 性能影响
addr NULL(由内核选择) 指定地址可能导致映射失败或 TLB 未命中
length line_length * yres 或 smem_len 过小导致访问越界 SIGSEGV;过大浪费 VMA
prot PROT_READ | PROT_WRITE 只读映射无法更新屏幕;只写映射无法回读
flags MAP_SHARED MAP_PRIVATE 导致写时复制,性能下降 30%~50%
offset 0(从头映射) 非零偏移用于映射多缓冲区的第二页,需页对齐
高级技巧: 对于双缓冲或多缓冲场景,可将 offset 设置为 page_aligned_offset(必须是 getpagesize() 的整数倍),映射不同缓冲页到不同虚拟地址,实现快速切换。

4.4 映射流程与内存布局 SVG

下图展示了从用户空间 mmap() 调用到帧缓冲物理内存的完整映射链路:

mmap 帧缓冲映射流程与内存布局 mmap 帧缓冲映射流程与内存布局 用户空间进程 虚拟地址空间 mmap() MAP_SHARED 内核帧缓冲驱动 fb_mmap() 回调 物理内存(帧缓冲区域) 行 0 (line_length 字节) 行 1 (line_length 字节) 行 2 (line_length 字节) ... 对齐填充 (padding) line_length > xres * (bpp/8) 映射大小计算 策略A: line_length × yres 策略B: smem_len offset: 页对齐倍数

4.5 映射后验证清单

完成 mmap() 后,建议执行以下验证步骤确保映射正确:

  1. 检查返回值: MAP_FAILED(即 (void*)-1)表示映射失败,检查 errno(常见 EINVAL、ENOMEM、EACCES)。
  2. 验证可访问范围: 尝试读写映射区域最后一个字节(fb_ptr[screensize - 1]),不触发 SIGSEGV 即成功。
  3. 检查页对齐: 映射大小会被内核向上取整到页大小(通常 4096 字节),实际可用大小可能略大于请求的 screensize,但不要依赖。
  4. 性能基准: 使用 gettimeofday() 或 clock_gettime() 测量一次全屏填充耗时,作为后续优化对比基线。
性能基线参考: 在 1024×768×32bpp 配置下,使用 MAP_SHARED 映射后,一次全屏 memset 填充(4 字节每像素)耗时约 8~15 ms(取决于内存带宽和 CPU 频率)。若超过 30 ms,说明映射配置或硬件路径存在瓶颈。

4.6 常见映射错误与排查

错误现象 可能原因 解决方案
mmap 返回 MAP_FAILED,errno = EINVAL offset 不是页对齐,或 length 为 0 确保 offset 是 getpagesize() 整数倍;length > 0
mmap 返回 MAP_FAILED,errno = ENOMEM 请求映射大小超过系统允许的单个 VMA 上限 减小映射大小,或检查 /proc/sys/vm/max_map_count
写入屏幕无变化 使用了 MAP_PRIVATE 而非 MAP_SHARED 修改 flags 为 MAP_SHARED
写入部分区域花屏 映射大小小于实际 line_length × yres 使用 fix.line_length 重新计算映射大小
程序崩溃(SIGSEGV) 访问了映射范围之外的地址 严格限制像素操作在 fb_ptr[0] 到 fb_ptr[screensize-1] 之间
安全提醒: 帧缓冲映射区域属于共享内存,多进程同时写入可能导致数据竞争。若需多进程渲染,建议使用用户空间双缓冲 + 页面翻转(FBIOPAN_DISPLAY),而非直接并发写帧缓冲。