1. Linux 驱动基础回顾:字符设备驱动框架、file_operations 结构体、模块加载与卸载流程

在嵌入式 Linux 系统中,字符设备驱动是最基础、最常用的驱动类型。无论是 GPIO、UART、I2C 还是 SPI,底层都遵循字符设备驱动的统一框架。理解这一框架是后续进行性能优化和 CPU 亲和性绑定的前提。

1.1 字符设备驱动核心框架

字符设备驱动的核心思想是将硬件操作抽象为文件操作。用户空间通过 open()、read()、write()、ioctl() 等标准系统调用与设备交互,而驱动则负责将这些调用映射到具体的硬件寄存器操作。

关键概念: 在 Linux 中,“一切皆文件”。字符设备通过主设备号(Major Number)和次设备号(Minor Number)在系统中唯一标识。主设备号对应驱动,次设备号对应具体设备实例。
字符设备驱动框架图 用户空间 (User Space) 应用程序:open() / read() / write() / ioctl() / close() 系统调用层 (VFS) 内核空间 (Kernel Space) file_operations 结构体:.open / .read / .write / .unlocked_ioctl / .release 硬件设备 (Hardware) 说明:驱动位于内核空间,通过 file_operations 回调函数响应系统调用,最终操作硬件寄存器

1.2 file_operations 结构体详解

file_operations 是字符设备驱动的核心数据结构,它定义了驱动能够响应的所有系统调用回调函数。每个成员都是一个函数指针,指向驱动中具体的实现函数。

成员名称 对应系统调用 典型用途 性能关注点
.owner — 设置为 THIS_MODULE,防止模块被卸载时正在使用 无
.open open() 初始化设备、分配资源、递增引用计数 避免在 open 中做耗时初始化
.release close() 释放资源、递减引用计数 确保资源正确释放,防止泄漏
.read read() 从设备读取数据到用户空间 数据拷贝效率、锁粒度
.write write() 从用户空间写入数据到设备 数据拷贝效率、锁粒度
.unlocked_ioctl ioctl() 设备控制命令(配置、状态查询等) 避免在 ioctl 中执行长时间阻塞操作
.compat_ioctl ioctl() (32位兼容) 64位内核处理32位应用程序的 ioctl 数据转换开销
.mmap mmap() 将设备内存映射到用户空间,实现零拷贝 高性能场景首选,减少数据拷贝
.poll poll() / select() / epoll() 查询设备是否可读/可写,支持非阻塞 I/O 高效事件驱动,避免轮询
性能优化提示: 在后续章节中,我们将重点优化 .read、.write 和 .unlocked_ioctl 这三个回调函数。通过 CPU 亲和性绑定,可以显著减少这些回调函数执行时的缓存抖动和上下文切换开销。

1.3 模块加载与卸载流程

Linux 驱动以内核模块(Kernel Module)的形式存在,可以动态加载和卸载,无需重新编译整个内核。模块的生命周期由两个关键函数控制:module_init() 和 module_exit()。

1.3.1 模块加载流程

  1. insmod / modprobe:用户空间工具将 .ko 文件加载到内核。
  2. 内核模块加载器:执行模块的入口函数(由 module_init() 指定)。
  3. 资源分配:在入口函数中,驱动通常完成以下操作:
    • 分配主设备号(register_chrdev_region() 或 alloc_chrdev_region())
    • 初始化 cdev 结构体(cdev_init())
    • 将 cdev 添加到内核(cdev_add())
    • 创建设备类(class_create())和设备节点(device_create())
    • 初始化硬件(GPIO 请求、时钟使能、中断注册等)
  4. 设备节点创建:在 /dev/ 目录下生成设备文件,用户空间即可通过该文件访问设备。

1.3.2 模块卸载流程

  1. rmmod:用户空间工具触发模块卸载。
  2. 内核模块卸载器:执行模块的出口函数(由 module_exit() 指定)。
  3. 资源释放:在出口函数中,驱动必须按加载的逆序释放所有资源:
    • 销毁设备节点和类(device_destroy()、class_destroy())
    • 从内核移除 cdev(cdev_del())
    • 释放设备号(unregister_chrdev_region())
    • 释放硬件资源(GPIO 释放、时钟关闭、中断释放等)
  4. 模块从内核中移除:所有引用计数归零后,模块代码从内核地址空间卸载。
