3. 字符设备驱动框架

字符设备是Linux驱动中最基础的设备类型,其核心驱动框架围绕三个关键要素构建:cdev结构体(内核中代表字符设备的对象)、设备号(用户空间与内核空间交互的标识)、以及file_operations接口(驱动行为的具体实现)。本节将系统梳理这三者的关系与实现流程。

3.1 字符设备驱动核心架构

字符设备驱动的整体架构可以抽象为“用户空间 → VFS → 驱动层 → 硬件”的调用链。其中,cdev结构体是驱动在内核中的“身份证”,file_operations是驱动对外提供的“服务菜单”,而设备号则是用户空间访问驱动的“门牌号”。

核心概念速览:
  • cdev:内核中描述字符设备的数据结构,包含设备号、操作函数集指针等。
  • 设备号:由主设备号(major)和次设备号(minor)组成,用于唯一标识设备。
  • file_operations:函数指针结构体,定义了open、read、write、release等标准操作。
字符设备驱动整体架构图 用户空间 应用程序 (open/read/write) VFS 虚拟文件系统 系统调用接口层 驱动层 cdev + file_operations 驱动层内部结构展开 struct cdev dev_t dev (设备号) ops → file_operations struct file_operations .open = my_open .read = my_read 设备号 dev_t 主设备号 (major) 次设备号 (minor) 驱动注册流程:alloc_chrdev_region → cdev_init → cdev_add

3.2 cdev结构体详解

struct cdev是内核中字符设备的“本体”,定义在 <linux/cdev.h> 中。驱动开发者需要分配并初始化一个cdev实例,然后将其注册到内核中。

成员 类型 说明
kobj struct kobject 内核对象基类,用于设备模型管理
owner struct module * 指向所属模块,通常设为 THIS_MODULE
ops const struct file_operations * 指向该设备的操作函数集
list struct list_head 用于将cdev链接到系统设备链表
dev dev_t 设备号(主设备号 + 次设备号)
count unsigned int 该设备管理的次设备号数量
编程要点:cdev的初始化通常使用 cdev_init(&my_cdev, &my_fops),该函数会将ops指针赋值,并将owner设为 THIS_MODULE。注册使用 cdev_add(&my_cdev, devno, count),其中devno是已分配的设备号,count是次设备号范围。

3.3 设备号:注册与注销

设备号是用户空间访问驱动的“钥匙”。主设备号标识设备类型,次设备号标识同一类型下的不同实例。设备号的分配有两种方式:静态分配(手动指定主设备号)和动态分配(由内核自动分配)。

3.3.1 设备号分配与注册流程

  1. 定义设备号变量:dev_t devno;
  2. 分配设备号:
    • 动态分配:alloc_chrdev_region(&devno, minor_base, count, name)
    • 静态分配:register_chrdev_region(devno, count, name)
  3. 提取主/次设备号:MAJOR(devno) 和 MINOR(devno)
  4. 初始化并添加cdev:cdev_init() + cdev_add()
  5. 卸载时注销:cdev_del() + unregister_chrdev_region()
函数 用途 参数说明
alloc_chrdev_region 动态分配设备号 devno(输出), minor_base, count, name
register_chrdev_region 静态注册指定设备号 devno(输入), count, name
unregister_chrdev_region 注销设备号 devno, count
常见错误:
  • 设备号注销顺序错误:必须先 cdev_del() 再 unregister_chrdev_region(),否则可能导致内核访问已释放的资源。
  • 次设备号范围(count)设置过大,导致与其他驱动冲突。
  • 静态分配时使用已被占用的主设备号,导致注册失败。

3.4 file_operations接口实现

struct file_operations 是驱动与VFS之间的“协议”。驱动开发者通过实现该结构体中的函数指针,来定义设备的行为。最常用的接口包括:

接口 函数原型 触发时机
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() 系统调用时
llseek loff_t (*llseek)(struct file *, loff_t, int) 用户调用 lseek() 系统调用时

3.4.1 典型实现示例

以下是一个简化的字符设备驱动 file_operations 实现框架:

// 设备打开
static int my_open(struct inode *inode, struct file *filp)
{
    // 获取设备结构体指针,通常通过 container_of 或 inode->i_cdev
    struct my_device *dev = container_of(inode->i_cdev, struct my_device, cdev);
    filp->private_data = dev;  // 保存到文件私有数据
    return 0;
}

// 设备读取
static ssize_t my_read(struct file *filp, char __user *buf, size_t len, loff_t *off)
{
    struct my_device *dev = filp->private_data;
    // 从内核缓冲区拷贝数据到用户空间
    if (copy_to_user(buf, dev->buffer, len))
        return -EFAULT;
    return len;
}

// 设备写入
static ssize_t my_write(struct file *filp, const char __user *buf, size_t len, loff_t *off)
{
    struct my_device *dev = filp->private_data;
    // 从用户空间拷贝数据到内核缓冲区
    if (copy_from_user(dev->buffer, buf, len))
        return -EFAULT;
    return len;
}

// 设备释放
static int my_release(struct inode *inode, struct file *filp)
{
    return 0;
}

// 定义 file_operations 结构体
static const struct file_operations my_fops = {
    .owner   = THIS_MODULE,
    .open    = my_open,
    .read    = my_read,
    .write   = my_write,
    .release = my_release,
};
关键注意事项:
  • 用户空间与内核空间数据交换:必须使用 copy_to_user() 和 copy_from_user(),禁止直接解引用用户空间指针。
  • 错误处理:所有接口函数应返回负的错误码(如 -ENOMEM、-EFAULT)表示失败,返回非负值表示成功或实际传输的字节数。
  • 并发保护:如果设备可能被多个进程同时访问,需要在 read/write 实现中加入互斥锁或信号量。

3.5 完整驱动注册与注销流程

将上述三个要素整合起来,字符设备驱动的生命周期如下:

字符设备驱动注册与注销流程 注册流程 (模块加载) 1. alloc_chrdev_region() 2. cdev_init() 3. cdev_add() 4. 设备创建 (device_create) 注销流程 (模块卸载) 1. device_destroy() 2. cdev_del() 3. unregister_chrdev_region() 4. 释放私有数据/缓冲区 注意:注册与注销的顺序必须严格对称,否则可能导致内核资源泄漏或崩溃

3.6 本章小结

字符设备驱动框架是Linux驱动开发的基石。理解 cdev、设备号和 file_operations 三者的关系,掌握注册与注销的对称流程,是编写稳定可靠驱动的前提。在实际开发中,建议优先使用动态设备号分配(alloc_chrdev_region)以避免主设备号冲突,并在 file_operations 实现中严格遵循内核API规范,特别是用户空间数据拷贝和并发保护。