4. cdev 结构体:初始化、注册、注销与设备文件关联
在 Linux 字符设备驱动模型中,struct cdev 是内核用于表示字符设备的核心数据结构。每个字符设备在内核中都必须对应一个 cdev 实例,驱动开发者通过操作该结构体完成设备的注册与注销,并建立设备文件与驱动操作函数的关联。
4.1 cdev 结构体定义
struct cdev 定义在 <linux/cdev.h> 中,其关键字段如下:
| 字段 | 类型 | 说明 |
|---|---|---|
kobj |
struct kobject |
内核对象基类,用于设备模型管理、引用计数与 sysfs 导出 |
owner |
struct module * |
指向所属模块的指针,防止模块在设备打开时被卸载 |
ops |
const struct file_operations * |
指向设备操作函数表的指针,是设备文件与驱动逻辑的桥梁 |
list |
struct list_head |
用于将 cdev 挂入系统全局链表,便于内核查找 |
dev |
dev_t |
设备号(主设备号 + 次设备号),唯一标识该设备 |
count |
unsigned int |
与该 cdev 关联的次设备号数量,决定设备号范围 |
关键理解:
cdev 是内核视角的设备抽象,而 file_operations 是用户态操作(open/read/write)进入内核的入口表。两者通过 ops 指针绑定,驱动的主要工作就是填充这张函数表。
4.2 cdev 初始化
在分配 cdev 实例后,必须对其进行初始化。内核提供两种初始化方式:
4.2.1 静态初始化:cdev_init()
当 cdev 作为结构体成员或全局变量时,使用 cdev_init():
#include <linux/cdev.h>
struct cdev my_cdev;
void cdev_init(struct cdev *cdev, const struct file_operations *fops);
该函数完成以下工作:
- 将
cdev->ops指向传入的fops - 初始化
cdev->kobj,设置其类型为kobj_type_cdev - 初始化
cdev->list链表节点 - 将
cdev->owner置为NULL(需驱动手动设置)
4.2.2 动态分配:cdev_alloc()
当需要动态分配 cdev 实例时,使用 cdev_alloc():
struct cdev *cdev_alloc(void);
该函数在内核堆中分配 struct cdev 并返回指针,同时自动完成初始化。使用后必须通过 cdev_del() 释放。
注意: 使用
cdev_alloc() 时,cdev_del() 会自动释放内存;而使用 cdev_init() 时,cdev_del() 仅注销设备,不释放内存,驱动需自行管理生命周期。
4.3 cdev_add:注册设备
初始化完成后,调用 cdev_add() 将设备注册到内核:
int cdev_add(struct cdev *cdev, dev_t dev, unsigned int count);
| 参数 | 说明 |
|---|---|
cdev |
已初始化的 cdev 结构体指针 |
dev |
设备号,通常由 MKDEV(major, minor) 生成 |
count |
关联的次设备号数量,决定设备号范围 |
返回值: 成功返回 0,失败返回负的错误码(如 -ENOMEM)。
cdev_add() 执行后,内核会将 cdev 加入全局散列表,设备立即可被用户态程序访问。因此,必须在调用 cdev_add() 之前完成所有初始化工作,包括设置 owner 字段。
最佳实践: 在模块初始化函数中,先申请设备号,再初始化 cdev,最后调用 cdev_add()。若 cdev_add() 失败,需回滚已申请的设备号。
4.4 cdev_del:注销设备
在模块卸载或设备移除时,调用 cdev_del() 从内核中注销设备:
void cdev_del(struct cdev *cdev);
该函数执行以下操作:
- 从内核全局设备链表中移除该
cdev - 等待所有正在进行的文件操作(open/read/write)完成
- 若
cdev由cdev_alloc()分配,则释放内存
注意:
cdev_del() 不会自动释放设备号。驱动必须在调用 cdev_del() 后,显式调用 unregister_chrdev_region() 释放设备号。
4.5 设备与文件的关联机制
当用户态程序调用 open("/dev/mydevice", O_RDWR) 时,内核通过以下路径完成设备文件到 cdev 的关联:
- VFS 层: 根据文件路径找到对应的
inode,从inode->i_rdev中提取设备号 - 内核查找: 根据设备号在全局
cdev_map散列表中查找对应的cdev实例 - 绑定操作表: 将
inode->i_cdev指向找到的cdev,并将file->f_op设置为cdev->ops - 调用 open: 若
file_operations中定义了open方法,则调用之
理解要点: 设备文件与驱动的绑定发生在
open() 时刻,而非 cdev_add() 时刻。cdev_add() 只是让内核知道“存在这样一个设备”,而 open() 才真正建立文件实例与驱动函数的连接。
4.6 完整生命周期流程图
下图展示了 cdev 从初始化到注销的完整生命周期,以及设备文件打开时的关联路径:
4.7 完整代码示例
以下代码展示了字符设备驱动中 cdev 的完整生命周期管理:
#include <linux/module.h>
#include <linux/fs.h>
#include <linux/cdev.h>
#include <linux/device.h>
#define DEVICE_NAME "mychardev"
#define CLASS_NAME "mychar"
static int major;
static struct class *myclass = NULL;
static struct device *mydevice = NULL;
static struct cdev my_cdev;
static int my_open(struct inode *inode, struct file *file) {
pr_info("Device opened\n");
return 0;
}
static int my_release(struct inode *inode, struct file *file) {
pr_info("Device closed\n");
return 0;
}
static struct file_operations my_fops = {
.owner = THIS_MODULE,
.open = my_open,
.release = my_release,
};
static int __init my_init(void) {
dev_t dev_num;
int ret;
/* 1. 动态分配设备号 */
ret = alloc_chrdev_region(&dev_num, 0, 1, DEVICE_NAME);
if (ret < 0) {
pr_err("Failed to allocate device number\n");
return ret;
}
major = MAJOR(dev_num);
pr_info("Allocated major number %d\n", major);
/* 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("Failed to add cdev\n");
unregister_chrdev_region(dev_num, 1);
return ret;
}
/* 4. 创建设备类与设备文件(自动生成 /dev/mychardev) */
myclass = class_create(THIS_MODULE, CLASS_NAME);
if (IS_ERR(myclass)) {
pr_err("Failed to create class\n");
cdev_del(&my_cdev);
unregister_chrdev_region(dev_num, 1);
return PTR_ERR(myclass);
}
mydevice = device_create(myclass, NULL, dev_num, NULL, DEVICE_NAME);
if (IS_ERR(mydevice)) {
pr_err("Failed to create device\n");
class_destroy(myclass);
cdev_del(&my_cdev);
unregister_chrdev_region(dev_num, 1);
return PTR_ERR(mydevice);
}
pr_info("Module loaded successfully\n");
return 0;
}
static void __exit my_exit(void) {
dev_t dev_num = MKDEV(major, 0);
/* 按注册逆序释放资源 */
device_destroy(myclass, dev_num);
class_destroy(myclass);
cdev_del(&my_cdev);
unregister_chrdev_region(dev_num, 1);
pr_info("Module unloaded\n");
}
module_init(my_init);
module_exit(my_exit);
MODULE_LICENSE("GPL");
MODULE_AUTHOR("Kernel Expert");
MODULE_DESCRIPTION("cdev lifecycle example");
4.8 常见错误与调试要点
| 错误场景 | 现象 | 根因 | 解决方案 |
|---|---|---|---|
| cdev_add 后立即 open 失败 | open 返回 -ENODEV | 设备号与 cdev 未正确关联 | 检查 cdev_add 的 dev 参数是否与 mknod 的设备号一致 |
| 模块卸载时系统崩溃 | 内核 oops,提示在 cdev 操作中 | cdev_del 时仍有进程持有文件描述符 | 确保所有设备文件已关闭,或使用 try_module_get 机制 |
| open 成功但 read/write 无响应 | 系统调用返回 -EINVAL | file_operations 中未实现对应方法 | 检查 fops 表是否完整,未实现的方法应设为 NULL |
| 设备号泄漏 | 模块反复加载后设备号耗尽 | cdev_del 后未调用 unregister_chrdev_region | 在 exit 函数中确保释放设备号 |
调试技巧: 使用
cat /proc/devices 查看已注册的字符设备主设备号;使用 ls -l /dev/ 确认设备文件的主次设备号与内核注册的一致。若设备文件存在但 open 失败,优先检查设备号匹配。