4. cdev 结构体:初始化、注册与设备操作集

在 Linux 字符设备驱动中,cdev 是内核用于描述字符设备的核心结构体。它连接了设备编号、设备操作集 file_operations 以及设备实例。本章将深入剖析 cdev 的初始化、注册与注销流程,并详细解读 file_operations 中关键接口的实现规范。

4.1 cdev 结构体定义与核心字段

cdev 结构体定义在 <linux/cdev.h> 中,是字符设备在内核中的“身份证”。其核心字段如下:

字段 类型 说明
kobj struct kobject 内核对象基类,用于设备模型、引用计数与 sysfs 管理
owner struct module * 指向所属模块的指针,防止模块被意外卸载
ops const struct file_operations * 设备操作集指针,驱动核心功能入口
list struct list_head 用于将 cdev 链接到系统 cdev 链表
dev dev_t 设备号(主设备号 + 次设备号)
count unsigned int 关联的次设备号数量
关键理解: cdev 是内核 VFS 层与具体硬件驱动之间的桥梁。当用户空间通过 open() 打开设备文件时,VFS 通过 cdev 找到对应的 file_operations,从而调用驱动实现的函数。

4.2 cdev 初始化:静态与动态两种方式

驱动开发者有两种方式初始化 cdev 结构体:

4.2.1 静态初始化(运行时初始化)

使用 cdev_init() 函数,适用于 cdev 已分配内存(全局或动态分配)的场景:

#include <linux/cdev.h>

struct cdev my_cdev;

// 1. 初始化 cdev,绑定 file_operations
cdev_init(&my_cdev, &my_fops);

// 2. 设置 owner 字段,防止模块被卸载
my_cdev.owner = THIS_MODULE;

4.2.2 动态分配与初始化

使用 cdev_alloc() 函数,由内核自动分配 cdev 内存并初始化:

struct cdev *my_cdev;

my_cdev = cdev_alloc();
if (!my_cdev) {
    pr_err("Failed to allocate cdev\n");
    return -ENOMEM;
}

my_cdev->ops = &my_fops;
my_cdev->owner = THIS_MODULE;
推荐实践: 在大多数现代驱动中,推荐将 cdev 作为自定义设备结构体的成员进行静态初始化,这样可以更好地管理设备私有数据,避免内存泄漏。

4.3 cdev_add 与 cdev_del:注册与注销

初始化完成后,必须将 cdev 注册到内核,设备才能被系统识别。注销时则需从内核中移除。

4.3.1 cdev_add:向系统注册字符设备

int cdev_add(struct cdev *p, dev_t dev, unsigned count);
  • p:已初始化的 cdev 指针
  • dev:设备起始编号(已通过 alloc_chrdev_region 或 register_chrdev_region 获取)
  • count:关联的次设备号数量
  • 返回值:0 成功,负值失败
重要时序: cdev_add() 调用后,设备立即可被用户空间访问。因此,必须在 cdev_add() 之前完成所有硬件初始化、缓冲区分配等准备工作,避免设备打开时驱动尚未就绪。

4.3.2 cdev_del:从系统注销字符设备

void cdev_del(struct cdev *p);
  • 该函数将 cdev 从内核链表中移除
  • 调用后,设备文件不再关联到驱动
  • 通常在模块卸载函数中调用

4.3.3 完整注册与注销流程

标准模板: 字符设备驱动的生命周期通常遵循以下顺序:
// 模块加载
static int __init my_driver_init(void)
{
    dev_t dev_num;
    int ret;

    // 1. 分配设备号
    ret = alloc_chrdev_region(&dev_num, 0, 1, "my_device");
    if (ret < 0)
        return ret;

    // 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) {
        unregister_chrdev_region(dev_num, 1);
        return ret;
    }

    return 0;
}

// 模块卸载
static void __exit my_driver_exit(void)
{
    dev_t dev_num = my_cdev.dev;

    // 1. 注销 cdev
    cdev_del(&my_cdev);

    // 2. 释放设备号
    unregister_chrdev_region(dev_num, 1);
}

4.4 设备操作集 file_operations 详解

file_operations 是字符设备驱动的核心接口表,定义了用户空间通过系统调用操作设备文件时,内核应调用的驱动函数。其定义在 <linux/fs.h> 中。

