4. 字符设备核心数据结构
字符设备驱动的核心是内核如何标识、注册和暴露设备给用户空间。本章围绕三个关键要素展开:设备号(dev_t)、设备号分配函数以及字符设备对象(struct cdev)。理解这三者的关系是编写任何字符设备驱动的基础。
核心概念速览: 每个字符设备在内核中由 设备号 唯一标识;设备号由 主设备号(驱动类型)和 次设备号(设备实例)组成;struct cdev 是内核中代表字符设备的对象,必须与设备号绑定后才能向系统注册。
4.1 设备号:dev_t 与主/次设备号
内核使用 dev_t 类型(在 <linux/types.h> 中定义)来保存设备号。它是一个 32 位无符号整数,其中高 12 位表示主设备号,低 20 位表示次设备号。
| 概念 | 位宽 | 取值范围 | 作用 |
|---|---|---|---|
| 主设备号 (major) | 12 位 | 0 ~ 4095 | 标识设备对应的驱动程序 |
| 次设备号 (minor) | 20 位 | 0 ~ 1048575 | 标识由同一驱动管理的具体设备实例 |
内核提供了两个宏来从 dev_t 中提取主、次设备号,以及用主、次设备号组合成 dev_t:
#include <linux/kdev_t.h>
// 从 dev_t 中提取主设备号
unsigned int major = MAJOR(dev);
// 从 dev_t 中提取次设备号
unsigned int minor = MINOR(dev);
// 用主、次设备号组合成 dev_t
dev_t dev = MKDEV(major, minor);
经验提示: 在驱动代码中始终使用
MAJOR() 和 MINOR() 宏来操作设备号,不要直接对 dev_t 进行位运算,以保证代码在不同内核版本间的可移植性。
4.2 设备号分配策略:静态 vs 动态
驱动开发者面临两种设备号分配方式:静态分配(手动指定)和动态分配(内核自动分配)。下表对比了两种方式的优劣:
| 分配方式 | 函数 | 优点 | 缺点 | 适用场景 |
|---|---|---|---|---|
| 静态分配 | register_chrdev_region() |
设备号固定,便于用户空间创建节点 | 容易与其它驱动冲突,需向内核社区申请 | 广泛使用的标准驱动(如 tty、块设备) |
| 动态分配 | alloc_chrdev_region() |
避免冲突,自动分配空闲设备号 | 设备号不固定,需通过 /proc/devices 查看 |
大多数自定义驱动、实验性驱动 |
4.2.1 静态分配:register_chrdev_region()
当驱动需要固定的、已知的设备号时使用。调用前必须确保主设备号未被占用。
#include <linux/fs.h>
int register_chrdev_region(dev_t first, unsigned int count, const char *name);
// 参数说明:
// first - 要分配的设备号范围的起始值
// count - 连续设备号的数量(通常为 1)
// name - 设备名称(出现在 /proc/devices 中)
// 返回值:0 成功,负值失败
使用示例:
dev_t dev;
int major = 240; // 假设 240 未被使用
dev = MKDEV(major, 0);
if (register_chrdev_region(dev, 1, "my_device") < 0) {
pr_err("无法注册设备号 %d\n", major);
return -EBUSY;
}
4.2.2 动态分配:alloc_chrdev_region()
这是推荐的分配方式,内核自动从空闲区域分配设备号。
int alloc_chrdev_region(dev_t *dev, unsigned int baseminor, unsigned int count, const char *name);
// 参数说明:
// dev - 输出参数,指向分配到的设备号起始值
// baseminor - 起始次设备号(通常为 0)
// count - 连续设备号的数量
// name - 设备名称
// 返回值:0 成功,负值失败
使用示例:
dev_t dev;
if (alloc_chrdev_region(&dev, 0, 1, "my_device") < 0) {
pr_err("动态分配设备号失败\n");
return -ENODEV;
}
pr_info("分配到的设备号:主=%d 次=%d\n", MAJOR(dev), MINOR(dev));
重要提醒: 无论使用哪种分配方式,在模块卸载时都必须调用
unregister_chrdev_region(dev, count) 释放设备号,否则会导致资源泄漏。
4.3 字符设备核心结构:struct cdev
struct cdev 是内核中表示字符设备的核心对象。它封装了设备号、文件操作接口以及内核内部链表信息。
4.3.1 struct cdev 结构体定义
#include <linux/cdev.h>
struct cdev {
struct kobject kobj; // 内嵌的内核对象(用于 sysfs)
struct module *owner; // 指向所属模块(通常设为 THIS_MODULE)
const struct file_operations *ops; // 文件操作函数指针表
struct list_head list; // 内核链表节点
dev_t dev; // 设备号
unsigned int count; // 设备号数量
};
4.3.2 cdev 的初始化与注册流程
使用 struct cdev 需要遵循固定的三步流程:
- 分配 cdev 对象:静态定义或使用
cdev_alloc()动态分配 - 初始化 cdev:调用
cdev_init()绑定file_operations - 注册 cdev:调用
cdev_add()将 cdev 与设备号关联并加入系统
// 步骤1:定义 cdev 对象(静态方式)
static struct cdev my_cdev;
// 步骤2:初始化 cdev,绑定文件操作
cdev_init(&my_cdev, &my_fops);
my_cdev.owner = THIS_MODULE;
// 步骤3:将 cdev 添加到系统,关联设备号
if (cdev_add(&my_cdev, dev, 1) < 0) {
pr_err("cdev_add 失败\n");
unregister_chrdev_region(dev, 1);
return -ENODEV;
}
关键顺序: 必须先分配设备号(
alloc_chrdev_region 或 register_chrdev_region),再调用 cdev_add。卸载时顺序相反:先 cdev_del,再 unregister_chrdev_region。
4.4 完整流程:设备号分配与 cdev 注册
下面的 SVG 图展示了从设备号分配到 cdev 注册的完整数据流与函数调用关系:
4.5 完整代码模板:字符设备注册
以下是一个完整的字符设备注册代码框架,整合了动态设备号分配、cdev 初始化和注册:
#include <linux/module.h>
#include <linux/fs.h>
#include <linux/cdev.h>
#include <linux/device.h>
static dev_t dev_num;
static struct cdev my_cdev;
// 文件操作函数(简化)
static int my_open(struct inode *inode, struct file *filp) {
return 0;
}
static struct file_operations my_fops = {
.owner = THIS_MODULE,
.open = my_open,
// ... 其他操作
};
static int __init my_init(void) {
int ret;
// 1. 动态分配设备号
ret = alloc_chrdev_region(&dev_num, 0, 1, "my_device");
if (ret < 0) {
pr_err("设备号分配失败\n");
return ret;
}
pr_info("主设备号: %d\n", MAJOR(dev_num));
// 2. 初始化 cdev
cdev_init(&my_cdev, &my_fops);
my_cdev.owner = THIS_MODULE;
// 3. 注册 cdev
ret = cdev_add(&my_cdev, dev_num, 1);
if (ret < 0) {
pr_err("cdev_add 失败\n");
unregister_chrdev_region(dev_num, 1);
return ret;
}
pr_info("字符设备注册成功\n");
return 0;
}
static void __exit my_exit(void) {
// 卸载顺序:先删除 cdev,再释放设备号
cdev_del(&my_cdev);
unregister_chrdev_region(dev_num, 1);
pr_info("字符设备注销成功\n");
}
module_init(my_init);
module_exit(my_exit);
MODULE_LICENSE("GPL");
MODULE_AUTHOR("Your Name");
MODULE_DESCRIPTION("字符设备注册示例");
4.6 常见错误与避坑指南
- 顺序错误: 先
cdev_add再分配设备号 → 内核崩溃。必须严格遵循:设备号分配 → cdev_init → cdev_add。 - 设备号泄漏: 在
cdev_add失败后忘记调用unregister_chrdev_region→ 设备号被永久占用。 - cdev 重复添加: 对同一个
cdev对象多次调用cdev_add→ 内核链表混乱。 - 主设备号冲突: 使用
register_chrdev_region时指定已被占用的主设备号 → 注册失败。 - 次设备号溢出:
baseminor + count超过 1048575 → 分配失败。
调试技巧: 模块加载后执行
cat /proc/devices 查看已注册的设备号列表;执行 ls -l /dev 查看设备节点。如果设备号已分配但节点未创建,可使用 mknod /dev/mydevice c major minor 手动创建。