4. cdev 结构体:cdev_init、cdev_add、cdev_del 函数使用
在 Linux 字符设备驱动中,struct cdev 是内核用于管理字符设备的核心数据结构。每个字符设备在内核中都必须对应一个 cdev 实例,通过它完成设备号与文件操作集 file_operations 的绑定。本章详细讲解 cdev 结构体的定义、初始化、注册与注销流程,并给出标准实现模板。
4.1 cdev 结构体定义
struct cdev 定义在 <linux/cdev.h> 中,是内核字符设备驱动的基础设施。其核心字段如下:
| 字段 | 类型 | 说明 |
|---|---|---|
kobj |
struct kobject |
内核对象基类,用于引用计数与 sysfs 管理 |
owner |
struct module * |
指向所属模块,通常设为 THIS_MODULE |
ops |
const struct file_operations * |
设备文件操作函数指针表 |
list |
struct list_head |
用于将 cdev 挂入内核哈希链表 |
dev |
dev_t |
设备号(主设备号 + 次设备号) |
count |
unsigned int |
该设备管理的次设备号数量 |
关键理解:
cdev 是内核视角下字符设备的"身份证",它把设备号(谁)和操作函数(做什么)绑定在一起。驱动开发者只需填充 ops 和 dev,其余字段由内核管理。
4.2 cdev_init:初始化 cdev 实例
cdev_init() 用于初始化一个 cdev 结构体,建立 cdev 与 file_operations 的关联。函数原型如下:
void cdev_init(struct cdev *cdev, const struct file_operations *fops);
参数说明:
cdev:指向待初始化的cdev结构体指针fops:指向已实现的file_operations结构体
内部行为:
- 将
cdev->ops指向fops - 将
cdev->kobj初始化为默认状态 - 将
cdev->owner设为NULL(需手动设置THIS_MODULE) - 初始化内部链表节点
常见错误: 调用
cdev_init() 后忘记设置 cdev->owner = THIS_MODULE,会导致模块卸载时内核无法正确管理引用计数,可能引发 Oops 或模块无法卸载。
4.3 cdev_add:向内核注册字符设备
cdev_add() 将初始化完成的 cdev 实例注册到内核,使其对用户空间可见。函数原型:
int cdev_add(struct cdev *cdev, dev_t dev, unsigned int count);
参数说明:
| 参数 | 说明 |
|---|---|
cdev |
已初始化的 cdev 指针 |
dev |
设备起始编号(含主设备号和起始次设备号) |
count |
该设备管理的次设备号数量 |
返回值: 成功返回 0,失败返回负的错误码(如 -ENOMEM、-EBUSY)。
最佳实践: 调用
cdev_add() 之前必须先确保设备号已成功分配(通过 alloc_chrdev_region() 或 register_chrdev_region())。cdev_add() 一旦成功,设备立即对用户空间可用,因此应在驱动初始化最后一步调用。
4.4 cdev_del:从内核注销字符设备
cdev_del() 从内核中移除 cdev 实例,释放其占用的内核资源。函数原型:
void cdev_del(struct cdev *cdev);
调用时机: 在模块卸载函数中,先于 unregister_chrdev_region() 调用。典型顺序为:
- 调用
cdev_del()移除设备 - 调用
unregister_chrdev_region()释放设备号
注意:
cdev_del() 不会释放 cdev 结构体本身占用的内存。如果 cdev 是动态分配的(如使用 cdev_alloc()),需要额外调用 kfree() 释放;如果是静态或嵌入在结构体中的,则无需手动释放。
4.5 标准实现模板与流程
以下是一个完整的字符设备驱动初始化与注销模板,整合了设备号分配、cdev 初始化、注册与注销:
#include <linux/module.h>
#include <linux/fs.h>
#include <linux/cdev.h>
#include <linux/device.h>
#define DEVICE_NAME "my_char_dev"
#define CLASS_NAME "my_class"
static int major;
static struct class *my_class;
static struct device *my_device;
static struct cdev my_cdev;
/* 文件操作函数声明 */
static int my_open(struct inode *inode, struct file *file);
static int my_release(struct inode *inode, struct file *file);
static ssize_t my_read(struct file *file, char __user *buf,
size_t len, loff_t *off);
static ssize_t my_write(struct file *file, const char __user *buf,
size_t len, loff_t *off);
static struct file_operations my_fops = {
.owner = THIS_MODULE,
.open = my_open,
.release = my_release,
.read = my_read,
.write = my_write,
};
static int __init my_driver_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);
/* 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");
goto err_cdev_add;
}
/* 4. 创建设备类与设备节点(自动生成 /dev/ 节点) */
my_class = class_create(THIS_MODULE, CLASS_NAME);
if (IS_ERR(my_class)) {
ret = PTR_ERR(my_class);
goto err_class_create;
}
my_device = device_create(my_class, NULL, dev_num,
NULL, DEVICE_NAME);
if (IS_ERR(my_device)) {
ret = PTR_ERR(my_device);
goto err_device_create;
}
pr_info("My char device registered: major=%d\n", major);
return 0;
err_device_create:
class_destroy(my_class);
err_class_create:
cdev_del(&my_cdev);
err_cdev_add:
unregister_chrdev_region(dev_num, 1);
return ret;
}
static void __exit my_driver_exit(void)
{
dev_t dev_num = MKDEV(major, 0);
/* 逆序释放资源 */
device_destroy(my_class, dev_num);
class_destroy(my_class);
cdev_del(&my_cdev);
unregister_chrdev_region(dev_num, 1);
pr_info("My char device unregistered\n");
}
module_init(my_driver_init);
module_exit(my_driver_exit);
MODULE_LICENSE("GPL");
MODULE_AUTHOR("Driver Expert");
MODULE_DESCRIPTION("Character device driver with cdev example");
4.6 cdev 生命周期与内核交互流程
下图展示了 cdev 从初始化到注销的完整生命周期,以及与内核 VFS 层的交互关系:
4.7 常见错误与避坑指南
| 错误场景 | 后果 | 正确做法 |
|---|---|---|
未设置 cdev->owner |
模块引用计数异常,rmmod 可能失败或崩溃 | 在 cdev_init() 后添加 cdev->owner = THIS_MODULE |
先 cdev_add() 后分配设备号 |
cdev_add 使用未初始化的 dev_t,注册失败 | 严格遵循:分配设备号 → cdev_init → cdev_add |
| cdev_add 失败后未清理设备号 | 设备号泄漏,后续模块无法使用 | 在错误处理路径中调用 unregister_chrdev_region() |
| 注销时先释放设备号再 cdev_del | cdev_del 访问已释放的设备号,内核崩溃 | 先 cdev_del,再 unregister_chrdev_region |
| 静态 cdev 未初始化直接 cdev_add | 内核访问未初始化的 ops 指针,Oops | 任何 cdev 在使用前都必须调用 cdev_init |
4.8 动态分配 vs 静态嵌入 cdev
驱动开发中 cdev 实例有两种管理方式,各有适用场景:
| 方式 | 分配方法 | 释放方法 | 适用场景 |
|---|---|---|---|
| 静态嵌入 | struct cdev my_cdev; 全局或嵌入自定义结构体 |
无需手动释放,随模块卸载自动回收 | 单设备驱动,设备数量固定 |
| 动态分配 | struct cdev *p = cdev_alloc(); |
cdev_del(p); kfree(p); |
多设备驱动,设备数量动态变化 |
推荐做法: 对于大多数教学和简单项目,使用静态嵌入方式更简洁安全。动态分配时务必注意
cdev_alloc() 已自动初始化部分字段,但仍需手动设置 ops 和 owner。
4.9 本章小结
struct cdev是字符设备驱动的内核核心结构,负责绑定设备号与文件操作集cdev_init()初始化 cdev 并关联file_operationscdev_add()将设备注册到内核,成功后设备对用户空间可见cdev_del()从内核移除设备,需在释放设备号之前调用- 标准流程:分配设备号 → cdev_init → cdev_add → (设备操作)→ cdev_del → 释放设备号
- 注意
owner字段设置、错误路径资源清理、注销顺序等关键细节