0
  • 聊天消息
  • 系统消息
  • 评论与回复
登录后你可以
  • 下载海量资料
  • 学习在线课程
  • 观看技术视频
  • 写文章/发帖/加入社区
会员中心
创作中心

完善资料让更多小伙伴认识你,还能领取20积分哦,立即完善>

3天内不再提示

如何理解Linux内核中的PCIe驱动

FPGA技术江湖 来源:AdriftCoreFPGA芯研社 2026-04-11 17:22 次阅读
加入交流群
微信小助手二维码

扫码添加小助手

加入工程师交流群

以下文章来源于AdriftCoreFPGA芯研社,作者CNL中子

前言

我们习惯了用 Verilog 去死磕 PCIe 的底层协议状态机。但一旦越过硬件边界来到操作系统层面,Linux 内核是如何接管并驱动这些 PCI/PCIe 设备的呢?由于不同的 CPU 架构实现了各异的芯片组,加上各种 PCI 设备自身独特的功能需求,Linux 内核中的 PCI 支持远比我们希望的要复杂得多。今天这篇文章,我们将从驱动开发的视角,梳理 Linux PCI 设备驱动的核心生命周期与关键 API

驱动注册和发现

在 Linux 中,PCI 驱动程序通过pci_register_driver()在系统中发现设备。但实际上,这个过程是反向的:当 PCI 通用代码发现了一个新设备时,具有匹配描述的驱动程序才会被内核通知 。

系统上电或有 PCIe 设备热插拔时,底层的总线枚举其实已经完成了。Linux 的 PCI 通用代码会去扫描物理总线,读取每个设备的配置空间(拿到 Vendor ID, Device ID 等),把系统里所有的硬件设备都登记在自己的花名册上。这时候,你的驱动程序可能还没加载进系统。

当你的驱动程序跑起来,调用pci_register_driver()进行注册时,它并不是去物理总线上找设备。相反,它只是向内核提交了一份匹配描述(也就是包含它能支持的 Vendor ID 和 Device ID 的id_table)。

内核会拿着你提交的匹配描述,去自己早就登记好的设备花名册里比对。一旦内核发现:总线上有个硬件设备的 ID,刚好和这个驱动要求的 ID 对上了;内核就会主动触发(通知)驱动程序里写好的probe探测函数,并把指向该硬件设备的指针(struct pci_dev *)塞给驱动 。

staticstructpci_drivermy_pci_driver={
  .name ="my_pci_driver",
  .id_table = my_driver_id_table,
  .probe = my_probe_function,
  .remove = my_remove_function,
};

匹配描述

在传给注册函数pci_register_driver()的struct pci_driver结构体中,有一个名为id_table的字段,它就是一个指向驱动程序感兴趣的设备 ID 表的指针。

这个 ID 表是一个struct pci_device_id类型的数组,并且必须以一个全零的条目作为结束标志。通常建议将其定义为static const。

在实际写代码时,你不需要手动去填充上面所有的字段。大多数驱动程序只需要使用宏PCI_DEVICE()或PCI_DEVICE_CLASS()就可以非常方便地设置pci_device_id表了 。

staticconststructpci_device_idmy_driver_id_table[] ={
  { PCI_DEVICE(0x10EC,0x8168) },/* 使用宏,只匹配具体的 Vendor ID 和 Device ID */
  {0, }/* 必须以全 0 结尾,告诉内核数组到此为止 */
};
MODULE_DEVICE_TABLE(pci, my_driver_id_table);/* 导出表 */

staticstructpci_drivermy_pci_driver={
  ...
  .id_table = my_driver_id_table,
  ...
};

驱动名称(name)

这是驱动程序必不可少的身份标识。当你注册驱动后,内核会使用这个名字在 sysfs 文件系统中创建对应的目录(例如/sys/bus/pci/drivers/AdriftCorePCIe/)。后续如果在运行时动态添加新的设备 ID 到驱动中,也会用到这个名字对应的路径。

#defineDEVICE_NAME"AdriftCorePCIe"

