2. 内核驱动框架

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)是内核驱动中常见的错误处理模式。
模块加载/卸载生命周期 insmod 加载模块 module_init() 执行 注册设备号/探测设备 rmmod 卸载模块 module_exit() 执行 注销设备号/释放资源 init 失败时不会调用 exit

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 返回值会导致部分数据未拷贝而用户态不知情。
file_operations 调用链路 用户态程序 系统调用层 VFS 层 file_operations 回调 PCIe 硬件操作(BAR 读写)

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 自动创建。
设备号注册流程对比 静态分配 register_chrdev_region 需提前在 Documentation/devices.txt 申请 设备号固定,适合生产环境 动态分配 alloc_chrdev_region 内核自动分配未使用的主设备号 设备号不固定,适合调试阶段

2.4 三要素协同工作流

在 PCIe 驱动中,module_init/module_exit、file_operations 和设备号注册三者必须紧密配合,形成完整的驱动生命周期。

  1. 加载阶段(module_init): 分配/注册设备号 → 初始化 cdev 结构体并关联 file_operations → 调用 cdev_add() 将设备加入系统 → 探测 PCIe 设备并映射 BAR 空间。
  2. 运行阶段: 用户态通过设备节点调用 file_operations 回调,驱动通过 BAR 地址读写 PCIe 配置空间、内存空间或 I/O 空间。
  3. 卸载阶段(module_exit): 调用 cdev_del() 移除设备 → 注销设备号 → 释放 PCIe 资源(取消 BAR 映射、释放中断、释放 DMA 缓冲区)。
调试检查清单:
  • 检查 module_init 中是否按顺序注册资源,错误路径是否逐级释放。
  • 检查 file_operations 中是否所有回调都正确处理了 filp->private_data。
  • 检查设备号注销是否在 cdev_del 之后调用,避免竞态。
  • 使用 ls -l /dev/my_pcie 确认设备节点主次设备号与 /proc/devices 一致。