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 结构体

内部行为:

  1. 将 cdev->ops 指向 fops
  2. 将 cdev->kobj 初始化为默认状态
  3. 将 cdev->owner 设为 NULL(需手动设置 THIS_MODULE)
  4. 初始化内部链表节点
常见错误: 调用 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() 调用。典型顺序为:

  1. 调用 cdev_del() 移除设备
  2. 调用 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 层的交互关系:

cdev 生命周期与内核交互流程 alloc_chrdev_region 分配设备号 cdev_init 绑定 file_operations cdev_add 注册到内核 内核 VFS 层 用户调用 open/read/write/close 时,VFS 通过 cdev 找到对应的 file_operations cdev 结构体挂入内核 chrdevs 哈希表,实现设备号到操作函数的快速查找 cdev_add 成功后,/proc/devices 即可看到该设备 用户空间 通过 /dev/my_char_dev 设备文件进行 open/read/write/ioctl 操作 cdev_del 从内核移除 cdev unregister_chrdev_region 释放设备号

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_operations
  • cdev_add() 将设备注册到内核,成功后设备对用户空间可见
  • cdev_del() 从内核移除设备,需在释放设备号之前调用
  • 标准流程:分配设备号 → cdev_init → cdev_add → (设备操作)→ cdev_del → 释放设备号
  • 注意 owner 字段设置、错误路径资源清理、注销顺序等关键细节