4. 使用 mmap 映射 Framebuffer:打开设备文件、获取屏幕参数、mmap 映射、解除映射 (munmap)

Framebuffer 设备在 Linux 用户空间中的核心操作遵循“打开 → 获取参数 → 映射 → 访问 → 解除映射 → 关闭”的生命周期。其中 mmap 系统调用将内核空间的帧缓冲物理内存直接映射到用户进程的虚拟地址空间,从而避免 read/write 的系统调用开销,是实现高性能像素操作的基础。

4.1 操作流程总览

完整的 Framebuffer mmap 映射流程包含以下 6 个步骤,其中步骤 3 和步骤 5 是本章的核心关注点:

步骤 系统调用 / 函数 关键参数 / 返回值 说明
1 open() "/dev/fb0" → fd 以 O_RDWR 方式打开帧缓冲设备
2 ioctl() FBIOGET_VSCREENINFO 获取可变屏幕参数(分辨率、位深、像素格式)
3 ioctl() FBIOGET_FSCREENINFO 获取固定屏幕参数(smem_len、line_length)
4 mmap() smem_len → mapped_addr 将物理帧缓冲映射到用户空间虚拟地址
5 munmap() mapped_addr, smem_len 解除映射,释放虚拟地址空间
6 close() fd 关闭设备文件描述符
Framebuffer mmap 映射生命周期 用户空间进程 内核 Framebuffer ① open("/dev/fb0") ② ioctl(FBIOGET_*) ③ mmap() 映射 虚拟地址 ↔ 物理内存 帧缓冲物理内存 smem_len bytes ④ 直接读写像素 ⑤ munmap() 解除映射 解除虚拟地址映射

4.2 打开设备文件

Framebuffer 设备文件通常位于 /dev/fb0(主设备号 29,次设备号 0)。多显示器系统可能包含 /dev/fb1、/dev/fb2 等。打开时必须使用 O_RDWR 标志,因为后续的 mmap 映射需要读写权限。

#include <fcntl.h>
#include <unistd.h>

int fb_fd = open("/dev/fb0", O_RDWR);
if (fb_fd < 0) {
    perror("open /dev/fb0 failed");
    return -1;
}
权限提示: 非 root 用户需要将用户加入 video 组(sudo usermod -aG video $USER)才能获得 /dev/fb0 的读写权限。否则 open 会返回 EACCES。

4.3 获取屏幕参数

在 mmap 之前,必须通过 ioctl 获取两个关键结构体:可变屏幕信息(struct fb_var_screeninfo)和 固定屏幕信息(struct fb_fix_screeninfo)。

4.3.1 可变屏幕信息 (fb_var_screeninfo)

该结构体描述当前可变的显示模式参数,通过 FBIOGET_VSCREENINFO 命令获取:

成员 类型 说明
xres __u32 可见水平分辨率(像素)
yres __u32 可见垂直分辨率(像素)
bits_per_pixel __u32 每像素位数(16/24/32)
red/green/blue/transp struct fb_bitfield 各颜色通道的偏移和长度
pixclock __u32 像素时钟(皮秒)

4.3.2 固定屏幕信息 (fb_fix_screeninfo)

该结构体描述帧缓冲的固定硬件属性,通过 FBIOGET_FSCREENINFO 命令获取:

成员 类型 说明
smem_start unsigned long 帧缓冲物理起始地址(仅供内核使用)
smem_len __u32 帧缓冲总长度(字节)—— mmap 的 size 参数
line_length __u32 一行像素占用的字节数(含行对齐填充)
type __u32 帧缓冲类型(通常为 FB_TYPE_PACKED_PIXELS)
visual __u32 颜色模式(真彩色/伪彩色/直接彩色)
#include <linux/fb.h>
#include <sys/ioctl.h>

struct fb_var_screeninfo vinfo;
struct fb_fix_screeninfo finfo;

// 获取可变信息
if (ioctl(fb_fd, FBIOGET_VSCREENINFO, &vinfo) < 0) {
    perror("FBIOGET_VSCREENINFO failed");
    close(fb_fd);
    return -1;
}

// 获取固定信息
if (ioctl(fb_fd, FBIOGET_FSCREENINFO, &finfo) < 0) {
    perror("FBIOGET_FSCREENINFO failed");
    close(fb_fd);
    return -1;
}

printf("Resolution: %dx%d, %d bpp\n",
       vinfo.xres, vinfo.yres, vinfo.bits_per_pixel);
printf("Framebuffer size: %u bytes, line_length: %u\n",
       finfo.smem_len, finfo.line_length);
关键参数确认: smem_len 是 mmap 映射长度的唯一正确依据。不要使用 xres * yres * (bits_per_pixel / 8) 计算,因为硬件可能因行对齐(line_length > xres * bpp/8)导致实际内存大于理论值。使用错误长度映射会导致访问越界或显示错位。

4.4 mmap 映射帧缓冲

获取屏幕参数后,使用 mmap 将内核帧缓冲映射到用户空间。映射后的指针可以直接像操作普通内存数组一样读写像素数据。

#include <sys/mman.h>

// 计算映射长度:使用 finfo.smem_len 确保覆盖整个帧缓冲
size_t screensize = finfo.smem_len;

// mmap 映射
void *fb_ptr = mmap(NULL,                    // 内核选择起始地址
                    screensize,              // 映射长度
                    PROT_READ | PROT_WRITE,  // 读写权限
                    MAP_SHARED,              // 共享映射,写回设备
                    fb_fd,                   // 设备文件描述符
                    0);                      // 偏移量(通常为 0)

