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() 分配颜色映射表。
register_framebuffer() 注册流程 驱动初始化 struct fb_info register_framebuffer(fb_info) 前置检查:fb_info 有效性、分配空闲编号 设备注册:device_register() + fb_create_device() 用户空间通知:创建 /dev/fbX + uevent 注册完成,返回 0

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() 是注册的逆操作,负责从系统中移除帧缓冲设备,释放编号并通知用户空间。其流程与注册对称,但顺序相反。

unregister_framebuffer() 注销流程 驱动卸载触发 unregister 发送 FB_EVENT_FB_UNREGISTERED 移除字符设备:device_destroy() 释放 fb 编号:fb_release_free_index() 注销完成,返回 0

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 内部,驱动开发者无需直接调用。但理解其内部流程对于调试兼容层问题至关重要。