3. 字符设备驱动框架:file_operations结构体、cdev注册流程、设备节点与驱动的绑定

字符设备是Linux驱动开发中最基础的设备类型,其核心框架围绕file_operations结构体、cdev注册流程以及设备节点与驱动的绑定三个关键环节展开。理解这一框架是掌握HAL层设备节点访问控制的前提。

3.1 file_operations结构体:驱动与用户空间的接口契约

file_operations结构体是字符设备驱动的核心数据结构,它定义了驱动向用户空间暴露的操作函数指针集合。用户空间通过系统调用(如open、read、write、ioctl)访问设备节点时,VFS(虚拟文件系统)会查找该设备节点对应的file_operations实例,并调用相应的驱动函数。

关键理解: file_operations是驱动与用户空间之间的“函数跳转表”,每个设备节点在打开后都会关联一个file结构,该结构中的f_op指针指向驱动提供的file_operations实例。

3.1.1 核心成员与功能

成员函数 系统调用对应 功能描述 典型返回值
open open() 打开设备,初始化硬件或分配资源 0(成功)或负错误码
release close() 关闭设备,释放资源 0
read read() 从设备读取数据到用户缓冲区 实际读取字节数
write write() 从用户缓冲区写入数据到设备 实际写入字节数
unlocked_ioctl ioctl() 设备特定控制命令(无大内核锁) 0或命令特定值
mmap mmap() 将设备内存映射到用户空间 0或负错误码
poll poll()/select()/epoll() 查询设备是否可读写 POLLIN/POLLOUT掩码
开发提示: 在HAL层驱动开发中,unlocked_ioctl是最常用的接口,用于实现设备特定的控制逻辑(如设置采样率、启动/停止采集)。read和write则用于批量数据传输。

3.1.2 结构体定义示例

// 定义驱动实现的函数
static int my_dev_open(struct inode *inode, struct file *filp);
static int my_dev_release(struct inode *inode, struct file *filp);
static ssize_t my_dev_read(struct file *filp, char __user *buf, size_t count, loff_t *f_pos);
static ssize_t my_dev_write(struct file *filp, const char __user *buf, size_t count, loff_t *f_pos);
static long my_dev_ioctl(struct file *filp, unsigned int cmd, unsigned long arg);

// 填充 file_operations 结构体
static struct file_operations my_dev_fops = {
    .owner          = THIS_MODULE,
    .open           = my_dev_open,
    .release        = my_dev_release,
    .read           = my_dev_read,
    .write          = my_dev_write,
    .unlocked_ioctl = my_dev_ioctl,
};
常见错误: 忘记设置.owner = THIS_MODULE会导致模块引用计数失效,在模块卸载时可能引发内核崩溃。另外,如果驱动不需要某个操作,应将其设置为NULL,而不是实现一个空函数。

3.2 cdev注册流程:将驱动注册到内核

cdev(字符设备)结构体是内核中表示字符设备的抽象。注册流程分为三个核心步骤:分配cdev、初始化cdev、添加cdev到内核。只有完成注册后,内核才能将设备节点与驱动关联起来。

3.2.1 注册流程步骤

  1. 分配设备号:使用alloc_chrdev_region()动态分配主设备号和次设备号范围,或使用register_chrdev_region()注册静态设备号。
  2. 初始化cdev:调用cdev_init(),将cdev结构与file_operations绑定。
  3. 添加cdev到内核:调用cdev_add(),将cdev注册到内核的设备管理链表中,并关联设备号范围。
  4. 创建设备节点(可选但推荐):使用device_create()在/dev目录下自动创建设备节点,或手动使用mknod命令。
核心原则: cdev_add()必须在设备号分配之后调用,且必须在模块初始化函数中完成。设备节点可以在cdev_add之后随时创建,但只有cdev_add成功后,设备节点才能正确绑定到驱动。

3.2.2 完整注册代码示例

#include <linux/fs.h>
#include <linux/cdev.h>
#include <linux/device.h>

static dev_t my_dev_num;
static struct cdev my_cdev;
static struct class *my_class;

static int __init my_driver_init(void)
{
    int ret;

    // 步骤1:动态分配设备号(主设备号由内核分配)
    ret = alloc_chrdev_region(&my_dev_num, 0, 1, "my_device");
    if (ret < 0) {
        pr_err("Failed to allocate device number\n");
        return ret;
    }

    // 步骤2:初始化cdev,绑定file_operations
    cdev_init(&my_cdev, &my_dev_fops);
    my_cdev.owner = THIS_MODULE;

    // 步骤3:添加cdev到内核
    ret = cdev_add(&my_cdev, my_dev_num, 1);
    if (ret < 0) {
        pr_err("Failed to add cdev\n");
        unregister_chrdev_region(my_dev_num, 1);
        return ret;
    }

    // 步骤4:创建device class和设备节点(自动在/dev下生成)
    my_class = class_create(THIS_MODULE, "my_class");
    if (IS_ERR(my_class)) {
        pr_err("Failed to create class\n");
        cdev_del(&my_cdev);
        unregister_chrdev_region(my_dev_num, 1);
        return PTR_ERR(my_class);
    }

    device_create(my_class, NULL, my_dev_num, NULL, "my_device0");

    pr_info("My driver initialized, major=%d, minor=%d\n",
            MAJOR(my_dev_num), MINOR(my_dev_num));
    return 0;
}

