3. 实现帧缓冲的mmap操作

实现帧缓冲的mmap操作 学习卡片

任务目标

为帧缓冲驱动实现 mmap 文件操作,使用户空间程序能够将内核中的显存(帧缓冲)直接映射到进程地址空间,从而绕过 read/write 系统调用的数据拷贝开销,实现高性能的像素读写。

核心实现

本案例基于 Linux 内核的 struct fb_info 框架,在自定义帧缓冲驱动中注册 fb_ops 时提供 fb_mmap 回调。该回调通过 remap_pfn_range 将物理连续的帧缓冲内存映射到用户空间。关键点在于:帧缓冲内存必须使用 kmalloc(而非 vmalloc)分配以保证物理连续性,且映射大小不能超过实际分配的内存范围。

#include <linux/module.h>
#include <linux/kernel.h>
#include <linux/fb.h>
#include <linux/mm.h>
#include <linux/slab.h>
#include <linux/platform_device.h>

#define FB_MMAP_SIZE   (800 * 600 * 2)  /* 800x600 RGB565 */

static char *fake_screen;

static int my_fb_mmap(struct fb_info *info, struct vm_area_struct *vma)
{
	unsigned long start = vma->vm_start;
	unsigned long size = vma->vm_end - vma->vm_start;
	unsigned long pfn = virt_to_phys(fake_screen) >> PAGE_SHIFT;

	/* 防止映射超过实际分配大小 */
	if (size > FB_MMAP_SIZE)
		return -EINVAL;

	/* 禁止映射为可执行 */
	vma->vm_flags &= ~VM_EXEC;
	vma->vm_flags |= VM_IO | VM_DONTEXPAND | VM_DONTDUMP;

	/* 映射物理内存到用户空间 */
	if (remap_pfn_range(vma, start, pfn, size, vma->vm_page_prot))
		return -EAGAIN;

	return 0;
}

static struct fb_ops my_fb_ops = {
	.owner = THIS_MODULE,
	.fb_read = fb_sys_read,
	.fb_write = fb_sys_write,
	.fb_fillrect = sys_fillrect,
	.fb_copyarea = sys_copyarea,
	.fb_imageblit = sys_imageblit,
	.fb_mmap = my_fb_mmap,
};

static int my_fb_probe(struct platform_device *pdev)
{
	struct fb_info *fbi;
	int ret;

	/* 使用 kmalloc 保证物理连续,这是 mmap 的前提 */
	fake_screen = kmalloc(FB_MMAP_SIZE, GFP_KERNEL);
	if (!fake_screen)
		return -ENOMEM;

	fbi = framebuffer_alloc(0, &pdev->dev);
	if (!fbi) {
		kfree(fake_screen);
		return -ENOMEM;
	}

	fbi->screen_base = fake_screen;
	fbi->screen_size = FB_MMAP_SIZE;
	fbi->fbops = &my_fb_ops;
	strcpy(fbi->fix.id, "myfb");
	fbi->fix.smem_start = virt_to_phys(fake_screen);
	fbi->fix.smem_len = FB_MMAP_SIZE;
	fbi->fix.type = FB_TYPE_PACKED_PIXELS;
	fbi->fix.visual = FB_VISUAL_TRUECOLOR;
	fbi->fix.line_length = 800 * 2;

	/* 设置 800x600 RGB565 格式 */
	fbi->var.xres = 800;
	fbi->var.yres = 600;
	fbi->var.xres_virtual = 800;
	fbi->var.yres_virtual = 600;
	fbi->var.bits_per_pixel = 16;
	fbi->var.red.offset = 11; fbi->var.red.length = 5;
	fbi->var.green.offset = 5; fbi->var.green.length = 6;
	fbi->var.blue.offset = 0; fbi->var.blue.length = 5;

	ret = register_framebuffer(fbi);
	if (ret) {
		framebuffer_release(fbi);
		kfree(fake_screen);
		return ret;
	}

	platform_set_drvdata(pdev, fbi);
	return 0;
}

static int my_fb_remove(struct platform_device *pdev)
{
	struct fb_info *fbi = platform_get_drvdata(pdev);

	unregister_framebuffer(fbi);
	kfree(fbi->screen_base);
	framebuffer_release(fbi);
	return 0;
}

static struct platform_driver my_fb_driver = {
	.probe = my_fb_probe,
	.remove = my_fb_remove,
	.driver = {
		.name = "myfb",
	},
};

module_platform_driver(my_fb_driver);
MODULE_LICENSE("GPL");

编译与加载

# 编译(内核源码树中)
make -C /lib/modules/$(uname -r)/build M=$(pwd) modules

# 加载驱动
sudo insmod myfb.ko

# 确认设备节点生成
ls /dev/fb*

用户空间验证

#include <fcntl.h>
#include <sys/mman.h>
#include <stdio.h>
#include <string.h>
#include <unistd.h>

int main(void)
{
	int fd = open("/dev/fb0", O_RDWR);
	if (fd < 0) { perror("open"); return 1; }

	/* 映射一页大小的显存 */
	void *map = mmap(NULL, 4096, PROT_READ | PROT_WRITE,
			 MAP_SHARED, fd, 0);
	if (map == MAP_FAILED) { perror("mmap"); return 1; }

	/* 写入像素数据(RGB565 红色) */
	unsigned short *pixel = (unsigned short *)map;
	pixel[0] = 0xF800;  /* 红色 */
	msync(map, 4096, MS_SYNC);

	/* 读回验证 */
	printf("Read back: 0x%04X\n", pixel[0]);

	munmap(map, 4096);
	close(fd);
	return 0;
}
# 编译并运行验证程序
gcc -o fb_mmap_test fb_mmap_test.c
./fb_mmap_test

判定标准

  • 驱动加载后 /dev/fb0 存在,且 dmesg 无错误信息。
  • 验证程序输出 Read back: 0xF800,说明写入的数据通过 mmap 映射成功写入了内核显存并读回一致。
  • 若在真实 LCD 上运行,屏幕左上角像素应显示为红色。

关键说明

  • 帧缓冲内存必须用 kmalloc 分配(物理连续),vmalloc 分配的内存无法通过 remap_pfn_range 直接映射。
  • fb_sys_read/fb_sys_write 依赖 screen_base 指向的内存,与 mmap 共享同一块物理内存,因此读写操作保持一致。
  • 若需支持更大的显存(如 1920x1080x4),需确认物理内存分配是否成功,并相应调整 FB_MMAP_SIZE。