3. 字符设备驱动框架:file_operations结构体、主次设备号、cdev注册与注销

字符设备是Linux驱动开发中最基础的设备类型,其核心框架围绕file_operations结构体、设备号与cdev对象展开。本章将系统讲解这三要素的协作关系与编程实现。

3.1 字符设备驱动核心架构

字符设备驱动的整体架构分为三层:应用层通过系统调用访问设备文件;VFS层将系统调用映射到驱动注册的file_operations回调;驱动层通过cdev对象与设备号关联,最终操作硬件寄存器。

字符设备驱动三层架构 应用层 用户程序 → open() / read() / write() / ioctl() VFS层(虚拟文件系统) sys_open → 查找inode → 调用file_operations.open 驱动层 cdev对象 → 设备号(主/次) → file_operations回调 硬件寄存器操作 / 中断处理 / DMA传输 核心:驱动通过cdev向VFS注册file_operations,应用通过设备文件访问驱动

3.2 file_operations结构体

file_operations是驱动与VFS之间的回调函数表,定义在linux/fs.h中。驱动只需实现需要的回调,未实现的成员设为NULL,VFS会使用默认行为。

成员名称 函数原型 说明
owner struct module *owner 通常设为THIS_MODULE,防止模块被卸载时仍有打开的文件
open int (*open)(struct inode *, struct file *) 打开设备时调用,用于初始化硬件或分配资源
release int (*release)(struct inode *, struct file *) 关闭设备时调用,用于释放资源
read ssize_t (*read)(struct file *, char __user *, size_t, loff_t *) 从设备读取数据,需使用copy_to_user安全拷贝
write ssize_t (*write)(struct file *, const char __user *, size_t, loff_t *) 向设备写入数据,需使用copy_from_user安全拷贝
unlocked_ioctl long (*unlocked_ioctl)(struct file *, unsigned int, unsigned long) 设备控制命令处理,替代旧的ioctl(无大内核锁)
llseek loff_t (*llseek)(struct file *, loff_t, int) 修改文件读写位置,默认实现default_llseek
关键提示:用户空间指针__user不能直接解引用!必须使用copy_to_user/copy_from_user或get_user/put_user系列函数,否则会导致内核崩溃或安全漏洞。

3.2.1 典型file_operations定义示例

// 定义设备结构体
struct my_device {
    char buffer[256];
    int  available;
    struct mutex lock;
};

// open回调:初始化设备
static int my_open(struct inode *inode, struct file *filp)
{
    struct my_device *dev = container_of(inode->i_cdev, struct my_device, cdev);
    filp->private_data = dev;  // 保存设备结构体到文件私有数据
    return 0;
}

// read回调:从设备读取数据
static ssize_t my_read(struct file *filp, char __user *buf, size_t count, loff_t *f_pos)
{
    struct my_device *dev = filp->private_data;
    ssize_t ret;

    mutex_lock(&dev->lock);
    if (*f_pos >= sizeof(dev->buffer)) {
        mutex_unlock(&dev->lock);
        return 0;  // EOF
    }
    count = min(count, sizeof(dev->buffer) - (size_t)*f_pos);
    if (copy_to_user(buf, dev->buffer + *f_pos, count)) {
        mutex_unlock(&dev->lock);
        return -EFAULT;
    }
    *f_pos += count;
    ret = count;
    mutex_unlock(&dev->lock);
    return ret;
}

// 定义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,
    .llseek  = default_llseek,
};

3.3 主次设备号

设备号是内核识别设备的唯一标识,由主设备号和次设备号组成(dev_t类型,32位)。主设备号对应驱动,次设备号对应具体设备实例。

设备号结构(dev_t) 主设备号(12位) MAJOR(dev) → 标识驱动 次设备号(20位) MINOR(dev) → 标识设备实例 MKDEV(major, minor) 组合设备号 | MAJOR(dev) / MINOR(dev) 提取

3.3.1 设备号分配方式

方式 函数 适用场景 注意事项
静态分配 register_chrdev_region(dev_t first, unsigned int count, const char *name) 已知主设备号(如I2C、SPI等标准设备) 需确保主设备号未被占用,否则注册失败
动态分配 alloc_chrdev_region(dev_t *dev, unsigned int baseminor, unsigned int count, const char *name) 自定义设备,推荐使用 内核自动分配未使用的主设备号,避免冲突

3.3.2 设备号分配示例

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

#define DEVICE_NAME "my_char_dev"
#define MINOR_COUNT 4  // 支持4个次设备

static dev_t dev_num;  // 存储分配的设备号

// 动态分配设备号
static int __init my_init(void)
{
    int ret;

    ret = alloc_chrdev_region(&dev_num, 0, MINOR_COUNT, DEVICE_NAME);
    if (ret < 0) {
        pr_err("Failed to allocate chrdev region\n");
        return ret;
    }

    pr_info("Allocated dev_t: major=%d, minor=%d\n",
            MAJOR(dev_num), MINOR(dev_num));
    return 0;
}

// 释放设备号
static void __exit my_exit(void)
{
    unregister_chrdev_region(dev_num, MINOR_COUNT);
    pr_info("Unregistered chrdev region\n");
}
常见错误:忘记调用unregister_chrdev_region释放设备号,导致模块卸载后设备号仍被占用,下次加载时动态分配可能失败。务必在exit函数中释放。

