3. 显存映射与 mmap 机制:用户空间如何通过 mmap 访问显存
在 Linux FrameBuffer 驱动架构中,显存(FrameBuffer 内存)通常由内核在驱动初始化时分配(通过 dma_alloc_coherent 或 alloc_pages_exact),其物理地址连续且固定。用户空间应用程序无法直接通过物理地址访问硬件资源,必须借助 mmap 系统调用 将内核空间的物理显存映射到用户空间的虚拟地址区域。本章节深入剖析 mmap 机制、物理地址与虚拟地址的转换流程,以及关键实现细节。
3.1 mmap 系统调用与驱动接口
用户空间调用 mmap 时,内核会查找该文件描述符对应的 struct file 操作表,并调用驱动注册的 mmap 方法。FrameBuffer 驱动的 fb_mmap 函数负责将物理显存映射到用户虚拟地址空间。
3.1.1 用户空间调用原型
#include <sys/mman.h>
#include <fcntl.h>
#include <unistd.h>
int fd = open("/dev/fb0", O_RDWR);
if (fd < 0) {
perror("open /dev/fb0 failed");
return -1;
}
// 获取固定信息,得到 smem_len(显存大小)
struct fb_fix_screeninfo fix;
ioctl(fd, FBIOGET_FSCREENINFO, &fix);
// 将显存映射到用户空间
size_t screensize = fix.smem_len;
void *fbp = mmap(NULL, screensize, PROT_READ | PROT_WRITE, MAP_SHARED, fd, 0);
if (fbp == MAP_FAILED) {
perror("mmap failed");
close(fd);
return -1;
}
// 现在 fbp 指向用户空间虚拟地址,可直接读写
// 例如:将屏幕第一个像素设为红色
*((unsigned short *)(fbp)) = 0xF800; // RGB565 红色
// 解除映射
munmap(fbp, screensize);
close(fd);
3.1.2 驱动侧 mmap 实现流程
| 步骤 | 驱动函数 / 操作 | 说明 |
|---|---|---|
| 1 | fb_mmap(struct file *file, struct vm_area_struct *vma) |
内核调用驱动注册的 mmap 方法 |
| 2 | 获取 info->fix.smem_start 和 info->fix.smem_len |
从 fb_info 结构中取出物理地址和大小 |
| 3 | pgprot_noncached(vma->vm_page_prot) |
设置页属性为非缓存(防止 CPU Cache 导致显示不一致) |
| 4 | remap_pfn_range(vma, vma->vm_start, smem_start >> PAGE_SHIFT, size, vma->vm_page_prot) |
核心函数:建立物理页帧号到用户虚拟地址的页表映射 |
| 5 | 返回 0 表示成功 | 用户空间即可通过返回的虚拟地址访问显存 |
remap_pfn_range 是内核提供的核心 API,它接受物理页帧号(PFN,即物理地址右移 PAGE_SHIFT 位)、用户虚拟地址范围、大小和页保护属性,自动完成页表的建立。驱动开发者无需手动操作页表。
3.2 物理地址与虚拟地址转换原理
理解 mmap 映射的本质,需要掌握 物理地址 → 页帧号(PFN)→ 用户虚拟地址 的转换链条。CPU 通过 MMU(内存管理单元)将用户程序发出的虚拟地址转换为物理地址,而 mmap 的核心工作就是预先在页表中插入这些映射条目。
3.2.1 地址转换层级
3.2.2 页表映射细节
当驱动调用 remap_pfn_range 时,内核会遍历用户虚拟地址区间 [vma->vm_start, vma->vm_start + size),对每个页面执行以下操作:
- 计算目标 PFN:
pfn = (smem_start >> PAGE_SHIFT) + (offset >> PAGE_SHIFT),其中 offset 是当前虚拟页相对于映射起始的偏移。 - 分配页表项(PTE): 在进程的页全局目录(PGD)中查找或创建中间页表,最终在页表项中写入物理页帧号和保护位。
- 设置缓存属性: 通过
pgprot_noncached或pgprot_writecombine设置 PTE 的缓存位,确保对显存的写入不会被 CPU Cache 延迟,保证显示一致性。
3.3 双缓冲场景下的 mmap 映射策略
在双缓冲机制中,显存通常被划分为两个缓冲区:前台缓冲区(Front Buffer)和后台缓冲区(Back Buffer)。用户空间通过 mmap 映射整个显存区域,然后通过偏移量访问不同的缓冲区。
3.3.1 双缓冲显存布局
| 缓冲区 | 偏移量(相对 smem_start) | 大小 | 用户空间虚拟地址 |
|---|---|---|---|
| 前台缓冲区 | 0 | screen_size | fbp + 0 |
| 后台缓冲区 | screen_size | screen_size | fbp + screen_size |
应用程序在后台缓冲区完成渲染后,通过 FBIO_WAITFORVSYNC 等待垂直消隐期,然后交换缓冲区指针(或通过 FBIOPAN_DISPLAY 设置显示起始地址),实现无撕裂的画面更新。
3.3.2 用户空间双缓冲访问示例
// 假设 fix.smem_len = 2 * screen_size
void *fbp = mmap(NULL, fix.smem_len, PROT_READ | PROT_WRITE, MAP_SHARED, fd, 0);
// 定义前后台指针
void *front_buffer = fbp;
void *back_buffer = fbp + screen_size;
// 在后台缓冲区绘制一帧
draw_frame(back_buffer);
// 等待 VSync
ioctl(fd, FBIO_WAITFORVSYNC, 0);
// 交换缓冲区:通过 pan_display 设置显示起始地址为后台缓冲区
struct fb_var_screeninfo var;
ioctl(fd, FBIOGET_VSCREENINFO, &var);
var.yoffset = screen_size / var.xres_virtual / (var.bits_per_pixel / 8);
ioctl(fd, FBIOPAN_DISPLAY, &var);
// 交换指针(逻辑交换)
void *tmp = front_buffer;
front_buffer = back_buffer;
back_buffer = tmp;
3.4 mmap 映射的常见问题与优化
- 映射大小不足: 如果用户空间只映射了单个缓冲区大小,而驱动实际分配了双缓冲空间,访问后台缓冲区会导致段错误。解决方案:映射时使用
fix.smem_len作为总大小。 - 缓存一致性问题: 未设置
pgprot_noncached时,CPU 写入可能滞留在 Cache 中。ARM 平台建议使用pgprot_writecombine以兼顾性能与一致性。 - 多进程共享映射: 多个进程可以同时 mmap 同一个 FrameBuffer 设备,但需要用户空间自行同步(如使用信号量或原子操作),驱动不提供并发保护。
- 大页支持: 对于高分辨率显示(如 4K),显存可能达到数十 MB。使用
MAP_HUGETLB标志可以启用大页映射,减少 TLB Miss,提升访问性能。
3.5 本章小结
mmap 机制是用户空间高效访问 FrameBuffer 显存的基石。通过 remap_pfn_range 将物理显存映射到用户虚拟地址空间,应用程序可以直接操作像素数据,避免了系统调用的开销。理解物理地址到虚拟地址的转换流程、页表映射原理以及缓存属性设置,对于编写高性能、无残影的 FrameBuffer 应用程序至关重要。在双缓冲场景下,合理的映射策略和缓冲区切换逻辑是消除画面撕裂的关键。