模块加载与卸载流程 加载流程 (insmod) 1. insmod / modprobe 2. module_init() 入口函数 3. 分配资源 / 注册设备 4. 创建 /dev/ 设备节点 卸载流程 (rmmod) 1. rmmod 2. module_exit() 出口函数 3. 销毁节点 / 释放资源 4. 模块从内核移除 关键注意事项 • 加载和卸载必须成对出现,资源分配与释放必须一一对应 • 卸载时如果设备被占用(引用计数 > 0),内核会阻止卸载 • 使用 lsmod 查看已加载模块,modinfo 查看模块信息 • 性能优化中,模块加载/卸载本身不是瓶颈,但资源分配策略会影响运行时性能

1.4 最小字符设备驱动代码示例

以下是一个最简的字符设备驱动模板,展示了 file_operations 的注册和模块生命周期管理。后续章节的性能优化将基于此模板展开。

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

#define DEVICE_NAME "my_char_dev"
#define CLASS_NAME  "my_class"
#define BUFFER_SIZE 1024

static int major;
static struct class *dev_class;
static struct cdev my_cdev;
static char device_buffer[BUFFER_SIZE];

/* open 回调 */
static int my_open(struct inode *inode, struct file *file) {
    pr_info("Device opened\n");
    return 0;
}

/* release 回调 */
static int my_release(struct inode *inode, struct file *file) {
    pr_info("Device closed\n");
    return 0;
}

/* read 回调 */
static ssize_t my_read(struct file *file, char __user *user_buf,
                       size_t count, loff_t *ppos) {
    size_t bytes_to_read = min(count, (size_t)BUFFER_SIZE - *ppos);
    if (bytes_to_read == 0) return 0;

    if (copy_to_user(user_buf, device_buffer + *ppos, bytes_to_read)) {
        return -EFAULT;
    }
    *ppos += bytes_to_read;
    return bytes_to_read;
}

/* write 回调 */
static ssize_t my_write(struct file *file, const char __user *user_buf,
                        size_t count, loff_t *ppos) {
    size_t bytes_to_write = min(count, (size_t)BUFFER_SIZE - *ppos);
    if (bytes_to_write == 0) return -ENOSPC;

    if (copy_from_user(device_buffer + *ppos, user_buf, bytes_to_write)) {
        return -EFAULT;
    }
    *ppos += bytes_to_write;
    return bytes_to_write;
}

/* file_operations 结构体定义 */
static struct file_operations fops = {
    .owner   = THIS_MODULE,
    .open    = my_open,
    .release = my_release,
    .read    = my_read,
    .write   = my_write,
};

/* 模块入口函数 */
static int __init my_driver_init(void) {
    dev_t dev_num;

    /* 1. 动态分配主设备号 */
    if (alloc_chrdev_region(&dev_num, 0, 1, DEVICE_NAME) < 0) {
        pr_err("Failed to allocate device number\n");
        return -1;
    }
    major = MAJOR(dev_num);
    pr_info("Allocated major number %d\n", major);

    /* 2. 初始化 cdev 并关联 fops */
    cdev_init(&my_cdev, &fops);
    my_cdev.owner = THIS_MODULE;

    /* 3. 将 cdev 添加到内核 */
    if (cdev_add(&my_cdev, dev_num, 1) < 0) {
        pr_err("Failed to add cdev\n");
        unregister_chrdev_region(dev_num, 1);
        return -1;
    }

    /* 4. 创建设备类 */
    dev_class = class_create(THIS_MODULE, CLASS_NAME);
    if (IS_ERR(dev_class)) {
        pr_err("Failed to create class\n");
        cdev_del(&my_cdev);
        unregister_chrdev_region(dev_num, 1);
        return PTR_ERR(dev_class);
    }

    /* 5. 创建设备节点 */
    if (device_create(dev_class, NULL, dev_num, NULL, DEVICE_NAME) == NULL) {
        pr_err("Failed to create device\n");
        class_destroy(dev_class);
        cdev_del(&my_cdev);
        unregister_chrdev_region(dev_num, 1);
        return -1;
    }

    pr_info("Driver loaded successfully\n");
    return 0;
}

/* 模块出口函数 */
static void __exit my_driver_exit(void) {
    dev_t dev_num = MKDEV(major, 0);

    device_destroy(dev_class, dev_num);
    class_destroy(dev_class);
    cdev_del(&my_cdev);
    unregister_chrdev_region(dev_num, 1);

    pr_info("Driver unloaded successfully\n");
}

module_init(my_driver_init);
module_exit(my_driver_exit);

MODULE_LICENSE("GPL");
MODULE_AUTHOR("Embedded Linux Expert");
MODULE_DESCRIPTION("Minimal character device driver template");
常见陷阱: 在 module_exit() 中,资源释放的顺序必须与 module_init() 中资源分配的顺序完全相反。如果顺序颠倒,可能导致内核崩溃或资源泄漏。例如,先销毁设备节点,再销毁类,最后释放设备号。

1.5 本章小结