1. Linux 驱动基础回顾:字符设备驱动框架、file_operations 结构体、模块加载与卸载流程
在嵌入式 Linux 系统中,字符设备驱动是最基础、最常用的驱动类型。无论是 GPIO、UART、I2C 还是 SPI,底层都遵循字符设备驱动的统一框架。理解这一框架是后续进行性能优化和 CPU 亲和性绑定的前提。
1.1 字符设备驱动核心框架
字符设备驱动的核心思想是将硬件操作抽象为文件操作。用户空间通过 open()、read()、write()、ioctl() 等标准系统调用与设备交互,而驱动则负责将这些调用映射到具体的硬件寄存器操作。
关键概念: 在 Linux 中,“一切皆文件”。字符设备通过主设备号(Major Number)和次设备号(Minor Number)在系统中唯一标识。主设备号对应驱动,次设备号对应具体设备实例。
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 模块加载流程
- insmod / modprobe:用户空间工具将 .ko 文件加载到内核。
- 内核模块加载器:执行模块的入口函数(由
module_init()指定)。 - 资源分配:在入口函数中,驱动通常完成以下操作:
- 分配主设备号(
register_chrdev_region()或alloc_chrdev_region()) - 初始化 cdev 结构体(
cdev_init()) - 将 cdev 添加到内核(
cdev_add()) - 创建设备类(
class_create())和设备节点(device_create()) - 初始化硬件(GPIO 请求、时钟使能、中断注册等)
- 分配主设备号(
- 设备节点创建:在
/dev/目录下生成设备文件,用户空间即可通过该文件访问设备。
1.3.2 模块卸载流程
- rmmod:用户空间工具触发模块卸载。
- 内核模块卸载器:执行模块的出口函数(由
module_exit()指定)。 - 资源释放:在出口函数中,驱动必须按加载的逆序释放所有资源:
- 销毁设备节点和类(
device_destroy()、class_destroy()) - 从内核移除 cdev(
cdev_del()) - 释放设备号(
unregister_chrdev_region()) - 释放硬件资源(GPIO 释放、时钟关闭、中断释放等)
- 销毁设备节点和类(
- 模块从内核中移除:所有引用计数归零后,模块代码从内核地址空间卸载。
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() 中资源分配的顺序完全相反。如果顺序颠倒,可能导致内核崩溃或资源泄漏。例如,先销毁设备节点,再销毁类,最后释放设备号。