3. fbdev 注册与注销:register_framebuffer() 与 unregister_framebuffer() 的调用流程与内核接口
在 Linux 内核显示子系统中,register_framebuffer() 与 unregister_framebuffer() 是 fbdev 兼容层最核心的两个接口函数。它们负责将帧缓冲设备注册到内核框架,并在驱动卸载时完成资源回收。本章将深入分析这两个函数的调用流程、内核接口交互以及关键数据结构的变化。
3.1 register_framebuffer() 注册流程
register_framebuffer() 函数位于 drivers/video/fbdev/core/fbmem.c,其核心任务是将一个 struct fb_info 实例注册到系统,并创建对应的字符设备节点 /dev/fbX。整个注册流程分为三个阶段:前置检查、设备注册、用户空间通知。
关键数据结构:
struct fb_info 是 fbdev 驱动的核心描述符,包含帧缓冲内存、可变/固定参数、操作函数表、设备指针等。驱动必须正确初始化该结构体后才能调用注册函数。
注册前置条件: 调用
register_framebuffer() 前,驱动必须完成 fb_info 的 var、fix、fbops、screen_base、screen_size 等字段的初始化,并调用 fb_alloc_cmap() 分配颜色映射表。
3.2 注册流程关键步骤详解
下表列出了 register_framebuffer() 内部调用的核心子函数及其作用:
| 调用顺序 | 子函数 | 功能描述 | 返回值/影响 |
|---|---|---|---|
| 1 | fb_info->node = -1 |
初始化节点编号为无效值 | 后续分配有效编号 |
| 2 | fb_find_next_free_index() |
查找全局空闲的 fb 编号 | 返回 0~31 的整数 |
| 3 | device_register() |
将 fb_info->dev 注册到设备模型 | 创建 sysfs 条目 |
| 4 | fb_create_device() |
创建字符设备 /dev/fbX | 生成设备节点 |
| 5 | fb_notifier_call_chain() |
发送 FB_EVENT_FB_REGISTERED 事件 | 通知其他子系统 |
| 6 | kobject_uevent() |
发送 uevent 到用户空间 | 触发 udev 规则 |
注册失败常见原因:
fb_info指针为 NULL 或关键字段未初始化- 全局 fb 编号耗尽(最大 32 个)
device_register()失败(如设备名称冲突)- 内存不足导致
fb_create_device()失败
3.3 unregister_framebuffer() 注销流程
unregister_framebuffer() 是注册的逆操作,负责从系统中移除帧缓冲设备,释放编号并通知用户空间。其流程与注册对称,但顺序相反。
3.4 注册与注销的内核接口对比
| 对比维度 | register_framebuffer() | unregister_framebuffer() |
|---|---|---|
| 函数原型 | int register_framebuffer(struct fb_info *fb_info) |
int unregister_framebuffer(struct fb_info *fb_info) |
| 返回值 | 成功返回 0,失败返回负错误码 | 成功返回 0,失败返回负错误码 |
| 核心操作 | 分配编号 + 注册设备 + 创建节点 | 移除节点 + 释放编号 + 通知子系统 |
| 锁机制 | 持有 fb_info->lock 及全局 registered_fb 锁 |
持有 fb_info->lock 及全局 registered_fb 锁 |
| 事件通知 | FB_EVENT_FB_REGISTERED | FB_EVENT_FB_UNREGISTERED |
| 用户空间影响 | 创建 /dev/fbX,触发 udev | 删除 /dev/fbX,触发 udev |
| 调用时机 | 驱动 probe 函数末尾 | 驱动 remove 函数开头 |
3.5 驱动中的典型调用模式
以下代码展示了 fbdev 驱动中注册与注销的标准实现模式:
static int myfb_probe(struct platform_device *pdev)
{
struct fb_info *info;
int ret;
// 1. 分配 fb_info 结构体
info = framebuffer_alloc(0, &pdev->dev);
if (!info)
return -ENOMEM;
// 2. 初始化 fb_info 字段
info->var = myfb_var; // 可变参数
info->fix = myfb_fix; // 固定参数
info->fbops = &myfb_ops; // 操作函数表
info->screen_base = dma_alloc_coherent(...);
info->screen_size = MYFB_SIZE;
fb_alloc_cmap(&info->cmap, 256, 0);
// 3. 注册帧缓冲设备
ret = register_framebuffer(info);
if (ret) {
dev_err(&pdev->dev, "register_framebuffer failed: %d\n", ret);
goto err_free_cmap;
}
platform_set_drvdata(pdev, info);
return 0;
err_free_cmap:
fb_dealloc_cmap(&info->cmap);
err_free_info:
framebuffer_release(info);
return ret;
}
static int myfb_remove(struct platform_device *pdev)
{
struct fb_info *info = platform_get_drvdata(pdev);
// 1. 注销帧缓冲设备
unregister_framebuffer(info);
// 2. 清理资源
fb_dealloc_cmap(&info->cmap);
dma_free_coherent(info->device, info->screen_size,
info->screen_base, info->screen_dma);
framebuffer_release(info);
return 0;
}
最佳实践:
- 注册前务必调用
framebuffer_alloc()而非直接kzalloc(),因为前者会正确初始化内部引用计数和锁 - 注销后应立即调用
framebuffer_release()释放fb_info内存 - 在
unregister_framebuffer()返回后,不应再访问fb_info中的任何字段 - 若驱动支持热插拔,需在注册前设置
fb_info->par指向私有数据
3.6 全局数据结构与编号管理
内核维护了一个全局数组 registered_fb[FB_MAX] 来管理所有已注册的帧缓冲设备。FB_MAX 默认为 32,意味着系统最多支持 32 个帧缓冲设备。编号分配采用线性扫描策略:
// fbmem.c 中的核心全局变量
struct fb_info *registered_fb[FB_MAX]; // 已注册设备指针数组
int num_registered_fb; // 已注册设备数量
struct mutex registration_lock; // 注册/注销互斥锁
// 编号分配逻辑(简化)
static int fb_find_next_free_index(void)
{
int i;
for (i = 0; i < FB_MAX; i++) {
if (!registered_fb[i])
return i;
}
return -ENOSPC; // 无可用编号
}
编号冲突风险: 在并发注册场景下,
fb_find_next_free_index() 与 registered_fb[i] = info 之间必须持有 registration_lock,否则可能出现两个驱动分配到相同编号的竞态条件。内核 5.4+ 版本已修复此问题。
3.7 兼容层迁移注意事项
当从原生 fbdev 驱动迁移到 DRM 兼容层时,register_framebuffer() 和 unregister_framebuffer() 的调用模式需要调整:
| 原生 fbdev 驱动 | DRM 兼容层驱动 | 说明 |
|---|---|---|
直接调用 register_framebuffer() |
通过 drm_fbdev_generic_setup() 间接注册 |
兼容层封装了注册逻辑 |
手动管理 fb_info 生命周期 |
由 DRM 框架管理 drm_fb_helper |
减少驱动负担 |
直接操作 registered_fb[] |
通过 drm_fb_helper 接口访问 |
避免直接依赖全局数组 |
| 注销时手动清理所有资源 | DRM 框架自动调用 unregister_framebuffer() |
降低遗漏风险 |
迁移建议: 在兼容层实现中,
register_framebuffer() 和 unregister_framebuffer() 应被封装在 drm_fb_helper 内部,驱动开发者无需直接调用。但理解其内部流程对于调试兼容层问题至关重要。