4. 设备号注册与注销:alloc_chrdev_region、register_chrdev_region、unregister_chrdev_region

在 Linux 字符设备驱动开发中,设备号是内核与用户空间识别设备的核心标识。设备号由主设备号(Major Number)和次设备号(Minor Number)组成,前者标识设备对应的驱动程序,后者标识由该驱动管理的具体设备实例。本章详细讲解设备号的分配、注册与注销机制,涵盖三种核心 API 的使用场景与差异。

4.1 设备号的数据结构

内核使用 dev_t 类型(32 位无符号整数)表示设备号,其中高 12 位为主设备号,低 20 位为次设备号。操作设备号的宏定义如下:

#include <linux/types.h>   /* dev_t */
#include <linux/kdev_t.h>  /* MAJOR/MINOR/MKDEV */

dev_t devno = MKDEV(major, minor);   /* 由主次号合成 dev_t */
unsigned int major = MAJOR(devno);   /* 从 dev_t 提取主设备号 */
unsigned int minor = MINOR(devno);   /* 从 dev_t 提取次设备号 */
提示:主设备号范围 0~4095(12 位),次设备号范围 0~1048575(20 位)。静态分配时需确保主设备号未被占用,否则注册会失败。

4.2 设备号分配策略对比

内核提供两种设备号分配方式:静态分配(register_chrdev_region)和动态分配(alloc_chrdev_region)。下表从多个维度对比两者的差异:

对比维度 静态分配(register_chrdev_region) 动态分配(alloc_chrdev_region)
主设备号来源 开发者手动指定(需确保唯一) 内核自动分配(避免冲突)
适用场景 已知固定主设备号(如 10、89) 通用驱动、模块化开发、避免冲突
注册前需检查 需查询 /proc/devices 确认空闲 无需检查,内核保证唯一
次设备号范围 由 count 参数指定连续范围 由 count 参数指定连续范围
推荐度 不推荐(除非有特殊约定) 强烈推荐(现代驱动标准做法)

4.3 动态分配:alloc_chrdev_region

alloc_chrdev_region 是内核推荐的设备号分配方式,由内核自动选择一个未被使用的主设备号,并返回起始设备号。函数原型如下:

#include <linux/fs.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)。

典型用法:

dev_t dev_num;
int ret;

ret = alloc_chrdev_region(&dev_num, 0, 1, "my_char_dev");
if (ret < 0) {
    pr_err("Failed to allocate device number\n");
    return ret;
}
pr_info("Allocated device number: major=%d, minor=%d\n",
        MAJOR(dev_num), MINOR(dev_num));
注意:动态分配的主设备号每次加载模块时可能不同,因此用户空间需要通过 /sys/class/ 或 udev 自动创建设备节点,而非硬编码 mknod。

4.4 静态分配:register_chrdev_region

register_chrdev_region 用于开发者已知且确认空闲的主设备号场景。函数原型如下:

int register_chrdev_region(dev_t from, unsigned int count, const char *name);

参数说明:

  • from:起始设备号(由 MKDEV(major, minor) 生成)。
  • count:连续分配的次设备号数量。
  • name:设备名称。

典型用法:

#define MY_MAJOR 240
#define MY_MINOR 0
#define DEVICE_COUNT 4

dev_t dev_num = MKDEV(MY_MAJOR, MY_MINOR);
int ret;

ret = register_chrdev_region(dev_num, DEVICE_COUNT, "my_static_dev");
if (ret < 0) {
    pr_err("Failed to register device number %d\n", MY_MAJOR);
    return ret;
}
风险提示:静态分配的主设备号可能与内核其他驱动或后续加载的模块冲突。建议在开发阶段先通过 cat /proc/devices 确认目标主设备号未被占用。

4.5 设备号注销:unregister_chrdev_region

无论使用哪种分配方式,在模块卸载时都必须调用 unregister_chrdev_region 释放设备号。函数原型如下:

