2. Linux 驱动基础回顾:模块加载/卸载、file_operations 结构体、字符设备驱动框架
在深入 Platform 驱动与设备树匹配机制之前,必须牢固掌握 Linux 驱动开发的基础。本章将系统回顾三个核心支柱:模块的加载与卸载、file_operations 结构体以及字符设备驱动框架。这些概念是所有复杂驱动(包括 Platform 驱动)的基石。
2.1 内核模块:加载与卸载
Linux 驱动通常以内核模块(Kernel Module)的形式存在,这意味着它们可以在系统运行时动态加载到内核或从内核卸载,无需重新编译整个内核或重启系统。
2.1.1 模块的入口与出口
每个内核模块都必须定义两个关键函数:
- 入口函数:模块被加载时执行,通常用于初始化资源、注册驱动。
- 出口函数:模块被卸载时执行,用于释放入口函数申请的资源、注销驱动。
这两个函数通过特定的宏向内核声明:
#include <linux/module.h>
#include <linux/init.h>
// 模块入口函数
static int __init my_driver_init(void)
{
printk(KERN_INFO "My driver loaded successfully.\n");
// 在此处进行硬件初始化、注册设备等操作
return 0;
}
// 模块出口函数
static void __exit my_driver_exit(void)
{
printk(KERN_INFO "My driver unloaded.\n");
// 在此处释放资源、注销设备
}
module_init(my_driver_init);
module_exit(my_driver_exit);
MODULE_LICENSE("GPL");
MODULE_AUTHOR("Your Name");
MODULE_DESCRIPTION("A simple example driver module");
提示:
__init 和 __exit 标记告诉内核,这些函数在初始化或卸载完成后,其占用的内存可以被回收,以节省系统资源。
2.1.2 模块操作命令
用户空间通过以下工具与内核模块交互:
| 命令 | 功能 | 内核行为 |
|---|---|---|
insmod my_driver.ko |
加载模块 | 调用模块的 module_init() 指定的函数 |
rmmod my_driver |
卸载模块 | 调用模块的 module_exit() 指定的函数 |
modprobe my_driver |
智能加载模块 | 自动处理模块依赖关系,并调用 module_init() |
lsmod |
列出已加载模块 | 读取 /proc/modules 信息 |
注意: 使用
insmod 加载模块时,如果模块依赖其他模块,需要先手动加载依赖。而 modprobe 会自动处理这些依赖关系,是更推荐的方式。
2.2 file_operations 结构体:驱动与用户空间的桥梁
在 Linux 中,“一切皆文件”。用户空间程序通过标准的文件操作(如 open, read, write, close)与硬件设备通信。驱动开发者通过填充 struct file_operations 结构体,将这些系统调用与具体的硬件操作函数绑定起来。
2.2.1 核心成员解析
file_operations 结构体定义在 <linux/fs.h> 中,包含大量函数指针。以下是驱动开发中最常用的几个:
| 成员 | 系统调用 | 驱动实现职责 |
|---|---|---|
.owner |
— | 通常设置为 THIS_MODULE,防止模块在使用中被卸载 |
.open |
open() |
打开设备,进行权限检查、初始化硬件状态 |
.release |
close() |
关闭设备,释放打开时申请的资源 |
.read |
read() |
从设备读取数据到用户空间缓冲区 |
.write |
write() |
从用户空间缓冲区写入数据到设备 |
.unlocked_ioctl |
ioctl() |
执行设备特定的控制命令(无大内核锁) |
2.2.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 len, loff_t *off);
static ssize_t my_dev_write(struct file *filp, const char __user *buf, size_t len, loff_t *off);
static long my_dev_ioctl(struct file *filp, unsigned int cmd, unsigned long arg);
// 定义 file_operations 结构体
static const struct file_operations my_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,
};
关键点: 驱动不直接实现系统调用,而是实现
file_operations 中的回调函数。当用户程序调用 read() 时,VFS(虚拟文件系统)根据设备文件关联的 file_operations,自动调用驱动中的 my_dev_read 函数。
2.3 字符设备驱动框架:从注册到注销
字符设备是 Linux 中最基本的设备类型,以字节流的形式进行数据传输。实现一个完整的字符设备驱动,需要遵循一套标准的框架流程。
2.3.1 核心流程与数据结构
字符设备驱动的生命周期涉及三个核心步骤:分配设备号、初始化并注册 cdev、创建设备文件。
2.3.2 完整代码框架示例
以下代码片段展示了字符设备驱动框架的核心骨架:
#include <linux/module.h>
#include <linux/fs.h>
#include <linux/cdev.h>
#include <linux/device.h>
#define DEVICE_NAME "my_char_dev"
#define CLASS_NAME "my_class"
static int major;
static struct class *my_class;
static struct device *my_device;
static struct cdev my_cdev;
// 设备操作函数(简化)
static int my_open(struct inode *inode, struct file *filp) { return 0; }
static int my_release(struct inode *inode, struct file *filp) { return 0; }
static struct file_operations my_fops = {
.owner = THIS_MODULE,
.open = my_open,
.release = my_release,
};
static int __init my_init(void)
{
dev_t dev_num;
// 1. 动态分配设备号
alloc_chrdev_region(&dev_num, 0, 1, DEVICE_NAME);
major = MAJOR(dev_num);
// 2. 初始化并注册 cdev
cdev_init(&my_cdev, &my_fops);
cdev_add(&my_cdev, dev_num, 1);
// 3. 在 /sys/class 下创建类,并在 /dev 下创建设备节点
my_class = class_create(THIS_MODULE, CLASS_NAME);
my_device = device_create(my_class, NULL, dev_num, NULL, DEVICE_NAME);
printk(KERN_INFO "Char device registered with major %d\n", major);
return 0;
}
static void __exit my_exit(void)
{
dev_t dev_num = MKDEV(major, 0);
// 反向操作:销毁设备、类、注销 cdev、释放设备号
device_destroy(my_class, dev_num);
class_destroy(my_class);
cdev_del(&my_cdev);
unregister_chrdev_region(dev_num, 1);
printk(KERN_INFO "Char device unregistered\n");
}
module_init(my_init);
module_exit(my_exit);
MODULE_LICENSE("GPL");
2.3.3 框架步骤总结
- 分配设备号:使用
alloc_chrdev_region()动态分配,避免与现有设备号冲突。 - 初始化 cdev:使用
cdev_init()将cdev结构与file_operations绑定。 - 注册 cdev:使用
cdev_add()将字符设备通知给 VFS。 - 创建设备文件:通过
class_create()和device_create()在/dev目录下自动生成设备节点,方便用户空间访问。 - 卸载时逆序释放:严格按照注册的相反顺序销毁设备、类、注销 cdev 并释放设备号。
核心思想: 字符设备驱动框架的本质是“注册回调”。驱动将
file_operations 注册到内核,内核在用户程序操作设备文件时,回调驱动中对应的函数。这种机制将硬件操作与系统调用解耦,是 Linux 驱动设计的精髓。
掌握本章的模块、file_operations 和字符设备框架,是理解后续 Platform 驱动的基础。Platform 驱动本质上是对字符设备框架的进一步封装和抽象,用于管理那些不挂载在传统总线(如 PCI、USB)上的设备。