3. 设备号管理:主设备号与次设备号、静态分配与动态分配
在 Linux 字符设备驱动中,设备号是内核与用户空间识别设备实例的唯一标识。设备号由 主设备号(Major Number) 和 次设备号(Minor Number) 两部分组成,分别用于定位驱动程序和区分同一驱动下的不同设备实例。本章将深入剖析设备号的构成、分配策略以及核心 API alloc_chrdev_region 的实现机制。
3.1 设备号结构:主设备号与次设备号
设备号在内核中定义为 dev_t 类型(32 位无符号整数)。高 12 位表示主设备号,低 20 位表示次设备号。主设备号用于匹配驱动程序,次设备号用于区分同一驱动下的多个设备节点。
内核宏操作:使用
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)。
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 中的相关宏定义。