3. 设备号管理:主设备号与次设备号

在 Linux 字符设备驱动中,设备号是内核识别和访问设备的唯一标识。每个字符设备在系统中都对应一个 dev_t 类型的设备号,该类型是一个 32 位无符号整数,其中高 12 位表示主设备号,低 20 位表示次设备号。

核心概念:主设备号用于驱动与设备的绑定,次设备号用于区分同一驱动下的不同设备实例。

3.1 主设备号与次设备号

属性 主设备号 (Major) 次设备号 (Minor)
位数 12 位 (0~4095) 20 位 (0~1048575)
作用 标识设备对应的驱动程序 标识同一驱动下的具体设备实例
分配方式 静态分配或动态分配 通常由驱动开发者规划
内核查找 通过主设备号找到 cdev_map 传递给驱动 open 方法区分设备

内核通过 MAJOR(dev_t) 和 MINOR(dev_t) 宏从设备号中提取主、次设备号,使用 MKDEV(major, minor) 宏将两者合并为 dev_t。

#include <linux/kdev_t.h>

dev_t dev = MKDEV(250, 0);       // 主设备号 250,次设备号 0
int major = MAJOR(dev);          // 返回 250
int minor = MINOR(dev);          // 返回 0
提示:主设备号 0 和 1 通常保留给系统使用,2~255 可用于静态分配,256 以上常用于动态分配。

3.2 设备号分配策略对比

内核提供两种设备号分配方式:静态分配和动态分配。开发者需要根据项目场景选择合适的策略。

对比维度 静态分配 动态分配
函数 register_chrdev_region() alloc_chrdev_region()
主设备号来源 开发者手动指定 内核自动分配
优点 设备号固定,用户空间可预知 避免主设备号冲突
缺点 可能与其他驱动冲突 设备号不固定,需动态创建设备节点
适用场景 标准设备、嵌入式固定硬件 通用驱动、开源模块
设备号分配策略决策流程 开始分配设备号 固定硬件? 是 静态分配 register_chrdev_region 否 动态分配 alloc_chrdev_region cdev_init + cdev_add 绑定设备号与文件操作 设备注册完成

3.3 静态分配:register_chrdev_region

静态分配要求开发者预先知道可用的主设备号,适用于嵌入式平台中硬件设备固定的场景。

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

#define MY_MAJOR  250
#define MY_MINOR  0
#define MY_COUNT  4   // 分配 4 个次设备号

dev_t devno;
int ret;

devno = MKDEV(MY_MAJOR, MY_MINOR);
ret = register_chrdev_region(devno, MY_COUNT, "my_device");
if (ret < 0) {
    printk(KERN_ERR "Failed to register chrdev region\n");
    return ret;
}
注意:静态分配前应检查 /proc/devices 确认主设备号未被占用。若冲突,驱动加载将失败。

3.4 动态分配:alloc_chrdev_region

动态分配由内核自动选择一个未使用的主设备号,避免冲突,是推荐的做法,尤其适用于可加载模块。

dev_t devno;
int ret;

// 动态分配:次设备号从 0 开始,连续 4 个
ret = alloc_chrdev_region(&devno, 0, 4, "my_device");
if (ret < 0) {
    printk(KERN_ERR "Failed to alloc chrdev region\n");
    return ret;
}

printk(KERN_INFO "Allocated major=%d minor=%d\n",
       MAJOR(devno), MINOR(devno));

动态分配后,驱动应通过 MAJOR(devno) 获取分配的主设备号,并据此在 /dev 下创建设备节点(通常配合 udev 或 mdev 自动完成)。

3.5 设备号释放

无论采用哪种分配方式,在驱动卸载时都必须释放设备号,否则会导致资源泄漏。

void cleanup_module(void)
{
    // 释放设备号区域
    unregister_chrdev_region(devno, 4);
}

3.6 两种分配方式完整对比表

操作 静态分配 动态分配
分配函数 register_chrdev_region(devno, count, name) alloc_chrdev_region(&devno, minor, count, name)
主设备号 手动指定 内核自动分配
返回值 0 成功,负值失败 0 成功,负值失败
释放函数 unregister_chrdev_region(devno, count)
设备节点创建 可手动 mknod 或 udev 规则 依赖 udev 自动创建
推荐度 低(仅特殊场景) 高(通用驱动首选)
设备号生命周期管理 阶段1:分配设备号 register / alloc 阶段2:注册cdev cdev_init + cdev_add 阶段3:创建设备节点 mknod / udev 阶段4:驱动运行 open/read/write/ioctl 阶段5:卸载驱动 cdev_del 阶段6:释放设备号 unregister_chrdev_region 设备号管理贯穿驱动整个生命周期:分配 → 注册 → 运行 → 卸载 → 释放 任何阶段遗漏释放操作都会导致内核资源泄漏

3.7 实践建议与常见错误

  • 优先使用动态分配:除非有明确的硬件平台约束,否则始终使用 alloc_chrdev_region。
  • 检查返回值:设备号分配可能因资源耗尽而失败,务必检查返回值。
  • 匹配分配与释放:unregister_chrdev_region 的 count 参数必须与分配时一致。
  • 避免主设备号硬编码:动态分配后通过 MAJOR(devno) 获取,不要写死。
  • 设备节点自动创建:配合 udev 规则或 class_create + device_create 自动生成节点。
最佳实践:在 module_init 中先分配设备号,再注册 cdev;在 module_exit 中先删除 cdev,再释放设备号。顺序不可颠倒。