2. 内核驱动框架:module_init/module_exit、file_operations 结构体、设备号注册
PCIe 设备驱动本质上是遵循 Linux 内核驱动框架的内核模块。理解 module_init/module_exit 的加载/卸载机制、file_operations 结构体提供的用户态交互接口,以及设备号注册的静态与动态策略,是编写和调试 PCIe 驱动的基石。本节将拆解这三个核心要素,并展示它们如何协同工作。
2.1 模块入口与出口:module_init / module_exit
每个内核驱动模块都必须声明初始化函数和清理函数。宏 module_init 和 module_exit 将这两个函数注册到内核模块加载/卸载框架中。
| 宏 | 触发时机 | 典型职责 | 返回值 |
|---|---|---|---|
module_init(x) |
insmod 加载模块时 |
注册设备号、初始化数据结构、映射 BAR 空间、注册中断 | 0 成功;负值失败 |
module_exit(x) |
rmmod 卸载模块时 |
注销设备号、释放内存、取消中断注册、解除 BAR 映射 | void |
调试要点: 如果
module_init 返回非零值,内核会立即卸载模块,不会调用 module_exit。此时已分配的资源(如已注册的设备号)必须由 init 函数在返回错误前自行释放,否则会造成资源泄漏。
// 典型的 PCIe 驱动模块入口/出口骨架
#include <linux/module.h>
#include <linux/pci.h>
static int __init my_pcie_init(void)
{
int ret;
pr_info("my_pcie: module loaded\n");
// 1. 分配/注册设备号
// 2. 探测 PCIe 设备
// 3. 初始化 file_operations
return 0;
}
static void __exit my_pcie_exit(void)
{
// 1. 注销设备号
// 2. 释放 PCIe 设备
// 3. 清理 file_operations
pr_info("my_pcie: module unloaded\n");
}
module_init(my_pcie_init);
module_exit(my_pcie_exit);
MODULE_LICENSE("GPL");
MODULE_DESCRIPTION("PCIe Driver Example");
调试技巧: 在 init 函数中每步分配资源后立即打印日志,并在错误路径上逐级释放。使用
goto 标签(如 err_region、err_irq)是内核驱动中常见的错误处理模式。
2.2 file_operations 结构体:用户态与内核态的桥梁
PCIe 驱动通常通过字符设备向用户态暴露控制接口。file_operations 结构体定义了用户态程序通过 open、read、write、ioctl、mmap 等系统调用与驱动交互的回调函数。
| 成员 | 系统调用 | PCIe 驱动典型用途 |
|---|---|---|
.open |
open() |
递增设备引用计数,初始化每打开文件的数据 |
.release |
close() |
递减引用计数,清理每打开文件的数据 |
.read |
read() |
从 PCIe BAR 空间或 DMA 缓冲区读取数据 |
.write |
write() |
向 PCIe BAR 空间或配置寄存器写入数据 |
.unlocked_ioctl |
ioctl() |
执行自定义控制命令(如复位、中断使能、DMA 启动) |
.mmap |
mmap() |
将 PCIe BAR 空间直接映射到用户态地址空间 |
// file_operations 结构体定义示例
static int my_pcie_open(struct inode *inode, struct file *filp)
{
// 从 inode 获取设备私有数据
struct my_dev *dev = container_of(inode->i_cdev, struct my_dev, cdev);
filp->private_data = dev;
return 0;
}
static ssize_t my_pcie_read(struct file *filp, char __user *buf,
size_t count, loff_t *f_pos)
{
struct my_dev *dev = filp->private_data;
// 从 PCIe BAR 读取数据并拷贝到用户空间
// 使用 copy_to_user() 确保安全
return count;
}
static long my_pcie_ioctl(struct file *filp, unsigned int cmd,
unsigned long arg)
{
struct my_dev *dev = filp->private_data;
// 根据 cmd 执行不同操作
return 0;
}
static const struct file_operations my_pcie_fops = {
.owner = THIS_MODULE,
.open = my_pcie_open,
.release = my_pcie_release,
.read = my_pcie_read,
.write = my_pcie_write,
.unlocked_ioctl = my_pcie_ioctl,
.mmap = my_pcie_mmap,
};
调试陷阱: 在
read/write 回调中,必须使用 copy_to_user/copy_from_user 访问用户空间缓冲区,禁止直接解引用用户指针。忘记检查 copy_to_user 返回值会导致部分数据未拷贝而用户态不知情。
2.3 设备号注册:静态分配 vs 动态分配
每个字符设备在系统中由主设备号(major)和次设备号(minor)唯一标识。PCIe 驱动必须在 module_init 中注册设备号,并在 module_exit 中注销。
| 策略 | API | 优点 | 缺点 | 适用场景 |
|---|---|---|---|---|
| 静态分配 | register_chrdev_region() |
设备号固定,方便创建节点 | 需提前申请,可能冲突 | 广泛使用的标准驱动 |
| 动态分配 | alloc_chrdev_region() |
避免冲突,自动分配 | 设备号不固定,需 /proc/devices 查询 |
开发调试阶段、私有驱动 |
// 动态分配设备号(推荐用于调试阶段)
#include <linux/fs.h>
#define DEVICE_NAME "my_pcie"
#define MINOR_COUNT 1
static int dev_major;
static dev_t dev_num;
static int __init my_pcie_init(void)
{
int ret;
// 动态分配设备号
ret = alloc_chrdev_region(&dev_num, 0, MINOR_COUNT, DEVICE_NAME);
if (ret < 0) {
pr_err("Failed to allocate device number\n");
return ret;
}
dev_major = MAJOR(dev_num);
pr_info("Allocated major number %d\n", dev_major);
// 后续:cdev_init() + cdev_add()
return 0;
}
static void __exit my_pcie_exit(void)
{
unregister_chrdev_region(dev_num, MINOR_COUNT);
pr_info("Device number unregistered\n");
}
调试技巧: 动态分配后,通过
cat /proc/devices | grep my_pcie 查看分配的主设备号。使用 mknod /dev/my_pcie c <major> 0 手动创建设备节点,或使用 udev 自动创建。
2.4 三要素协同工作流
在 PCIe 驱动中,module_init/module_exit、file_operations 和设备号注册三者必须紧密配合,形成完整的驱动生命周期。
- 加载阶段(module_init): 分配/注册设备号 → 初始化 cdev 结构体并关联 file_operations → 调用
cdev_add()将设备加入系统 → 探测 PCIe 设备并映射 BAR 空间。 - 运行阶段: 用户态通过设备节点调用 file_operations 回调,驱动通过 BAR 地址读写 PCIe 配置空间、内存空间或 I/O 空间。
- 卸载阶段(module_exit): 调用
cdev_del()移除设备 → 注销设备号 → 释放 PCIe 资源(取消 BAR 映射、释放中断、释放 DMA 缓冲区)。
调试检查清单:
- 检查
module_init中是否按顺序注册资源,错误路径是否逐级释放。 - 检查
file_operations中是否所有回调都正确处理了filp->private_data。 - 检查设备号注销是否在
cdev_del之后调用,避免竞态。 - 使用
ls -l /dev/my_pcie确认设备节点主次设备号与/proc/devices一致。