if (fb_ptr == MAP_FAILED) {
    perror("mmap framebuffer failed");
    close(fb_fd);
    return -1;
}

printf("Framebuffer mapped at: %p, size: %zu bytes\n", fb_ptr, screensize);

4.4.1 mmap 参数详解

参数 取值 说明
addr NULL 由内核选择虚拟地址,避免地址冲突
length finfo.smem_len 必须与帧缓冲物理内存大小一致
prot PROT_READ | PROT_WRITE 允许读写访问;只读映射无法更新屏幕
flags MAP_SHARED 必须使用共享映射,写操作直接同步到物理内存
fd fb_fd 已打开的 Framebuffer 设备文件描述符
offset 0 通常为 0;多缓冲时可指定偏移访问不同页
MAP_SHARED vs MAP_PRIVATE: 帧缓冲映射必须使用 MAP_SHARED。如果使用 MAP_PRIVATE,写操作会触发写时复制(Copy-on-Write),数据只停留在匿名页面而不会同步到帧缓冲物理内存,屏幕不会产生任何变化。

4.5 解除映射 (munmap)

当程序不再需要访问帧缓冲时(例如程序退出前、切换显示模式前),必须调用 munmap 解除映射,释放用户空间的虚拟地址资源。

// 解除映射
if (munmap(fb_ptr, screensize) < 0) {
    perror("munmap failed");
}

// 关闭设备文件
close(fb_fd);

4.5.1 munmap 注意事项

  • 参数一致性: munmap 的 addr 和 length 必须与 mmap 时完全一致。如果只解除部分映射,需确保长度对齐到页面边界(通常 4096 字节)。
  • 顺序要求: 先 munmap 再 close。如果先关闭文件描述符再解除映射,在 Linux 上通常仍可正常工作,但 POSIX 标准建议先解除映射再关闭 fd。
  • 隐式解除: 进程退出时内核会自动解除所有映射,但显式调用 munmap 是良好的资源管理习惯,尤其在长时间运行的程序中。

4.6 完整代码示例

以下代码整合了打开设备、获取参数、mmap 映射、像素填充和解除映射的完整流程:

#include <stdio.h>
#include <stdlib.h>
#include <fcntl.h>
#include <unistd.h>
#include <sys/ioctl.h>
#include <sys/mman.h>
#include <linux/fb.h>

int main() {
    int fb_fd;
    struct fb_var_screeninfo vinfo;
    struct fb_fix_screeninfo finfo;
    void *fb_ptr = NULL;
    size_t screensize;

    // 1. 打开设备
    fb_fd = open("/dev/fb0", O_RDWR);
    if (fb_fd < 0) { perror("open"); return 1; }

    // 2. 获取屏幕参数
    if (ioctl(fb_fd, FBIOGET_VSCREENINFO, &vinfo) < 0) { perror("ioctl var"); goto cleanup; }
    if (ioctl(fb_fd, FBIOGET_FSCREENINFO, &finfo) < 0)  { perror("ioctl fix"); goto cleanup; }

    screensize = finfo.smem_len;

    // 3. mmap 映射
    fb_ptr = mmap(NULL, screensize, PROT_READ | PROT_WRITE, MAP_SHARED, fb_fd, 0);
    if (fb_ptr == MAP_FAILED) { perror("mmap"); goto cleanup; }

    printf("Framebuffer info: %dx%d, %d bpp, %zu bytes mapped\n",
           vinfo.xres, vinfo.yres, vinfo.bits_per_pixel, screensize);

    // 4. 示例:将屏幕填充为蓝色(假设 32bpp ARGB 格式)
    // 注意:实际颜色偏移需根据 vinfo.red/green/blue.offset 确定
    unsigned int *pixel = (unsigned int *)fb_ptr;
    for (int i = 0; i < screensize / 4; i++) {
        pixel[i] = 0x000000FF;  // 蓝色(B 通道在低 8 位)
    }

    // 5. 解除映射
    if (munmap(fb_ptr, screensize) < 0) { perror("munmap"); }

cleanup:
    close(fb_fd);
    return 0;
}
调试技巧: 在 mmap 之后,可以使用 /proc/<pid>/maps 查看进程的虚拟内存映射,确认帧缓冲映射的起始地址和权限。例如:cat /proc/$(pgrep my_fb_app)/maps | grep fb。

4.7 常见错误与排查

错误现象 可能原因 解决方案
mmap: Permission denied 文件描述符以 O_RDONLY 打开,或 MAP_SHARED 与 PROT_WRITE 冲突 使用 O_RDWR 打开设备
mmap: Invalid argument length 为 0,或 offset 不是页面大小(4096)的整数倍 确保 smem_len > 0,offset 使用 0
ioctl: Inappropriate ioctl for device 设备文件不是 Framebuffer 设备 检查 /dev/fb0 是否存在,或使用 fbset -i 验证
屏幕无变化但无错误 使用了 MAP_PRIVATE,或颜色格式/偏移错误 确认 flags 为 MAP_SHARED;检查 vinfo 中的颜色位域
写入后屏幕花屏 line_length 与 xres*bpp/8 不一致,写入越界 使用 line_length 作为每行步长,而非 xres * (bpp/8)
mmap 映射错误排查决策树 mmap 返回 MAP_FAILED 检查 errno 值 EACCES: 权限不足 检查 O_RDWR 和 video 组 EINVAL: 参数无效 检查 length/offset/flag ENODEV: 不支持 mmap 检查内核 fb 驱动