4.4.1 核心成员与函数原型

成员 函数原型 触发系统调用 说明
open int (*open)(struct inode *, struct file *) open() 打开设备,进行初始化、权限检查、分配私有数据
release int (*release)(struct inode *, struct file *) close() 关闭设备,释放资源、递减引用计数
read ssize_t (*read)(struct file *, char __user *, size_t, loff_t *) read() 从设备读取数据到用户空间
write ssize_t (*write)(struct file *, const char __user *, size_t, loff_t *) write() 从用户空间写入数据到设备
unlocked_ioctl long (*unlocked_ioctl)(struct file *, unsigned int, unsigned long) ioctl() 设备控制命令,无大内核锁(现代驱动使用)
poll __poll_t (*poll)(struct file *, struct poll_table_struct *) select()/poll()/epoll() 查询设备是否可读/可写,支持 I/O 多路复用
mmap int (*mmap)(struct file *, struct vm_area_struct *) mmap() 将设备内存映射到用户空间

4.4.2 典型 file_operations 实例化

static const struct file_operations my_fops = {
    .owner          = THIS_MODULE,
    .open           = my_open,
    .release        = my_release,
    .read           = my_read,
    .write          = my_write,
    .unlocked_ioctl = my_ioctl,
    .poll           = my_poll,
    .mmap           = my_mmap,
};
注意: 对于不需要实现的操作,可以将其设置为 NULL。内核会为 NULL 的操作返回默认行为(如 -EINVAL 或 -ENOTTY)。owner 字段必须设置为 THIS_MODULE,以支持模块引用计数。

4.5 cdev、设备号与 file_operations 的关系

下图展示了 cdev、设备号、file_operations 以及 VFS 层之间的协作关系:

cdev、设备号与 file_operations 关系图 用户空间:open("/dev/mydev", O_RDWR) VFS 层:根据设备号 inode->i_rdev 查找 cdev cdev 实例 dev = MKDEV(240, 0) | ops = &my_fops file_operations .open = my_open 设备号:主设备号 240,次设备号 0 inode->i_rdev = 设备号

4.6 私有数据与 container_of 宏

在实际驱动中,通常需要将 cdev 嵌入到自定义的设备结构体中,以便在操作函数中访问设备私有数据。这是通过 container_of 宏实现的:

struct my_device {
    struct cdev cdev;        // 必须作为第一个成员(推荐)
    spinlock_t lock;
    void __iomem *base_addr;
    unsigned int buffer_size;
    // ... 其他私有数据
};

static int my_open(struct inode *inode, struct file *filp)
{
    // 从 inode->i_cdev 获取 cdev 指针,再通过 container_of 获取 my_device
    struct my_device *mydev = container_of(inode->i_cdev, struct my_device, cdev);

    // 将私有数据存入 file->private_data,供其他操作使用
    filp->private_data = mydev;

    return 0;
}

static ssize_t my_read(struct file *filp, char __user *buf, size_t len, loff_t *off)
{
    struct my_device *mydev = filp->private_data;
    // ... 使用 mydev 进行读取操作
}
设计模式: 将 cdev 嵌入自定义结构体,并通过 container_of 获取父结构体,是 Linux 字符设备驱动的标准设计模式。这种方式避免了全局变量,支持多实例设备。

4.7 本章小结

  • cdev 初始化:使用 cdev_init() 或 cdev_alloc(),并设置 owner 和 ops 字段。
  • cdev_add:在设备号分配后调用,将设备注册到内核,调用后设备立即可用。
  • cdev_del:在模块卸载时调用,从内核中移除设备。
  • file_operations:驱动核心接口表,必须实现 open/release,根据需求实现 read/write/ioctl/poll 等。
  • 私有数据管理:通过 container_of 和 file->private_data 实现设备实例与操作函数的绑定。
常见错误:
  • 在 cdev_add 之后才进行硬件初始化,导致设备打开时驱动未就绪。
  • 忘记设置 owner = THIS_MODULE,导致模块引用计数失效,可能引发内核崩溃。
  • 在 file_operations 中使用了 ioctl 而非 unlocked_ioctl,导致在 5.x 以上内核中编译警告或功能异常。