内核ASPM控制接口:pci_enable_aspm()、pci_disable_aspm() 等API的用途与限制

当你拿到一块PCIe设备,跑在Linux下,发现链路功耗偏高,或者反过来——链路总掉坑、丢数据,第一个想到的就是ASPM(Active State Power Management)。但ASPM不是你想开就能开、想关就能关的。它背后藏着硬件固件、PCIe核心、以及你写的驱动三者之间的拉扯。

这一节,我们就聊内核里那几个跟ASPM打交道最频繁的API。我敢说,你只要写过PCIe驱动的probe函数,就一定会碰到它们。

为什么需要专门的ASPM控制接口?

ASPM的配置寄存器挂在PCIe能力结构里,按理说直接在驱动里写点pci_write_config_byte就能搞定。但Linux内核并不鼓励驱动直接去捅那些寄存器——原因有三:

  • 策略与机制分离:内核的PCI子系统负责统一管理ASPM策略(比如通过pcie_aspm=off内核参数全局关闭),驱动不应该绕过策略。
  • 引用计数与状态跟踪:多个驱动、多个设备可能共享同一PCIe桥,乱写寄存器会导致状态混乱。内核用引用计数保证只有最后一个用户关闭时才真正禁掉。
  • 恢复时机:设备进入D3cold或系统睡眠后,ASPM设置可能丢失。内核需要帮驱动记录并自动恢复。

所以,官方接口就是这几把钥匙:pci_enable_aspm()、pci_disable_aspm()、pci_restore_aspm_state(),以及配套的pci_disable_link_state()和pci_enable_link_state()。

pci_enable_aspm() – 主动开启,但别指望一次成功

用途:尝试为指定设备开启ASPM功能(L0s和/或L1)。

原型:

int pci_enable_aspm(struct pci_dev *pdev, int state);

参数state可以传PCIE_LINK_STATE_L0S、PCIE_LINK_STATE_L1、PCIE_LINK_STATE_ALL(即两者都开)。

这个接口并不是“想开就开”的。我最开始用的时候也以为传了参数就能成功,结果返回负值,查了半天发现是固件把ASPM锁死了。说白了,pci_enable_aspm()只是向PCI核心提交一个“请求”,内核会综合以下几方面决定让不让开:

  • 内核参数pcie_aspm=off(全局禁用)
  • 平台是否通过ACPI _OSC宣称支持
  • 设备所在桥的兼容性
  • 是否有其他驱动已经关闭了该链路的ASPM

典型返回值:0表示成功,-ENOTSUPP表示不支持,-EACCES表示被策略阻止。

我遇到过一块网卡,在cold boot后调用pci_enable_aspm()能成功,但一旦热插拔后再重新probe,就返回-EACCES。后来发现是ACPI固件在热拔后把状态标记改了。嗯,这种坑得靠打一个quirk补丁绕过。

pci_disable_aspm() – 救命的关闭武器

用途:强制关闭指定设备路径上的ASPM能力。

原型:

int pci_disable_aspm(struct pci_dev *pdev);

不需要传state,因为它会把L0s和L1全关掉。

这个API是我在驱动调试中最常调用的一个。尤其是当你发现设备在高负载下莫名掉线、或者出现Uncorrectable PCIe Error时,第一个反应就是关掉ASPM看看能不能稳定。

有什么限制?

  1. 可能被固件重新打开:某些平台在链路重新训练(比如热复位)后会恢复ASPM的默认设置,你关了也没用。需要在驱动里加一个看门狗或定期刷新。
  2. 功耗增加:关闭后实测大约多出200mW~500mW,对笔记本电池续航影响明显。所以在做产品时不能无脑全关,得按设备逐个避坑。
  3. 需要配合恢复:我习惯在probe里调用pci_disable_aspm(),然后在remove里调用pci_restore_aspm_state()还回去。否则模块卸载后ASPM可能永久处于关闭状态(参考计数没清零)。

pci_restore_aspm_state() – 优雅地归还控制权

这个API你看名字就知道,是恢复ASPM状态的。它的作用是把驱动之前修改的ASPM设置还原到内核策略决定的值。

void pci_restore_aspm_state(struct pci_dev *pdev);

没什么参数,也不需要返回值。我建议在驱动的remove()函数和suspend()回调里调用它。如果你之前调用了pci_disable_aspm(),那恢复后会“重新考虑”是否开启;如果你调用了pci_enable_aspm(),恢复后可能会由于其他约束而自动关闭——你不用管,交给内核就好。

其他重要接口:pci_disable_link_state() / pci_enable_link_state()

这两个接口比前面的更精细,可以分别控制L0s、L1,甚至L2/L3。pci_disable_link_state()原型如下:

int pci_disable_link_state(struct pci_dev *pdev, int state);

参数state用位掩码,比如PCIE_LINK_STATE_L0S | PCIE_LINK_STATE_L1。我一般在调试阶段用它关闭L1但保留L0s,看看是哪个状态引起了问题。表1总结了这几个接口的关键差异。

表1:内核ASPM核心接口对比
API 作用范围 是否受内核策略约束 典型返回值 我常用的场景
pci_enable_aspm() 整条链路 是 0 / -ENOTSUPP / -EACCES 已知设备稳定且需要节能时
pci_disable_aspm() 整条链路 否(强关) 0 / -EINVAL 设备出现链路错误时应急
pci_disable_link_state() 指定状态 否(强关) 0 / -EINVAL 只关L1保留L0s,精确定位
pci_restore_aspm_state() 整条链路 重新评估 void remove/suspend中配合使用

驱动中使用的典型流程

光看文字可能还不太直观,我把整个流程画成一张图,你看看就明白了。

ASPM API调用流程(驱动probe → remove) 驱动probe() 检查PCIe能力ASPM位 是否需禁用ASPM? 尝试启用ASPM 注册pci_restore_aspm_state调用 probe结束 说明: • 红色路径:禁用ASPM(错误场景) • 绿色路径:启用ASPM(理想场景) • 最终都需注册恢复调用

实战建议:如果你不确定设备是否支持ASPM,可以先用lspci -vvv查看(不生成链接,仅指代)LnkCtl里的L0s/L1位。我更推荐在probe最后调用pci_restore_aspm_state()而不是手动写寄存器——因为内核会帮你处理好引用计数和策略冲突。

避坑指南:引用计数与并发

内核里这些API都依赖一个内部引用计数器。每次调用pci_disable_aspm()都会递增计数,而pci_restore_aspm_state()递减。如果你在多个地方关闭同一条链路(比如两个驱动共享一个PCIe to PCIe桥),只有全部恢复后ASPM才能真正重新打开。

我曾经在写一个NVMe驱动时,probe里调用了pci_disable_aspm(),但忘记了在remove里调用恢复。结果卸载模块后,同一端口上的另一块SSD再也打不开ASPM——因为它以为那边还有人端着枪。嗯,后来被QA抓到,老老实实补上了恢复。

什么时候用哪个?一句话总结

  • 如果你只是想修复某个已知bug(比如某款xHCI控制器在L1下丢包),在probe里直接pci_disable_aspm(),然后remove里pci_restore_aspm_state()。
  • 如果你要调试定位,用pci_disable_link_state(L1)分步关闭,找出到底是L0s还是L1引起的问题。
  • 如果你设备很稳定,只是想省电,调用pci_enable_aspm(PCIE_LINK_STATE_ALL),但还是得确认返回值——别以为每次都成功。

记住,这些API都是“请求”,不是“命令”。内核和硬件有最后的拍板权。不过话说回来,正是这种设计让我们的驱动更健壮,不至于一个乱写就把整个PCIe子系统搞崩了。