3. 设备号管理:主设备号与次设备号、静态分配与动态分配

在 Linux 字符设备驱动中,设备号是内核与用户空间识别设备实例的唯一标识。设备号由 主设备号(Major Number) 和 次设备号(Minor Number) 两部分组成,分别用于定位驱动程序和区分同一驱动下的不同设备实例。本章将深入剖析设备号的构成、分配策略以及核心 API alloc_chrdev_region 的实现机制。

3.1 设备号结构:主设备号与次设备号

设备号在内核中定义为 dev_t 类型(32 位无符号整数)。高 12 位表示主设备号,低 20 位表示次设备号。主设备号用于匹配驱动程序,次设备号用于区分同一驱动下的多个设备节点。

设备号 dev_t 位布局(32 位) 主设备号 (Major) 12 位 (0-4095) 次设备号 (Minor) 20 位 (0-1048575) bit 31 - bit 20 bit 19 - bit 0 MAJOR(dev) 宏提取 MINOR(dev) 宏提取
内核宏操作:使用 MAJOR(dev) 和 MINOR(dev) 宏从 dev_t 中提取主、次设备号;使用 MKDEV(major, minor) 宏将主、次设备号组合为 dev_t。

3.2 设备号分配策略:静态分配 vs 动态分配

Linux 内核提供两种设备号分配方式,开发者需根据项目场景选择:

对比维度 静态分配 动态分配
分配方式 手动指定主设备号,调用 register_chrdev_region 内核自动分配未使用的主设备号,调用 alloc_chrdev_region
主设备号来源 开发者从 /proc/devices 或内核文档中选取未占用的编号 内核动态查找空闲区域并分配
优点 设备节点可预创建,设备号固定不变 避免主设备号冲突,适合广泛分发的驱动
缺点 可能与其他驱动冲突,需手动管理编号 设备号不固定,需通过 /sys 或 udev 动态创建设备节点
适用场景 嵌入式板级支持包、固定硬件平台 通用驱动、内核主线驱动、可热插拔设备
工程建议:新开发的字符设备驱动应优先使用 动态分配。静态分配仅用于与旧版用户空间工具兼容或硬件平台完全固定的场景。

3.3 核心 API:alloc_chrdev_region 函数

alloc_chrdev_region 是动态分配设备号的核心函数,原型定义在 <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,失败返回负的错误码(如 -EINVAL、-EBUSY)。

alloc_chrdev_region 动态分配流程 驱动入口函数调用 alloc_chrdev_region 内核遍历已注册设备号位图,查找空闲主设备号 找到空闲区域? 返回 -EBUSY 分配 dev_t 并注册 通过 *dev 返回起始设备号 返回 0

3.4 典型代码模板:动态分配与释放

以下代码展示了在驱动模块初始化与退出时,使用 alloc_chrdev_region 和 unregister_chrdev_region 管理设备号的标准模式:

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

#define DEVICE_NAME "mychardev"
#define MINOR_COUNT 4

static dev_t dev_num;
static struct cdev my_cdev;

static int __init mydriver_init(void)
{
    int ret;

    /* 动态分配设备号:起始次设备号 0,连续 4 个次设备号 */
    ret = alloc_chrdev_region(&dev_num, 0, MINOR_COUNT, DEVICE_NAME);
    if (ret < 0) {
        pr_err("Failed to allocate chrdev region\n");
        return ret;
    }

    pr_info("Allocated major=%d, minor=%d\n",
            MAJOR(dev_num), MINOR(dev_num));

    /* 后续:cdev_init + cdev_add */
    return 0;
}

static void __exit mydriver_exit(void)
{
    /* 释放设备号区域 */
    unregister_chrdev_region(dev_num, MINOR_COUNT);
    pr_info("Device region unregistered\n");
}

module_init(mydriver_init);
module_exit(mydriver_exit);
常见错误:
  • 忘记检查 alloc_chrdev_region 的返回值,导致后续 cdev_add 使用无效设备号。
  • 释放时 count 参数与分配时不一致,导致设备号泄漏或部分未释放。
  • 在 cdev_add 之前就暴露设备节点给用户空间,导致打开设备时驱动未就绪。

3.5 查看已分配设备号:/proc/devices

设备号分配后,可通过 /proc/devices 文件查看当前系统中所有已注册的字符设备和块设备的主设备号:

$ cat /proc/devices
Character devices:
  1 mem
  4 /dev/vc/0
  4 tty
  4 ttyS
  5 /dev/tty
  5 /dev/console
  5 /dev/ptmx
  7 vcs
 10 misc
 13 input
 29 fb
 89 i2c
 90 mtd
116 alsa
128 ptm
136 pts
180 usb
189 usb_device
...
 249 mychardev    <-- 动态分配的主设备号 249

动态分配时,主设备号由内核从 240~255 或更大范围中选取(取决于内核配置和已占用情况)。驱动开发者不应硬编码该数值,而应通过 MAJOR(dev_num) 在运行时获取。

3.6 设备号管理最佳实践

  • 优先动态分配:所有新驱动默认使用 alloc_chrdev_region,避免主设备号冲突。
  • 次设备号规划:为每个物理通道或子设备分配独立的次设备号,便于用户空间精准访问。
  • 错误路径清理:在驱动初始化失败时,确保已分配的设备号被正确释放。
  • 配合 udev:动态分配的设备号需配合 udev 规则自动创建设备节点,或通过 sysfs 导出设备号信息。
  • 调试检查:使用 cat /proc/devices 确认驱动是否成功注册,以及主设备号是否与预期一致。
内核文档参考:更多关于设备号管理的细节可查阅内核源码中的 Documentation/admin-guide/devices.txt 以及 include/linux/fs.h 中的相关宏定义。