4. 设备号管理:主设备号与次设备号,静态分配与动态分配,alloc_chrdev_region

在 Linux 字符设备驱动中,设备号是内核与用户空间识别设备实例的唯一标识。设备号由 主设备号(Major Number) 和 次设备号(Minor Number) 两部分组成,内核通过 dev_t 类型(32 位)管理:高 12 位为主设备号,低 20 位为次设备号。

核心概念:主设备号对应驱动程序,次设备号对应具体设备实例或硬件通道。同一主设备号下的不同次设备号可代表同一芯片上的多个独立外设接口。

4.1 主设备号与次设备号

属性 主设备号(Major) 次设备号(Minor)
位数 12 位(0–4095) 20 位(0–1048575)
作用 标识设备驱动程序 标识同一驱动下的具体设备
分配方式 静态(手动指定)或动态(内核分配) 通常由驱动开发者规划范围
查看命令 cat /proc/devices ls -l /dev/
典型场景 一个 SPI 控制器驱动占用一个主设备号 SPI 总线上挂载的多个从设备各占一个次设备号
工程经验:在 HAL 驱动移植中,主设备号通常由芯片厂商或内核社区统一分配(如 Documentation/admin-guide/devices.txt),但嵌入式 BSP 开发中更推荐动态分配,避免与已有驱动冲突。

4.2 静态分配 vs 动态分配

设备号的分配策略直接影响驱动的可移植性与维护成本。以下从多个维度对比两种方式:

对比维度 静态分配 动态分配
API 函数 register_chrdev_region() alloc_chrdev_region()
主设备号来源 开发者手动指定(如 200) 内核自动分配(0–4095 中未使用值)
设备节点创建 可预知设备号,提前 mknod 需从 /proc/devices 读取或使用 udev
冲突风险 高(需人工核对 devices.txt) 低(内核保证唯一性)
模块加载顺序 无依赖 需 udev/mdev 配合自动创建设备节点
HAL 移植推荐 仅用于 legacy 或固定硬件平台 ✅ 推荐用于可移植 HAL 驱动
避坑提示:静态分配时若主设备号已被占用,register_chrdev_region() 会返回 -EBUSY。在 HAL 驱动移植中,若目标内核版本升级或设备树变更,静态分配的主设备号可能与其他驱动冲突,导致 probe 失败。

4.3 alloc_chrdev_region 动态分配流程

动态分配是 HAL 驱动移植中的首选方案。内核提供 alloc_chrdev_region() 函数,自动从未使用的主设备号中分配一个空闲值,并返回完整的 dev_t 设备号。

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

int alloc_chrdev_region(dev_t *dev, unsigned int baseminor,
                        unsigned int count, const char *name);
参数 说明
dev 输出参数,指向 dev_t 变量,接收分配的设备号
baseminor 起始次设备号(通常为 0)
count 需要分配的连续次设备号数量
name 设备名称,显示在 /proc/devices 中
返回值:成功返回 0,失败返回负的错误码(如 -ENOMEM)。分配成功后,可通过 MAJOR(dev) 和 MINOR(dev) 宏提取主、次设备号。

4.4 动态分配完整代码模板

以下代码展示在 HAL 驱动 probe 函数中使用 alloc_chrdev_region 的典型模式:

static int hal_driver_probe(struct platform_device *pdev)
{
    dev_t dev_num;
    int ret;

    /* 1. 动态分配设备号:起始次设备号 0,分配 2 个设备节点 */
    ret = alloc_chrdev_region(&dev_num, 0, 2, "hal_i2c");
    if (ret < 0) {
        dev_err(&pdev->dev, "alloc_chrdev_region failed: %d\n", ret);
        return ret;
    }

    /* 2. 提取主设备号并保存到私有数据结构 */
    pdata->major = MAJOR(dev_num);
    pdata->minor_base = MINOR(dev_num);

    /* 3. 初始化 cdev 并添加到内核 */
    cdev_init(&pdata->cdev, &hal_i2c_fops);
    pdata->cdev.owner = THIS_MODULE;
    ret = cdev_add(&pdata->cdev, dev_num, 2);
    if (ret) {
        unregister_chrdev_region(dev_num, 2);
        return ret;
    }

    /* 4. 创建设备类与设备节点(配合 udev/mdev) */
    pdata->class = class_create(THIS_MODULE, "hal_i2c_class");
    device_create(pdata->class, NULL, dev_num, NULL, "hal_i2c_%d", 0);

    dev_info(&pdev->dev, "HAL I2C driver loaded: major=%d\n", pdata->major);
    return 0;
}
HAL 移植要点:动态分配后,必须配合 class_create + device_create 自动创建设备节点,否则用户空间无法通过 /dev/hal_i2c_0 访问。在嵌入式系统中,需确保 udev 或 mdev 已运行。

4.5 设备号分配流程 SVG 架构图

设备号分配流程:静态 vs 动态 静态分配 register_chrdev_region() 手动指定主设备号(如 200) 检查 /proc/devices 是否冲突 冲突 → 返回 -EBUSY 成功 → 固定设备号 手动 mknod 创建设备节点 动态分配 alloc_chrdev_region() 内核自动分配未使用的主设备号 返回 dev_t,提取 MAJOR/MINOR cdev_init + cdev_add 注册 class_create + device_create udev/mdev 自动创建设备节点 HAL 驱动移植建议 动态分配 + udev 自动节点创建,避免主设备号冲突,提升驱动可移植性

4.6 设备号释放与错误处理

在驱动卸载或 probe 失败时,必须释放已分配的设备号,否则会造成资源泄漏。使用 unregister_chrdev_region() 函数:

void unregister_chrdev_region(dev_t from, unsigned int count);

典型错误处理路径:

  • cdev_add 失败:先调用 unregister_chrdev_region 释放设备号,再返回错误码。
  • device_create 失败:需依次销毁已创建的 device、class,最后释放设备号。
  • 模块卸载:先 cdev_del,再 device_destroy + class_destroy,最后 unregister_chrdev_region。
常见错误:忘记释放设备号会导致 /proc/devices 中出现僵尸条目,下次加载驱动时动态分配可能返回相同主设备号,但 cdev 已不存在,造成内核崩溃。

4.7 本章小结

知识点 关键要点
设备号组成 主设备号(12 bit)+ 次设备号(20 bit),dev_t 类型
静态分配 register_chrdev_region,手动指定,冲突风险高
动态分配 alloc_chrdev_region,内核自动分配,推荐用于 HAL 移植
节点创建 动态分配必须配合 class_create + device_create 自动创建
资源释放 unregister_chrdev_region 在错误路径和卸载时调用