4. 设备号注册与注销:动态分配与静态指定

字符设备驱动与内核交互的第一道门槛就是设备号。设备号是内核识别设备的唯一整数标识,由主设备号(Major)和次设备号(Minor)组成。主设备号对应驱动类型,次设备号对应同类型下的不同设备实例。内核提供了三种操作设备号的接口:动态分配、静态指定和注销。

4.1 设备号数据结构与宏

设备号在内核中使用 dev_t 类型(32位无符号整数)表示,其中高12位为主设备号,低20位为次设备号。内核提供了便捷的宏进行转换:

宏 / 函数 作用 示例
MKDEV(major, minor) 将主、次设备号组合为 dev_t MKDEV(240, 0)
MAJOR(dev) 从 dev_t 中提取主设备号 MAJOR(dev)
MINOR(dev) 从 dev_t 中提取次设备号 MINOR(dev)
提示:主设备号范围 0~4095(12位),其中 0~255 为静态预留,256~4095 可供动态分配。次设备号范围 0~1048575(20位)。

4.2 动态分配设备号:alloc_chrdev_region

动态分配是驱动开发中最推荐的方式。驱动无需关心主设备号是否冲突,内核会自动从空闲区域分配一个未使用的主设备号。函数原型如下:

int alloc_chrdev_region(dev_t *dev, unsigned int baseminor,
                        unsigned int count, const char *name);
参数 说明
dev 输出参数,返回分配到的设备号起始值
baseminor 起始次设备号,通常为 0
count 需要连续分配的次设备号数量
name 设备名称,会出现在 /proc/devices 中

典型用法:

dev_t dev_num;
int ret;

ret = alloc_chrdev_region(&dev_num, 0, 1, "my_char_dev");
if (ret < 0) {
    printk(KERN_ERR "Failed to allocate device number\n");
    return ret;
}
printk(KERN_INFO "Allocated major=%d, minor=%d\n",
       MAJOR(dev_num), MINOR(dev_num));
最佳实践:动态分配是内核社区推荐的做法。它避免了主设备号冲突,也方便驱动以模块形式加载/卸载。只有极少数需要固定主设备号的场景(如历史兼容、嵌入式平台固定映射)才使用静态指定。

4.3 静态指定设备号:register_chrdev_region

当驱动需要固定主设备号时(例如与硬件手册或用户空间约定一致),使用静态指定。函数原型:

int register_chrdev_region(dev_t from, unsigned int count,
                           const char *name);
参数 说明
from 指定的起始设备号(需用 MKDEV 构造)
count 连续设备号数量
name 设备名称

典型用法:

dev_t dev_num = MKDEV(240, 0);  // 指定主设备号240,起始次设备号0
int ret;

ret = register_chrdev_region(dev_num, 2, "my_fixed_dev");
if (ret < 0) {
    printk(KERN_ERR "Failed to register device number %d\n", ret);
    return ret;
}
// 成功注册了 240,0 和 240,1 两个设备号
注意:静态指定前必须确认该主设备号未被占用。可以通过 cat /proc/devices 查看当前已注册的设备号。如果指定的主设备号已被使用,register_chrdev_region 会返回 -EBUSY。

4.4 注销设备号:unregister_chrdev_region

在驱动卸载时,必须释放之前申请的设备号,否则下次加载时可能分配失败。函数原型:

void unregister_chrdev_region(dev_t from, unsigned int count);

典型用法:

// 在模块退出函数中
unregister_chrdev_region(dev_num, 1);  // 释放1个设备号
关键原则:注销时使用的 from 和 count 必须与注册时完全一致。如果注册时分配了3个连续设备号,注销时也必须指定 count=3,否则会导致资源泄漏或内核警告。

4.5 设备号生命周期流程

下图展示了从申请到注销的完整设备号生命周期:

设备号生命周期流程图 驱动加载入口 动态/静态? alloc_chrdev_region 内核自动分配空闲主设备号 register_chrdev_region 指定固定主设备号(需确认空闲) 动态 静态 驱动正常运行 unregister_chrdev_region 注销参数必须与注册一致 from + count 完全匹配

4.6 三种接口对比总结

接口 适用场景 主设备号来源 冲突风险 推荐度
alloc_chrdev_region 通用驱动、模块化驱动 内核自动分配 无 ★★★★★
register_chrdev_region 固定设备号、硬件平台约定 开发者指定 需手动检查 ★★★☆☆
unregister_chrdev_region 驱动卸载时必调 — 参数不匹配会泄漏 强制使用

4.7 完整示例:动态分配 + 注销

以下是一个完整的字符设备驱动模板,展示设备号管理的标准流程:

#include <linux/module.h>
#include <linux/fs.h>
#include <linux/device.h>

static dev_t my_dev_num;
static struct class *my_class;
static struct device *my_device;

static int __init my_driver_init(void)
{
    int ret;

    // 1. 动态分配设备号:起始次设备号0,申请1个
    ret = alloc_chrdev_region(&my_dev_num, 0, 1, "my_driver");
    if (ret < 0) {
        pr_err("alloc_chrdev_region failed: %d\n", ret);
        return ret;
    }
    pr_info("Allocated major=%d minor=%d\n",
            MAJOR(my_dev_num), MINOR(my_dev_num));

    // 2. 创建设备类(用于自动生成设备节点)
    my_class = class_create(THIS_MODULE, "my_class");
    if (IS_ERR(my_class)) {
        unregister_chrdev_region(my_dev_num, 1);
        return PTR_ERR(my_class);
    }

    // 3. 创建设备节点 /dev/my_device
    my_device = device_create(my_class, NULL, my_dev_num,
                              NULL, "my_device");
    if (IS_ERR(my_device)) {
        class_destroy(my_class);
        unregister_chrdev_region(my_dev_num, 1);
        return PTR_ERR(my_device);
    }

    return 0;
}

static void __exit my_driver_exit(void)
{
    // 逆序释放:先销毁设备节点,再销毁类,最后注销设备号
    device_destroy(my_class, my_dev_num);
    class_destroy(my_class);
    unregister_chrdev_region(my_dev_num, 1);
    pr_info("Driver unloaded, device number released\n");
}

module_init(my_driver_init);
module_exit(my_driver_exit);

MODULE_LICENSE("GPL");
MODULE_AUTHOR("Embedded Linux Expert");
MODULE_DESCRIPTION("Character device driver with dynamic dev number");
调试技巧:加载模块后执行 cat /proc/devices | grep my_driver 查看分配的主设备号;执行 ls -l /dev/my_device 查看设备节点的主次设备号是否匹配。

4.8 常见错误与避坑指南

  • 忘记注销:驱动卸载时不调用 unregister_chrdev_region,下次加载时动态分配可能返回相同主设备号,但内核会报 device number already registered 警告。
  • 注销参数不匹配:注册时 count=3,注销时 count=1,会导致剩余2个设备号泄漏,内核无法再分配给其他驱动。
  • 静态指定冲突:使用 register_chrdev_region 时未检查 /proc/devices,导致 -EBUSY 错误。建议在模块参数中允许用户指定主设备号,并回退到动态分配。
  • 主设备号溢出:手动指定主设备号大于 4095 时,MKDEV 会截断高20位,导致实际注册的设备号与预期不符。
内核版本差异:在 Linux 5.x 及以上内核中,class_create 已不再接收 THIS_MODULE 参数(仅需设备名)。请根据实际内核版本调整代码。本示例基于 4.x/5.x 通用写法。