void unregister_chrdev_region(dev_t from, unsigned int count);

参数说明:

  • from:与分配时相同的起始设备号。
  • count:与分配时相同的次设备号数量。

典型用法:

/* 在模块退出函数中 */
unregister_chrdev_region(dev_num, DEVICE_COUNT);
重要:设备号注销必须在所有设备文件被关闭、所有内核资源释放之后进行。如果设备号被提前注销,正在进行的文件操作将导致内核崩溃或不可预知的行为。

4.6 设备号生命周期流程图

下图展示了从设备号分配、注册到注销的完整生命周期,以及各阶段与内核模块加载/卸载的对应关系:

设备号生命周期流程图 模块加载 alloc_chrdev_region register_chrdev_region 设备节点创建 模块卸载 unregister_chrdev_region 关键说明 ① 模块加载后,首先分配设备号。 ② 分配方式可选动态或静态。 ③ 注册设备号后,内核才能识别该设备。 ④ 设备节点创建由 udev 或手动 mknod 完成。 ⑤ 模块卸载前必须先注销设备号。 ⑥ 注销后设备号可被其他驱动重用。 动态 vs 静态选择建议: • 动态分配:推荐,避免冲突。 • 静态分配:仅用于固定主设备号场景。 • 注销时 count 必须与分配时一致。 • 设备号释放后不可再访问设备文件。

4.7 完整代码示例:动态分配与注销

以下是一个完整的字符设备驱动框架,展示设备号分配、注册与注销的标准流程:

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

#define DEVICE_NAME "demo_char"
#define MINOR_COUNT 1

static dev_t dev_num;

static int __init demo_init(void)
{
    int ret;

    /* 步骤 1:动态分配设备号 */
    ret = alloc_chrdev_region(&dev_num, 0, MINOR_COUNT, DEVICE_NAME);
    if (ret < 0) {
        pr_err("alloc_chrdev_region failed: %d\n", ret);
        return ret;
    }
    pr_info("Allocated major=%d, minor=%d\n",
            MAJOR(dev_num), MINOR(dev_num));

    /* 步骤 2:后续注册 cdev、创建 class/device 等 */
    /* ... */

    return 0;
}

static void __exit demo_exit(void)
{
    /* 步骤 3:注销设备号 */
    unregister_chrdev_region(dev_num, MINOR_COUNT);
    pr_info("Device number unregistered\n");
}

module_init(demo_init);
module_exit(demo_exit);

MODULE_LICENSE("GPL");
MODULE_AUTHOR("Linux Driver Expert");
MODULE_DESCRIPTION("Demo: alloc_chrdev_region & unregister_chrdev_region");

4.8 常见错误与调试

错误场景 错误码 原因与解决方案
动态分配失败 -ENOMEM 内核内存不足,检查系统资源或减少 count 值。
静态分配冲突 -EBUSY 主设备号已被占用,改用动态分配或更换主设备号。
注销时 count 不匹配 无错误码(内核警告) 注销的 count 必须与分配时完全一致,否则会释放错误范围。
设备号未分配就注册 内核崩溃 确保在 cdev_init 前已成功分配设备号。
调试技巧:使用 cat /proc/devices 查看当前系统已注册的设备号列表;使用 ls -l /dev/ 查看设备节点的主次设备号。动态分配后,建议将主设备号通过 pr_info 打印到内核日志,便于用户空间创建设备节点。

4.9 本章小结

  • 设备号由主设备号(12 位)和次设备号(20 位)组成,使用 dev_t 类型存储。
  • 动态分配(alloc_chrdev_region)是现代驱动的标准做法,避免主设备号冲突。
  • 静态分配(register_chrdev_region)仅用于已知固定主设备号的特殊场景。
  • 设备号注销(unregister_chrdev_region)必须在模块卸载时调用,且参数与分配时一致。
  • 设备号的生命周期与模块加载/卸载严格对应,错误的顺序会导致内核崩溃或资源泄漏。