staticstructpci_drivermy_pci_driver={
  .name = DEVICE_NAME
  ...
  ...
};

探测与初始化(probe)

这是驱动认领设备的入口。

当内核发现了一个与你的驱动程序id_table匹配的、且尚未被其他驱动占有的 PCI 设备时,就会调用这个探测函数。这可能发生在执行pci_register_driver()的过程中(如果设备已经存在),或者在稍后插入新设备时触发。

在这个阶段,probe的核心动作包括:

接收设备指针:内核会为每一个 ID 表匹配的设备,将一个struct pci_dev *指针传递给该函数。

决定是否接管:如果驱动程序决定接受并获取该设备的所有权,probe必须返回零;如果无法接管,则返回一个负数的错误码。

允许睡眠上下文:probe函数始终在进程上下文 (process context) 中被调用,因此它是允许睡眠的。这对于后续申请内存或长时间等待硬件就绪非常关键。

一旦probe决定接管设备并获得了所有权,驱动程序通常需要在这个函数内部执行以下标准的初始化步骤:

启用设备 (Enable the device)

请求 MMIO/IOP 资源 (Request MMIO/IOP resources)

设置 DMA 掩码大小 (Set the DMA mask size):包含一致性 (coherent) 和流式 (streaming) DMA

分配并初始化共享控制数据 (Allocate and initialize shared control data):通常使用pci_allocate_coherent()

访问设备配置空间 (Access device configuration space):在需要的情况下执行

注册 IRQ 处理程序 (Register IRQ handler):通过request_irq()完成

初始化非 PCI 部分 (Initialize non-PCI):例如初始化芯片中的 LAN、SCSI 等特定功能模块

启用 DMA/处理引擎 (Enable DMA/processing engines)

probe就是你的驱动程序正式登台亮相的地方。内核把匹配的硬件交到它手上,它负责把这块硬件通电、申请资源、配中断、设 DMA,最终完成点亮,让系统能够真正使用这块硬件。

