3. 主设备号与次设备号:设备号分配机制

在 Linux 字符设备驱动中,设备号是内核识别和访问设备的唯一标识。设备号由两部分组成:主设备号(Major Number)和次设备号(Minor Number)。理解设备号的分配机制是驱动开发的基础,直接关系到设备节点的创建、文件操作接口的绑定以及系统资源的有效管理。

3.1 设备号的结构与作用

设备号在内核中使用 dev_t 类型表示,这是一个 32 位无符号整数。其中高 12 位用于主设备号,低 20 位用于次设备号。主设备号标识设备对应的驱动程序,次设备号则由驱动程序内部使用,用于区分同一驱动管理的不同设备实例。

组成部分 位数 取值范围 作用
主设备号 12 bit 0 ~ 4095 标识设备驱动程序,内核通过主设备号找到对应的 file_operations
次设备号 20 bit 0 ~ 1048575 由驱动内部使用,区分不同设备实例或硬件通道
关键理解:主设备号是“驱动索引”,次设备号是“实例索引”。同一个主设备号下的不同次设备号共享同一套 file_operations 操作函数,但可以对应不同的硬件资源或私有数据。

3.2 设备号分配方式对比

Linux 内核提供两种设备号分配方式:静态分配和动态分配。两种方式各有适用场景,开发者在实际项目中需要根据需求权衡选择。

对比维度 静态分配 动态分配
分配函数 register_chrdev_region() alloc_chrdev_region()
主设备号来源 开发者手动指定(需查表避免冲突) 内核自动分配未使用的主设备号
优点 设备节点可预创建,设备号固定不变 避免主设备号冲突,适合广泛分发
缺点 需维护设备号分配表,可能冲突 设备号不固定,需配合 udev 自动创建设备节点
适用场景 嵌入式平台、设备号已约定、无热插拔 通用驱动、模块化开发、需热插拔支持

3.3 静态分配:register_chrdev_region()

静态分配要求驱动开发者明确指定起始主设备号和次设备号范围。内核通过 register_chrdev_region() 函数将指定的设备号区域注册到系统中。

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

#define MY_DEVICE_MAJOR  240
#define MY_DEVICE_MINOR  0
#define MY_DEVICE_COUNT  4

static dev_t my_dev_id;

static int __init my_driver_init(void)
{
    int ret;

    /* 构造设备号:主设备号 240,起始次设备号 0 */
    my_dev_id = MKDEV(MY_DEVICE_MAJOR, MY_DEVICE_MINOR);

    /* 静态分配 4 个设备号:240,0 ~ 240,3 */
    ret = register_chrdev_region(my_dev_id, MY_DEVICE_COUNT, "my_device");
    if (ret < 0) {
        pr_err("Failed to register chrdev region: %d\n", ret);
        return ret;
    }

    pr_info("Static allocated major=%d, minor=%d\n",
            MAJOR(my_dev_id), MINOR(my_dev_id));
    return 0;
}

static void __exit my_driver_exit(void)
{
    unregister_chrdev_region(my_dev_id, MY_DEVICE_COUNT);
    pr_info("Device region unregistered\n");
}

module_init(my_driver_init);
module_exit(my_driver_exit);
静态分配风险:主设备号 240 仅为示例。实际开发中,若未查阅官方设备号分配文档(Documentation/admin-guide/devices.txt),直接使用未预留的主设备号可能导致与已有驱动冲突,造成系统启动失败或设备无法访问。

3.4 动态分配:alloc_chrdev_region()

动态分配由内核自动选择一个未使用的主设备号,驱动只需指定起始次设备号和设备数量。这种方式在现代 Linux 驱动开发中更为推荐。

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

#define MY_DEVICE_BASE_MINOR  0
#define MY_DEVICE_COUNT       4

static dev_t my_dev_id;

static int __init my_driver_init(void)
{
    int ret;

    /* 动态分配 4 个设备号,起始次设备号为 0 */
    ret = alloc_chrdev_region(&my_dev_id, MY_DEVICE_BASE_MINOR,
                              MY_DEVICE_COUNT, "my_device");
    if (ret < 0) {
        pr_err("Failed to alloc chrdev region: %d\n", ret);
        return ret;
    }

    pr_info("Dynamic allocated major=%d, minor=%d\n",
            MAJOR(my_dev_id), MINOR(my_dev_id));
    return 0;
}

static void __exit my_driver_exit(void)
{
    unregister_chrdev_region(my_dev_id, MY_DEVICE_COUNT);
    pr_info("Device region unregistered\n");
}

module_init(my_driver_init);
module_exit(my_driver_exit);
动态分配最佳实践:驱动加载后通过 dmesg 查看内核打印的主设备号,然后使用 mknod /dev/my_device0 c 主设备号 0 手动创建设备节点。更推荐的方式是配合 udev/mdev 自动创建设备节点,这将在后续章节详细讲解。

3.5 设备号分配流程对比图

下面的 SVG 流程图清晰展示了静态分配与动态分配在内核中的完整路径,以及各自的关键步骤差异。

设备号分配流程对比:静态分配 vs 动态分配 静态分配 register_chrdev_region() 开发者手动指定主设备号(如 240) 检查主设备号是否已被占用 注册成功,设备号固定不变 手动 mknod 创建 /dev 节点 动态分配 alloc_chrdev_region() 内核自动搜索空闲主设备号 分配未使用的主设备号 注册成功,主设备号动态生成 udev/mdev 自动创建设备节点 核心差异 静态分配:设备号固定,适合嵌入式平台;动态分配:避免冲突,适合通用驱动

3.6 设备号操作辅助宏

内核提供了一组宏,用于在 dev_t、主设备号、次设备号之间进行转换,驱动开发者应熟练掌握。

宏 / 函数 功能描述 使用示例
MKDEV(major, minor) 将主设备号和次设备号组合为 dev_t dev_t dev = MKDEV(240, 0);
MAJOR(dev) 从 dev_t 中提取主设备号 int major = MAJOR(dev);
MINOR(dev) 从 dev_t 中提取次设备号 int minor = MINOR(dev);

3.7 设备号分配决策树

在实际项目中,选择静态分配还是动态分配需要根据具体场景判断。下面的决策树可以帮助开发者快速做出选择。

设备号分配方式决策树 设备号分配方式? 静态分配 设备号已约定 / 无需热插拔 嵌入式平台 / 系统定制 动态分配 通用驱动 / 需热插拔支持 配合 udev / 模块化开发 提示:动态分配是现代 Linux 驱动开发的首选方式,静态分配仅用于特殊嵌入式场景

3.8 常见问题与避坑指南

  • 主设备号冲突:静态分配时未查阅官方设备号列表,导致与系统已有驱动冲突。解决:使用 cat /proc/devices 查看当前已注册的主设备号。
  • 设备号泄漏:驱动卸载时未调用 unregister_chrdev_region(),导致设备号资源泄漏。解决:在 exit 函数中确保释放所有已注册的设备号。
  • 次设备号溢出:次设备号范围是 0~1048575,若分配数量超过此范围,内核会返回错误。解决:合理规划次设备号使用量,避免一次性分配过多。
  • 动态分配后设备节点缺失:动态分配的主设备号不固定,手动 mknod 无法预知设备号。解决:使用 udev 规则或驱动内部调用 device_create() 自动创建节点。
调试技巧:在驱动初始化成功后,立即打印主设备号和次设备号:pr_info("Allocated dev_t: major=%d, minor=%d\n", MAJOR(dev), MINOR(dev));。通过 dmesg 查看输出,确认设备号分配是否符合预期。

3.9 本章小结