内核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看看能不能稳定。
有什么限制?
- 可能被固件重新打开:某些平台在链路重新训练(比如热复位)后会恢复ASPM的默认设置,你关了也没用。需要在驱动里加一个看门狗或定期刷新。
- 功耗增加:关闭后实测大约多出200mW~500mW,对笔记本电池续航影响明显。所以在做产品时不能无脑全关,得按设备逐个避坑。
- 需要配合恢复:我习惯在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总结了这几个接口的关键差异。
| 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,可以先用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子系统搞崩了。