4. 设备号管理
在 Linux 字符设备驱动中,设备号是内核与用户空间识别设备的唯一令牌。设备号由主设备号(major)和次设备号(minor)组成,内核通过 dev_t 类型(32 位)管理。本章深入剖析设备号的静态分配与动态分配机制,以及核心 API alloc_chrdev_region 与 register_chrdev_region 的实战用法。
4.1 设备号结构
设备号在内核中定义为 dev_t,高 12 位为主设备号,低 20 位为次设备号。主设备号标识设备驱动类型,次设备号标识同一驱动下的不同设备实例。
| 字段 | 位宽 | 范围 | 说明 |
|---|---|---|---|
| 主设备号(major) | 12 位 | 0 ~ 4095 | 标识驱动类型,由内核统一管理 |
| 次设备号(minor) | 20 位 | 0 ~ 1048575 | 标识同一驱动下的具体设备实例 |
提示:内核源码中通过
MAJOR(dev) 和 MINOR(dev) 宏从 dev_t 中提取主、次设备号,使用 MKDEV(major, minor) 宏组合设备号。
4.2 静态分配 vs 动态分配
设备号分配策略分为静态分配和动态分配两种,各有适用场景。下表对比了两种策略的核心差异:
| 对比维度 | 静态分配 | 动态分配 |
|---|---|---|
| 分配方式 | 驱动开发者手动指定主设备号 | 内核自动分配未使用的主设备号 |
| 核心 API | register_chrdev_region |
alloc_chrdev_region |
| 主设备号来源 | 从 /proc/devices 或内核文档中预知 |
由内核动态生成,驱动无需预知 |
| 设备节点创建 | 需手动 mknod 或提前约定主设备号 |
可通过 udev 自动创建设备节点 |
| 冲突风险 | 高,多个驱动可能使用相同主设备号 | 低,内核保证唯一性 |
| 推荐场景 | 嵌入式平台、固定硬件、内核内置驱动 | 通用驱动、可热插拔设备、模块化驱动 |
注意:静态分配的主设备号需要向 Linux 内核社区申请并登记在
Documentation/admin-guide/devices.txt 中,否则可能与其他驱动冲突。动态分配是当前驱动开发的主流实践。
4.3 核心 API 详解
4.3.1 register_chrdev_region — 静态分配
函数原型:
int register_chrdev_region(dev_t from, unsigned count, const char *name);
- from:起始设备号,通常使用
MKDEV(major, minor)构造。 - count:连续分配的次设备号数量。
- name:设备名称,显示在
/proc/devices中。 - 返回值:成功返回 0,失败返回负的错误码。
使用示例:
#include <linux/fs.h>
#include <linux/device.h>
#define MY_MAJOR 240
#define MY_MINOR 0
#define DEVICE_NAME "my_static_dev"
static dev_t dev_num;
static int __init my_init(void)
{
dev_num = MKDEV(MY_MAJOR, MY_MINOR);
int ret = register_chrdev_region(dev_num, 1, DEVICE_NAME);
if (ret < 0) {
pr_err("Failed to register device number: %d\n", ret);
return ret;
}
pr_info("Static device registered: major=%d, minor=%d\n",
MAJOR(dev_num), MINOR(dev_num));
return 0;
}
static void __exit my_exit(void)
{
unregister_chrdev_region(dev_num, 1);
pr_info("Device number unregistered\n");
}
module_init(my_init);
module_exit(my_exit);
4.3.2 alloc_chrdev_region — 动态分配
函数原型:
int alloc_chrdev_region(dev_t *dev, unsigned baseminor, unsigned count, const char *name);
- dev:输出参数,返回分配到的起始设备号。
- baseminor:起始次设备号,通常为 0。
- count:连续分配的次设备号数量。
- name:设备名称。
- 返回值:成功返回 0,失败返回负的错误码。
使用示例:
#include <linux/fs.h>
#include <linux/device.h>
#define DEVICE_NAME "my_dynamic_dev"
static dev_t dev_num;
static int __init my_init(void)
{
int ret = alloc_chrdev_region(&dev_num, 0, 1, DEVICE_NAME);
if (ret < 0) {
pr_err("Failed to allocate device number: %d\n", ret);
return ret;
}
pr_info("Dynamic device allocated: major=%d, minor=%d\n",
MAJOR(dev_num), MINOR(dev_num));
return 0;
}
static void __exit my_exit(void)
{
unregister_chrdev_region(dev_num, 1);
pr_info("Device number released\n");
}
module_init(my_init);
module_exit(my_exit);
关键区别:
register_chrdev_region 需要驱动预先知道主设备号,而 alloc_chrdev_region 由内核动态分配,驱动只需提供次设备号起始值和数量。动态分配是现代驱动开发的首选方式。
4.4 设备号分配流程对比图
下图展示了静态分配与动态分配在内核中的完整流程:
4.5 设备号释放
无论使用哪种分配方式,在驱动卸载时都必须释放设备号,使用统一的 API:
void unregister_chrdev_region(dev_t from, unsigned count);
- from:与分配时相同的起始设备号。
- count:与分配时相同的次设备号数量。
重要:设备号释放必须在
cdev_del 之后、class_destroy 之前调用,否则可能导致内核访问已释放的资源。典型顺序为:cdev_del → device_destroy → class_destroy → unregister_chrdev_region。
4.6 实战选择建议
| 场景 | 推荐策略 | 原因 |
|---|---|---|
| 嵌入式固定硬件平台 | 静态分配 | 主设备号固定,便于脚本和工具链集成 |
| 通用 Linux 驱动模块 | 动态分配 | 避免冲突,支持热插拔,udev 自动管理 |
| 内核主线驱动 | 静态分配(需申请) | 社区要求固定主设备号,便于用户空间兼容 |
| 教学实验驱动 | 动态分配 | 简化流程,聚焦驱动核心逻辑 |
最佳实践:在驱动开发初期使用
alloc_chrdev_region 动态分配,通过 dmesg 查看分配到的设备号,配合 udev 规则自动创建设备节点。待驱动稳定后,如需提交内核主线,再申请静态主设备号。
4.7 常见错误与调试
- EINVAL:
register_chrdev_region指定的主设备号超出范围(0~4095)或次设备号超出范围。 - EBUSY:静态分配时指定的主设备号已被其他驱动占用。
- ENOMEM:内核内存不足,无法分配设备号表项。
- 调试方法:使用
cat /proc/devices查看当前已注册的设备号列表,确认分配是否成功。