/* * 2. Probe 函数:设备的接管与点亮
* 该函数在进程上下文中调用,允许睡眠。
*/
staticintmy_pci_probe(structpci_dev *pdev,conststructpci_device_id *id)
{
  interr;

  /* 2.1 启用 PCI 设备 (唤醒设备、分配资源、分配 IRQ 等) */
  err = pci_enable_device(pdev);
  if(err) {
    dev_err(&pdev->dev,"Failed to enable PCI device
");
    returnerr;/* 接管失败,返回负数错误码 */
  }

  /* 2.2 请求 MMIO/IOP 资源,防止与其他设备发生地址冲突 */
  /* 注意:现代内核通常使用 pci_request_regions 包装函数 */
  err = pci_request_regions(pdev,"my_pci_driver");
  if(err) {
    dev_err(&pdev->dev,"Failed to request PCI regions
");
    gotoerr_disable_device;
  }

  /* 2.3 设置 DMA 掩码:声明设备的 DMA 寻址能力 (例如支持 64 位 DMA) */
  err = pci_set_dma_mask(pdev, DMA_BIT_MASK(64));
  if(err) {
    dev_err(&pdev->dev,"No suitable DMA available
");
    gotoerr_release_regions;
  }
  pci_set_consistent_dma_mask(pdev, DMA_BIT_MASK(64));

  /* 2.4 启用 DMA 总线主控模式 (设置 PCI_COMMAND 寄存器中的 Bus Master 位) */
  pci_set_master(pdev);

  /* * 后续初始化步骤(伪代码):
  * - 映射 MMIO 寄存器空间 (pci_iomap)
  * - 配置 MSI/MSI-X 中断 (pci_alloc_irq_vectors)
  * - 注册中断处理程序 (request_irq)
  * - 初始化你的 DPU/硬件引擎状态机
  */

  dev_info(&pdev->dev,"PCI device probed successfully!
");
  return0;/* 成功接管设备,返回 0 */

err_release_regions:
  pci_release_regions(pdev);
err_disable_device:
  pci_disable_device(pdev);
  returnerr;
}

设备的正确关闭与清理

如果说probe是驱动接管设备的入场仪式,那么remove就是它的优雅退场与资源回收大管家。

在内核的生命周期中,remove的主要职责就是完全反向执行probe中所做的一切,确保设备被安全关闭,并且所有占用的系统资源都被彻底释放,不留任何内存泄漏或导致系统崩溃的隐患。

remove()函数会在由该驱动程序处理的设备被移除时调用。这通常发生在两种场景:

驱动被注销时:比如当驱动程序退出(执行pci_unregister_driver()),或者你通过命令行(如rmmod)卸载驱动模块时,PCI 层会自动为该驱动处理的所有设备调用remove钩子。

设备被物理拔出时:当设备从支持热插拔 (hot-pluggable) 的插槽中被手动拔出时。

和probe一样,remove函数始终在进程上下文 (process context)中被调用,因此它是允许睡眠 (sleep) 的。

当模块需要被卸载或者设备不再使用时,remove函数通常需要严格按照以下步骤进行清理:

禁用设备生成中断 (Disable the device from generating IRQs):这是第一步,必须阻止芯片产生新的中断。如果不做这一步,且中断号是与其他设备共享的,可能会引发致命的尖叫中断 (screaming interrupt)问题 。

释放 IRQ (Release the IRQ):调用free_irq()来注销中断处理程序。

停止所有 DMA 活动 (Stop all DMA activity):在尝试释放 DMA 控制数据之前,停止所有 DMA 操作极其重要。如果未能停止 DMA 就直接释放内存,可能会导致内存损坏、系统挂起,甚至在某些芯片组上发生硬崩溃 ^^。

释放 DMA 缓冲区 (Release DMA buffers):包括流式 (streaming) 和一致性 (coherent) DMA 缓冲区的清理与解除映射。

从其他子系统注销 (Unregister from other subsystems):比如解绑相关的 SCSI 或网络设备 (netdev)。

禁用设备及释放区域:

• 禁用设备对 MMIO/IO 端口地址的响应。

• 释放 MMIO/IOP 资源。

/* * 3. Remove 函数:设备的优雅退场与资源回收
* 必须严格反向执行 probe 中的分配步骤。
*/
staticvoidmy_pci_remove(structpci_dev *pdev)
{
  /* * 卸载前期的关键清理(伪代码):
  * - 停止设备侧的数据收发与引擎运转
  * - 停止设备产生中断,并释放 IRQ (free_irq)
  * - 停止所有 DMA 活动,释放 DMA 缓冲区
  * - 解除 MMIO 空间映射 (pci_iounmap)
  */

  /* 3.1 释放 MMIO/IOP 资源区域 */
  pci_release_regions(pdev);

  /* 3.2 禁用 PCI 设备响应,与 pci_enable_device 对称相反 */
  pci_disable_device(pdev);

  dev_info(&pdev->dev,"PCI device removed successfully.
");
}

remove就是负责擦屁股的。它必须严丝合缝地把probe里申请的内存还给系统,把注册的中断注销掉,把开启的 DMA 停下来,最后让硬件安安静静地进入关闭状态。

整个驱动模块的执行

在 Linux 内核驱动的架构中,如果把 probe 和 remove 比作针对单个具体 PCIe 硬件的上岗和下岗,那么module_init和module_exit就是整个驱动程序模块本身的出生和消亡。

在早期的内核代码中,你通常需要手动编写这两个函数,看起来像这样:

staticint__initmy_pci_init(void)
{
  /* 向内核的 PCI 核心注册你的驱动结构体 */
  returnpci_register_driver(&my_pci_driver);
}

staticvoid__exitmy_pci_exit(void)
{
  /* 注销驱动,这会自动触发所有已接管设备的 remove 函数 */
  pci_unregister_driver(&my_pci_driver);
}

module_init(my_pci_init);
module_exit(my_pci_exit);

驱动的出生与注册

当你通过 insmod 或 modprobe 命令将编译好的 .ko 驱动模块加载到内核时,系统第一个调用的就是 module_init 宏指定的初始化函数。

在 PCIe 驱动中,它主要干一件事:向内核注册自己。

PCI 设备驱动程序会在其初始化期间调用 pci_register_driver(),并传入指向描述该驱动程序的结构体(struct pci_driver)的指针 。这就好比去内核那里挂号,把自己的 id_table、probe 和 remove 提交给内核。

注:module_init() 函数(以及仅由它调用的所有初始化函数)应该被标记为 __init 属性 。这个属性非常巧妙,它告诉内核:这段初始化代码在驱动完成初始化后就可以被直接丢弃,从而节省宝贵的内核内存空间 。

驱动的消亡与注销

当你通过rmmod命令卸载驱动模块时,内核会调用module_exit宏指定的退出函数。

在 PCIe 驱动中,它的核心任务是:向内核注销自己,并引发连锁清理。

调用核心 API:当驱动程序退出时,它只需调用pci_unregister_driver()。

连锁触发remove:这是非常省心的一点。当你调用注销函数后,PCI 层会自动去寻找所有目前正由该驱动程序处理的设备,并自动为它们逐一调用remove钩子函数。这意味着你不需要在module_exit里手动写循环去清理设备,内核全帮你包办了。

属性标记:与初始化对应,退出函数应该被标记为__exit属性。对于那些并非以模块形式动态加载,而是直接编译进内核镜像(非模块化)的驱动来说,带有__exit标记的代码会被直接忽略,因为它永远不会被卸载。

写在最后

习惯了使用 Verilog 雕琢 PCIe 的底层状态机,再回过头来看看 Linux 内核是如何以软件的视角接管这些硬件的,是一件非常有趣的事情。

正如我们在文中所看到的,Linux 并没有让驱动程序去干满大街找设备的脏活累活。相反,它构建了一个极其优雅的总线-设备-驱动模型:底层总线负责枚举和登记(发现硬件),驱动模块负责提交自己的匹配描述并注册(module_init与id_table),而内核的 PCI Core 则扮演了红娘的角色,精准地将匹配的设备交到驱动的probe函数手中进行点亮,最后在卸载时通过remove和module_exit实现系统资源的完美回收。

从 RTL 侧的代码逻辑,跨越物理层与链路层,最终到达操作系统的驱动框架,这种从底至顶的完整视角,能让我们在遇到复杂的系统级 Bug 时拥有更清晰的排查思路。硬件不只是冷冰冰的寄存器,软件也不只是虚无缥缈的指针,它们的完美交汇,才是系统稳定运行的基石。

声明:本文内容及配图由入驻作者撰写或者入驻合作网站授权转载。文章观点仅代表作者本人,不代表电子发烧友网立场。文章及其配图仅供工程师学习之用,如有内容侵权或者其他违规问题,请联系本站处理。 举报投诉
  • 内核
    +关注

    关注

    4

    文章

    1474

    浏览量

    43088
  • Linux
    +关注

    关注

    88

    文章

    11806

    浏览量

    219508
  • PCIe
    +关注

    关注

    16

    文章

    1474

    浏览量

    88894
  • 状态机
    +关注

    关注

    2

    文章

    501

    浏览量

    29315

原文标题:漫谈PCIe之如何理解PCIe驱动

文章出处:【微信号:HXSLH1010101010,微信公众号:FPGA技术江湖】欢迎添加关注!文章转载请注明出处。

收藏 人收藏
加入交流群
微信小助手二维码

扫码添加小助手

加入工程师交流群

    评论

    相关推荐
    热点推荐

    Linux内核container_of原理详解

    Linux内核中经常可见container_of的身影,它在实际驱动的编写也是广泛应用。
    发表于 07-14 15:19 882次阅读
    <b class='flag-5'>Linux</b><b class='flag-5'>内核</b><b class='flag-5'>中</b>container_of原理详解

    请问DVR RDK自带的Linux内核已经包含了PCIE相关的驱动吗?请问有没有PC和8168通过PCIE进行通信的例子?

    RDK自带的Linux内核已经包含了PCIE相关的驱动吗?2、我们想用板卡来做计算加速,那么板卡是不是该工作于End Point模式?3、有没有PC和8168通过
    发表于 05-31 01:09

    Linux内核添加wifi驱动

    Linux内核添加wifi驱动Linux WIFI驱动实验rtl8723 Wifi联网测试
    发表于 02-05 07:59

    Linux内核教程

    本章学习目标掌握LINUX内核版本的含义理解并掌握进程的概念掌握管道的概念及实现了解内核的数据结构了解LINUX
    发表于 04-10 16:59 0次下载

    基于Linux内核输入子系统的驱动研究

    Linux因其完全开放的特性和稳定优良的性能深受欢迎,当推出了内核输入子系统后,更方便了嵌入式领域的驱动开放。介绍了Linux的设备驱动基础
    发表于 09-12 16:38 23次下载

    linux2.6内核设备驱动模型精华

    linux 内核驱动部分详解
    发表于 04-27 10:43 20次下载

    基于Xilinx PCIe例程附带Linux驱动的修改

    本文档内容介绍了基于Xilinx PCIe例程附带Linux驱动的修改,供参考。
    发表于 09-15 16:38 23次下载

    Linux内核输入子系统的驱动研究

    Linux内核输入子系统的驱动研究
    发表于 10-31 14:41 14次下载
    <b class='flag-5'>Linux</b><b class='flag-5'>内核</b>输入子系统的<b class='flag-5'>驱动</b>研究

    米尔科技深入理解LINUX内核简介

    为了透彻理解Linux的工作机理,以及为何它在各种系统上能顺畅运行,你需要深入到内核的心脏。
    的头像 发表于 11-25 09:34 2496次阅读
    米尔科技深入<b class='flag-5'>理解</b><b class='flag-5'>LINUX</b><b class='flag-5'>内核</b>简介

    快速理解什么是Linux内核以及Linux内核的内容

    01 前言 本文主要讲解什么是Linux内核,以及通过多张图片展示Linux内核的作用与功能,以便于读者能快速理解什么是
    的头像 发表于 10-21 12:02 5154次阅读
    快速<b class='flag-5'>理解</b>什么是<b class='flag-5'>Linux</b><b class='flag-5'>内核</b>以及<b class='flag-5'>Linux</b><b class='flag-5'>内核</b>的内容

    如何使用Linux内核实现USB驱动程序框架

    Linux内核提供了完整的USB驱动程序框架。USB总线采用树形结构,在一条总线上只能有唯一的主机设备。 Linux内核从主机和设备两个角度
    发表于 11-06 17:59 20次下载
    如何使用<b class='flag-5'>Linux</b><b class='flag-5'>内核</b>实现USB<b class='flag-5'>驱动</b>程序框架

    Linux内核代码60%都是驱动

    为什么Linux内核代码60%都是驱动? 如果每支持新的设备就加入驱动内核会不会变得越来越臃肿?
    的头像 发表于 07-11 11:48 2011次阅读
    <b class='flag-5'>Linux</b><b class='flag-5'>内核</b>代码60%都是<b class='flag-5'>驱动</b>?

    linux内核的driver_register介绍

    linux内核注册驱动由driver_register()完成。它将驱动程序的信息添加到内核驱动
    的头像 发表于 07-14 09:17 4659次阅读
    <b class='flag-5'>linux</b><b class='flag-5'>内核</b><b class='flag-5'>中</b>的driver_register介绍

    linux驱动程序如何加载进内核

    Linux系统驱动程序是内核与硬件设备之间的桥梁。它们允许内核与硬件设备进行通信,从而实现对硬件设备的控制和管理。
    的头像 发表于 08-30 15:02 1939次阅读

    linux内核通用HID触摸驱动

    linux内核,为HID触摸面板实现了一个通用的驱动程序,位于/drivers/hid/hid-multitouch.c文件。hid触
    的头像 发表于 10-29 10:55 3868次阅读
    <b class='flag-5'>linux</b><b class='flag-5'>内核</b><b class='flag-5'>中</b>通用HID触摸<b class='flag-5'>驱动</b>