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 的关联:

  1. VFS 层: 根据文件路径找到对应的 inode,从 inode->i_rdev 中提取设备号
  2. 内核查找: 根据设备号在全局 cdev_map 散列表中查找对应的 cdev 实例
  3. 绑定操作表: 将 inode->i_cdev 指向找到的 cdev,并将 file->f_op 设置为 cdev->ops
  4. 调用 open: 若 file_operations 中定义了 open 方法,则调用之
理解要点: 设备文件与驱动的绑定发生在 open() 时刻,而非 cdev_add() 时刻。cdev_add() 只是让内核知道“存在这样一个设备”,而 open() 才真正建立文件实例与驱动函数的连接。

4.6 完整生命周期流程图

下图展示了 cdev 从初始化到注销的完整生命周期,以及设备文件打开时的关联路径:

cdev 生命周期与设备文件关联流程图 cdev 生命周期与设备文件关联流程图 ① cdev_init / cdev_alloc ② 设置 cdev->owner = THIS_MODULE ③ cdev_add(cdev, dev, count) ④ 设备可用,等待用户操作 ⑤ open() → 查找 cdev → 绑定 f_op ⑥ read / write → 调用 cdev->ops ⑦ cdev_del(cdev) ⑧ unregister_chrdev_region() 关键说明: • cdev_add 后设备立即可访问,但 f_op 绑定在 open 时 • cdev_del 等待所有操作完成后才返回 • 设备号释放必须在 cdev_del 之后进行

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 失败,优先检查设备号匹配。