3.4 cdev注册与注销

cdev(字符设备对象)是内核中表示字符设备的核心数据结构,通过cdev_init初始化后,使用cdev_add将其注册到系统。注册成功后,VFS就能通过设备号找到对应的file_operations。

3.4.1 cdev生命周期

cdev生命周期流程图 1. 分配cdev对象 2. cdev_init初始化 3. cdev_add注册 4. 使用 5. cdev_del注销 6. 释放cdev对象 关键顺序:先分配设备号 → 再cdev_add → 最后创建设备节点 注销顺序相反:先cdev_del → 再unregister_chrdev_region → 最后释放cdev

3.4.2 完整cdev注册示例

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

struct my_device {
    struct cdev cdev;          // 内嵌cdev对象
    char buffer[256];
    int  available;
    struct mutex lock;
};

static struct my_device *my_dev;

static int __init my_init(void)
{
    int ret;
    dev_t dev_num;

    // 1. 分配设备号(动态)
    ret = alloc_chrdev_region(&dev_num, 0, 1, "my_device");
    if (ret < 0) {
        pr_err("Failed to allocate device number\n");
        return ret;
    }

    // 2. 分配设备结构体内存
    my_dev = kzalloc(sizeof(struct my_device), GFP_KERNEL);
    if (!my_dev) {
        unregister_chrdev_region(dev_num, 1);
        return -ENOMEM;
    }

    // 3. 初始化cdev
    cdev_init(&my_dev->cdev, &my_fops);
    my_dev->cdev.owner = THIS_MODULE;

    // 4. 注册cdev到系统
    ret = cdev_add(&my_dev->cdev, dev_num, 1);
    if (ret < 0) {
        pr_err("Failed to add cdev\n");
        kfree(my_dev);
        unregister_chrdev_region(dev_num, 1);
        return ret;
    }

    // 5. 初始化设备内部数据
    mutex_init(&my_dev->lock);
    my_dev->available = 1;

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

static void __exit my_exit(void)
{
    dev_t dev_num = my_dev->cdev.dev;  // 从cdev获取设备号

    // 1. 注销cdev
    cdev_del(&my_dev->cdev);

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

    // 3. 释放设备结构体
    mutex_destroy(&my_dev->lock);
    kfree(my_dev);

    pr_info("My device exited\n");
}

module_init(my_init);
module_exit(my_exit);

3.4.3 cdev_add失败处理

cdev_add可能因为设备号冲突、内存不足等原因失败。失败后必须立即回滚已分配的资源,否则会导致资源泄漏。

最佳实践:使用goto错误处理模式,确保每个错误路径都正确释放资源。内核驱动中goto是标准的错误处理方式,不要滥用但也不要排斥。
static int __init my_init(void)
{
    int ret;
    dev_t dev_num;

    ret = alloc_chrdev_region(&dev_num, 0, 1, "my_device");
    if (ret < 0)
        goto err_alloc_region;

    my_dev = kzalloc(sizeof(*my_dev), GFP_KERNEL);
    if (!my_dev) {
        ret = -ENOMEM;
        goto err_alloc_dev;
    }

    cdev_init(&my_dev->cdev, &my_fops);
    my_dev->cdev.owner = THIS_MODULE;

    ret = cdev_add(&my_dev->cdev, dev_num, 1);
    if (ret < 0)
        goto err_cdev_add;

    mutex_init(&my_dev->lock);
    return 0;

err_cdev_add:
    kfree(my_dev);
err_alloc_dev:
    unregister_chrdev_region(dev_num, 1);
err_alloc_region:
    return ret;
}

3.5 设备节点自动创建

传统方式需要手动mknod创建设备节点。现代驱动使用udev/mdev机制,通过class_create和device_create自动创建设备节点。

#include <linux/device.h>

static struct class *my_class;
static struct device *my_device;

static int __init my_init(void)
{
    // ... 前面的cdev注册代码 ...

    // 创建设备类
    my_class = class_create(THIS_MODULE, "my_class");
    if (IS_ERR(my_class)) {
        ret = PTR_ERR(my_class);
        goto err_class_create;
    }

    // 创建设备节点(自动在/dev/下生成)
    my_device = device_create(my_class, NULL, dev_num, NULL, "my_device%d", 0);
    if (IS_ERR(my_device)) {
        ret = PTR_ERR(my_device);
        goto err_device_create;
    }

    return 0;

err_device_create:
    class_destroy(my_class);
err_class_create:
    cdev_del(&my_dev->cdev);
    // ... 其他清理 ...
    return ret;
}

static void __exit my_exit(void)
{
    device_destroy(my_class, my_dev->cdev.dev);
    class_destroy(my_class);
    cdev_del(&my_dev->cdev);
    // ... 其他清理 ...
}

3.6 本章小结

组件 作用 关键函数/宏
file_operations 定义驱动回调函数表 .open .read .write .unlocked_ioctl
设备号 唯一标识设备 alloc_chrdev_region MKDEV MAJOR MINOR
cdev 字符设备内核对象 cdev_init cdev_add cdev_del
设备节点 用户空间访问入口 class_create device_create
核心口诀:先分配设备号,再初始化cdev,最后cdev_add注册;注销顺序完全相反。设备节点通过class/device机制自动创建,无需手动mknod。