static void __exit my_driver_exit(void)
{
    device_destroy(my_class, my_dev_num);
    class_destroy(my_class);
    cdev_del(&my_cdev);
    unregister_chrdev_region(my_dev_num, 1);
    pr_info("My driver exited\n");
}

module_init(my_driver_init);
module_exit(my_driver_exit);

3.2.3 注册流程SVG图

字符设备cdev注册流程 步骤1:分配设备号 alloc_chrdev_region() 步骤2:初始化cdev cdev_init() + fops绑定 步骤3:添加cdev cdev_add() 步骤4:创建设备节点 class_create() + device_create() 设备节点与驱动绑定关系 设备节点(dev_t) → cdev → file_operations → 驱动函数 驱动就绪:可被用户空间访问 /dev/my_device0 → open/read/write/ioctl ⚠ 常见避坑 • cdev_add()必须在设备号分配之后调用 • 卸载时顺序相反:device_destroy → cdev_del → unregister_chrdev_region

3.3 设备节点与驱动的绑定机制

设备节点(/dev/my_device0)与驱动的绑定是通过设备号作为桥梁实现的。当用户空间打开设备节点时,内核VFS层根据设备节点中的主设备号和次设备号,在内核的cdev_map散列表中查找对应的cdev实例,进而获取file_operations指针。

3.3.1 绑定流程详解

  • 设备节点创建:device_create()在/dev下创建节点,节点中存储了dev_t(主设备号+次设备号)。
  • 打开操作:用户调用open("/dev/my_device0", O_RDWR),VFS解析路径找到inode,从中提取dev_t。
  • 查找cdev:内核通过dev_t在cdev_map中查找已注册的cdev对象。
  • 绑定fops:找到cdev后,将file结构的f_op指针设置为cdev->ops(即file_operations)。
  • 调用open函数:最后调用file_operations->open(),驱动获得控制权。

3.3.2 绑定关系SVG架构图

设备节点与驱动的绑定关系 用户空间 open("/dev/my_device0") VFS:解析inode获取dev_t cdev_map查找 内核空间 cdev实例 dev = MKDEV(major, minor) file_operations .open / .read / .write / .ioctl 驱动函数 my_dev_open() 绑定关键:设备节点(dev_t) → cdev_map → cdev → file_operations → 驱动函数

3.4 多设备支持与次设备号管理

在实际HAL层驱动中,一个驱动通常需要管理多个同类型设备(如多个传感器、多个GPIO控制器)。通过次设备号可以区分不同设备实例,驱动在open函数中根据次设备号初始化不同的硬件上下文。

3.4.1 多设备注册示例

// 注册多个次设备号
#define NUM_DEVICES 4
ret = alloc_chrdev_region(&my_dev_num, 0, NUM_DEVICES, "my_device");

// 为每个设备注册独立的cdev
for (int i = 0; i < NUM_DEVICES; i++) {
    cdev_init(&my_cdevs[i], &my_dev_fops);
    my_cdevs[i].owner = THIS_MODULE;
    ret = cdev_add(&my_cdevs[i], MKDEV(MAJOR(my_dev_num), i), 1);
    if (ret < 0) {
        // 错误处理:回滚已注册的cdev
        while (--i >= 0) cdev_del(&my_cdevs[i]);
        unregister_chrdev_region(my_dev_num, NUM_DEVICES);
        return ret;
    }
    // 创建设备节点
    device_create(my_class, NULL, MKDEV(MAJOR(my_dev_num), i), NULL, "my_device%d", i);
}

3.4.2 在open函数中区分设备

static int my_dev_open(struct inode *inode, struct file *filp)
{
    unsigned int minor = iminor(inode);  // 获取次设备号
    struct my_device_data *dev_data;

    // 根据次设备号选择对应的设备数据结构
    if (minor >= MAX_DEVICES) {
        return -ENODEV;
    }

    dev_data = &devices[minor];
    filp->private_data = dev_data;  // 保存到file的私有数据

    // 初始化硬件(如使能时钟、配置GPIO)
    dev_data->ref_count++;
    pr_info("Device %d opened, ref_count=%d\n", minor, dev_data->ref_count);
    return 0;
}
最佳实践: 使用filp->private_data保存设备私有数据结构,避免在后续read/write/ioctl中重复查找设备。这是驱动开发中广泛使用的模式。

3.5 本章小结

核心概念 关键函数/结构体 作用
file_operations struct file_operations 定义驱动与用户空间的接口函数集
cdev注册 cdev_init() + cdev_add() 将字符设备注册到内核,关联设备号与fops
设备节点绑定 device_create() + cdev_map 通过设备号将/dev节点与驱动绑定
多设备管理 次设备号 + iminor() 通过次设备号区分不同设备实例