PCI 电源管理¶
版权所有 (c) 2010 Rafael J. Wysocki <rjw@sisk.pl>, Novell Inc.
关于 PCI 电源管理的概念和 Linux 内核接口的概述。基于 Patrick Mochel <mochel@transmeta.com> (及其他) 的先前工作。
本文档仅涵盖特定于 PCI 设备的电源管理方面。有关内核与设备电源管理相关的接口的一般描述,请参阅 设备电源管理基础知识 和 I/O 设备的运行时电源管理框架。
1. PCI 电源管理的硬件和平台支持¶
1.1. 原生和基于平台的电源管理¶
一般来说,电源管理是一项功能,允许通过将设备置于功耗较低的状态(低功耗状态)来节省能源,但代价是降低功能或性能。
通常,当设备利用率不足或完全不活动时,会将其置于低功耗状态。但是,当再次需要使用该设备时,必须将其恢复到“完全功能”状态(全功率状态)。当有一些数据需要设备处理时,或者由于需要设备处于活动状态的外部事件(可能由设备本身发出信号)时,可能会发生这种情况。
PCI 设备可以通过两种方式置于低功耗状态:使用 PCI 总线电源管理接口规范引入的设备功能,或借助平台固件,例如 ACPI BIOS。在第一种方法中,即下文所指的原生 PCI 电源管理(原生 PCI PM),设备电源状态的更改是通过将特定值写入其标准配置寄存器之一来实现的。第二种方法要求平台固件提供特殊的方法,内核可以使用这些方法来更改设备的电源状态。
支持原生 PCI PM 的设备通常可以生成称为电源管理事件 (PME) 的唤醒信号,以告知内核需要设备处于活动状态的外部事件。收到 PME 后,内核应该将发送 PME 的设备置于全功率状态。但是,PCI 总线电源管理接口规范没有定义任何将 PME 从设备传递到 CPU 和操作系统内核的标准方法。假设平台固件将执行此任务,因此,即使将 PCI 设备设置为生成 PME,也可能需要准备平台固件以通知 CPU 来自设备的 PME(例如,通过生成中断)。
反过来,如果使用平台固件提供的方法来更改设备的电源状态,则通常平台还会提供一种方法来准备设备以生成唤醒信号。在这种情况下,通常还需要使用原生 PCI PM 机制来准备设备以生成 PME,因为平台提供的方法依赖于此。
因此,在许多情况下,必须同时使用原生电源管理机制和基于平台的电源管理机制才能获得期望的结果。
1.2. 原生 PCI 电源管理¶
PCI 总线电源管理接口规范 (PCI PM Spec) 是在 PCI 2.1 和 PCI 2.2 规范之间引入的。它定义了一个用于执行与电源管理相关的各种操作的标准接口。
对于传统的 PCI 设备,PCI PM 规范的实现是可选的,但对于 PCI Express 设备,它是强制性的。如果设备支持 PCI PM 规范,则其 PCI 配置空间中具有一个 8 字节的电源管理功能字段。此字段用于描述和控制与原生 PCI 电源管理相关的标准功能。
PCI PM 规范为设备定义了 4 种工作状态 (D0-D3),为总线定义了 4 种工作状态 (B0-B3)。数字越高,设备或总线在该状态下消耗的功率就越少。但是,数字越高,设备或总线恢复到全功率状态(分别为 D0 或 B0)的延迟就越长。
规范定义了 D3 状态的两种变体。第一个是 D3hot,称为软件可访问的 D3,因为可以对设备进行编程以进入该状态。第二个是 D3cold,当设备的供电电压 (Vcc) 被移除时,PCI 设备处于该状态。无法对 PCI 设备进行编程以进入 D3cold,尽管可能存在一个可编程接口,用于将设备所在的总线置于一种 Vcc 从总线上所有设备移除的状态。
但是,在撰写本文时,Linux 内核不支持 PCI 总线电源管理,因此本文档不涉及该方面。
请注意,每个 PCI 设备都可以处于全功率状态 (D0) 或 D3cold 状态,无论它是否实现了 PCI PM 规范。除此之外,如果设备实现了 PCI PM 规范,则它必须支持 D3hot 以及 D0。对 D1 和 D2 电源状态的支持是可选的。
可以对支持 PCI PM 规范的 PCI 设备进行编程以进入任何受支持的低功耗状态(D3cold 除外)。当处于 D1-D3hot 时,设备的标准配置寄存器必须可供软件访问(即,要求设备响应 PCI 配置访问),尽管其 I/O 和内存空间被禁用。这允许以编程方式将设备置于 D0。因此,内核可以在 D0 和受支持的低功耗状态(D3cold 除外)之间来回切换设备,并且设备可以经历的可能的电源状态转换如下:
当前状态 | 新状态 |
D0 | D1、D2、D3 |
D1 | D2、D3 |
D2 | D3 |
D1、D2、D3 | D0 |
当向设备提供供电电压时(即,恢复供电),会发生从 D3cold 到 D0 的转换。在这种情况下,设备以完整的上电复位序列返回到 D0,并且硬件会将上电默认值恢复到设备,就像在初始上电时一样。
可以对支持 PCI PM 规范的 PCI 设备进行编程,以便在任何电源状态 (D0-D3) 下生成 PME,但它们不一定能够从所有受支持的电源状态生成 PME。特别是,从 D3cold 生成 PME 的能力是可选的,并且取决于是否存在额外的电压 (3.3Vaux),从而允许设备保持足够的活动状态以生成唤醒信号。
1.3. ACPI 设备电源管理¶
平台固件对 PCI 设备电源管理的支持是特定于系统的。但是,如果相关系统符合高级配置和电源接口 (ACPI) 规范(如大多数基于 x86 的系统),则它应该实现 ACPI 标准定义的设备电源管理接口。
为此,ACPI BIOS 提供了称为“控制方法”的特殊函数,内核可以执行这些函数来执行特定任务,例如将设备置于低功耗状态。这些控制方法使用称为 ACPI 机器语言 (AML) 的特殊字节码语言进行编码,并存储在机器的 BIOS 中。内核从 BIOS 加载它们,并根据需要使用 AML 解释器执行它们,该解释器将 AML 字节码转换为计算和内存或 I/O 空间访问。这样,从理论上讲,BIOS 编写者可以为内核提供一种以系统特定的方式执行取决于系统设计的操作的方法。
ACPI 控制方法可以分为全局控制方法(不与任何特定设备关联)和设备控制方法(必须为每个应该在平台帮助下处理的设备单独定义)。这尤其意味着,ACPI 设备控制方法只能用于处理 BIOS 编写者事先知道的设备。用于设备电源管理的 ACPI 方法属于该类别。
ACPI 规范假设设备可以处于标记为 D0、D1、D2 和 D3 的四种电源状态之一,这些状态大致对应于原生 PCI PM D0-D3 状态(尽管 ACPI 没有考虑 D3hot 和 D3cold 之间的差异)。此外,对于设备的每个电源状态,都有一组电源资源,必须启用这些电源资源才能将设备置于该状态。这些电源资源是通过它们自己的控制方法 _ON 和 _OFF 进行控制(即启用或禁用)的,这些方法必须为它们中的每一个单独定义。
为了将设备置于 ACPI 电源状态 Dx(其中 x 是 0 到 3 之间的数字,包括 0 和 3),内核应该 (1) 使用其 _ON 控制方法启用此状态下设备所需的电源资源,以及 (2) 执行为该设备定义的 _PSx 控制方法。此外,如果要将设备置于低功耗状态 (D1-D3),并且要从该状态生成唤醒信号,则必须在 _PSx 之前执行为其定义的 _DSW(或 _PSW,由 ACPI 3.0 替换为 _DSW)控制方法。目标电源状态中设备不需要,且任何其他设备都不再需要的电源资源应该被禁用(通过执行其 _OFF 控制方法)。如果设备的当前电源状态为 D3,则只能以这种方式将其置于 D0。
然而,通常情况下,设备电源状态会在系统范围内的睡眠状态转换或返回工作状态期间发生改变。ACPI 定义了四种系统睡眠状态,S1、S2、S3 和 S4,并将系统工作状态表示为 S0。一般来说,目标系统睡眠(或工作)状态决定了设备可以进入的最高电源(最低数值)状态,内核应该通过执行设备的 _SxD 控制方法(其中 x 是 0 到 4(包括 0 和 4)之间的数字)来获取此信息。如果设备需要在目标睡眠状态下唤醒系统,则它可以进入的最低电源(最高数值)状态也由系统的目标状态决定。然后,内核应该使用设备的 _SxW 控制方法来获取该状态的数值。它还应该使用设备的 _PRW 控制方法来了解哪些电源资源需要启用才能使设备能够生成唤醒信号。
1.4. 唤醒信号¶
PCI 设备生成的唤醒信号,无论是作为原生 PCI PME,还是在将设备置于低功耗状态之前执行 _DSW(或 _PSW)ACPI 控制方法的结果,都必须被捕获并进行适当处理。如果它们是在系统处于工作状态(ACPI S0)时发送的,则应将其转换为中断,以便内核可以将生成它们的设备置于全功率状态,并处理触发它们的事件。反过来,如果它们是在系统睡眠时发送的,则应导致系统的核心逻辑触发唤醒。
在基于 ACPI 的系统上,传统 PCI 设备发送的唤醒信号会被转换为 ACPI 通用事件 (GPE),这些事件是系统核心逻辑响应各种需要执行的事件而生成的硬件信号。每个 GPE 都与一个或多个潜在的有趣事件源相关联。特别是,GPE 可能与能够发出唤醒信号的 PCI 设备相关联。有关 GPE 和事件源之间连接的信息记录在系统的 ACPI BIOS 中,内核可以从中读取该信息。
如果已知系统 ACPI BIOS 的 PCI 设备发出唤醒信号,则与其关联的 GPE(如果有)将被触发。与 PCI 桥接器关联的 GPE 也可能会因桥接器下某个设备的唤醒信号而触发(根桥接器也是如此),例如,来自系统 ACPI BIOS 未知的设备的原生 PCI PME 可以通过这种方式处理。
当系统处于睡眠状态时(即处于 ACPI S1-S4 状态之一),可能会触发 GPE,在这种情况下,系统核心逻辑会启动系统唤醒(导致系统唤醒的信号来源设备可能会稍后识别)。在这种情况下使用的 GPE 被称为唤醒 GPE。
然而,通常情况下,当系统处于工作状态(ACPI S0)时也会触发 GPE,在这种情况下,系统核心逻辑会生成系统控制中断 (SCI) 以通知内核发生事件。然后,SCI 处理程序会识别导致生成中断的 GPE,这反过来又允许内核识别事件的来源(可能是发出唤醒信号的 PCI 设备)。用于通知内核在系统处于工作状态时发生的事件的 GPE 被称为运行时 GPE。
遗憾的是,对于非基于 ACPI 的系统,没有处理传统 PCI 设备发送的唤醒信号的标准方法,但对于 PCI Express 设备则有一种标准方法。即,PCI Express 基础规范引入了一种原生机制,用于将原生 PCI PME 转换为根端口生成的中断。对于传统 PCI 设备,原生 PME 是带外的,因此它们会单独路由,并且无需通过桥接器(原则上,它们可以直接路由到系统的核心逻辑),但对于 PCI Express 设备,它们是带内消息,必须通过 PCI Express 层级结构,包括从设备到根复合体的路径上的根端口。因此,可以引入一种机制,通过该机制,根端口会在收到来自其下方某个设备的 PME 消息时生成中断。然后,发送 PME 消息的设备的 PCI Express 请求者 ID 会被记录在根端口的一个配置寄存器中,中断处理程序可以从中读取该 ID 以识别设备。[由与根复合体集成的 PCI Express 端点发送的 PME 消息不会通过根端口,而是会导致根复合体事件收集器(如果有)生成中断。]
原则上,原生 PCI Express PME 信号也可以在基于 ACPI 的系统上与 GPE 一起使用,但是要使用它,内核必须请求系统的 ACPI BIOS 释放对根端口配置寄存器的控制权。但是,ACPI BIOS 并非必须允许内核控制这些寄存器,如果它不允许,内核不得修改其内容。当然,在这种情况下,内核不能使用原生 PCI Express PME 信号。
2. PCI 子系统和设备电源管理¶
2.1. 设备电源管理回调¶
PCI 子系统以多种方式参与 PCI 设备的电源管理。首先,它在设备电源管理核心(PM 核心)和 PCI 设备驱动程序之间提供了一个中间代码层。具体来说,PCI 子系统的 struct bus_type
对象 pci_bus_type 的 pm 字段指向一个 struct dev_pm_ops
对象 pci_dev_pm_ops,其中包含指向多个设备电源管理回调的指针。
const struct dev_pm_ops pci_dev_pm_ops = {
.prepare = pci_pm_prepare,
.complete = pci_pm_complete,
.suspend = pci_pm_suspend,
.resume = pci_pm_resume,
.freeze = pci_pm_freeze,
.thaw = pci_pm_thaw,
.poweroff = pci_pm_poweroff,
.restore = pci_pm_restore,
.suspend_noirq = pci_pm_suspend_noirq,
.resume_noirq = pci_pm_resume_noirq,
.freeze_noirq = pci_pm_freeze_noirq,
.thaw_noirq = pci_pm_thaw_noirq,
.poweroff_noirq = pci_pm_poweroff_noirq,
.restore_noirq = pci_pm_restore_noirq,
.runtime_suspend = pci_pm_runtime_suspend,
.runtime_resume = pci_pm_runtime_resume,
.runtime_idle = pci_pm_runtime_idle,
};
这些回调由 PM 核心在与设备电源管理相关的各种情况下执行,并且它们反过来又执行 PCI 设备驱动程序提供的电源管理回调。它们还执行涉及 PCI 设备的一些标准配置寄存器的电源管理操作,而设备驱动程序不需要知道或关心这些寄存器。
表示 PCI 设备的结构 struct pci_dev 包含这些回调操作的多个字段。
struct pci_dev {
...
pci_power_t current_state; /* Current operating state. */
int pm_cap; /* PM capability offset in the
configuration space */
unsigned int pme_support:5; /* Bitmask of states from which PME#
can be generated */
unsigned int pme_poll:1; /* Poll device's PME status bit */
unsigned int d1_support:1; /* Low power state D1 is supported */
unsigned int d2_support:1; /* Low power state D2 is supported */
unsigned int no_d1d2:1; /* D1 and D2 are forbidden */
unsigned int wakeup_prepared:1; /* Device prepared for wake up */
unsigned int d3hot_delay; /* D3hot->D0 transition time in ms */
...
};
它们还间接使用嵌入在 struct pci_dev 中的 struct device
的一些字段。
2.2. 设备初始化¶
PCI 子系统与设备电源管理相关的首要任务是准备设备进行电源管理,并初始化用于此目的的 struct pci_dev 的字段。这发生在 drivers/pci/ 中定义的两个函数 pci_pm_init() 和 pci_acpi_setup() 中。
第一个函数检查设备是否支持原生 PCI PM,如果是,则将设备配置空间中其电源管理能力结构的偏移量存储在设备的 struct pci_dev 对象的 pm_cap 字段中。接下来,该函数检查设备支持哪些 PCI 低功耗状态,以及设备可以从哪些低功耗状态生成原生 PCI PME。设备的 struct pci_dev 和嵌入其中的 struct device
的电源管理字段会相应更新,并且会禁用设备生成 PME。
第二个函数检查是否可以使用平台固件(例如 ACPI BIOS)来准备设备发出唤醒信号。如果是,该函数会更新嵌入在设备 struct pci_dev 中的 struct device
中的唤醒字段,并使用固件提供的方法来阻止设备发出唤醒信号。
此时,设备已准备好进行电源管理。但是,对于无驱动程序的设备,此功能仅限于在系统范围内转换到睡眠状态并返回工作状态期间执行的几个基本操作。
2.3. 运行时设备电源管理¶
PCI 子系统在 PCI 设备的运行时电源管理中发挥着至关重要的作用。为此,它使用了 I/O 设备的运行时电源管理框架 中描述的通用运行时电源管理(运行时 PM)框架。即,它提供了子系统级别的回调
pci_pm_runtime_suspend()
pci_pm_runtime_resume()
pci_pm_runtime_idle()
这些回调由核心运行时 PM 例程执行。它还实现了处理来自低功耗状态的 PCI 设备的运行时唤醒信号的整个机制,在撰写本文时,该机制适用于第 1 节中描述的原生 PCI Express PME 信号和基于 ACPI GPE 的唤醒信号。
首先,借助 pm_schedule_suspend() 或 pm_runtime_suspend() 将 PCI 设备置于低功耗状态或挂起,这些函数会调用 pci_pm_runtime_suspend() 来完成实际工作。为此,设备的驱动程序必须提供一个 pm->runtime_suspend() 回调(见下文),该回调由 pci_pm_runtime_suspend() 作为第一个操作运行。如果驱动程序的回调成功返回,则会保存设备标准配置寄存器,准备设备生成唤醒信号,最后将其置于目标低功耗状态。
将设备置于的低功耗状态是它可以发出唤醒信号的最低功耗(最高数值)状态。确切的唤醒信号发送方法取决于系统,并由 PCI 子系统根据报告的设备能力和平台固件确定。为了准备设备发出唤醒信号并将其置于选定的低功耗状态,PCI 子系统可以使用平台固件以及设备的原生 PCI PM 功能(如果支持)。
设备驱动程序的 pm->runtime_suspend() 回调函数不应尝试准备设备以发出唤醒信号或将其置于低功耗状态。驱动程序应将这些任务留给 PCI 子系统,该子系统拥有执行这些任务所需的所有信息。
已挂起的设备通过 pm_request_resume() 或 pm_runtime_resume() 的帮助恢复到“活动”状态,即恢复状态。这两个函数都为 PCI 设备调用 pci_pm_runtime_resume()。同样,这只有在设备的驱动程序提供 pm->runtime_resume() 回调函数(见下文)时才有效。但是,在执行驱动程序的回调函数之前,pci_pm_runtime_resume() 会将设备恢复到全功率状态,阻止其在该状态下发出唤醒信号,并恢复其标准配置寄存器。因此,驱动程序的回调函数无需担心设备恢复的 PCI 特有方面。
请注意,通常 pci_pm_runtime_resume() 可能会在两种不同的情况下被调用。首先,它可能应设备驱动程序的要求被调用,例如,如果有一些数据需要处理。其次,它可能是由设备本身的唤醒信号触发的(有时称为“远程唤醒”)。当然,为此目的,唤醒信号会以第 1 节中描述的方式之一进行处理,并在确定源设备后最终转换为 PCI 子系统的通知。
pm_runtime_idle() 和 pm_request_idle() 为 PCI 设备调用的 pci_pm_runtime_idle() 函数,如果已定义,则执行设备驱动程序的 pm->runtime_idle() 回调函数,并且如果该回调函数没有返回错误代码(或者根本不存在),则会使用 pm_runtime_suspend() 挂起设备。有时,pci_pm_runtime_idle() 会由 PM 核心自动调用(例如,在设备刚恢复后立即调用),在这种情况下,如果这样做有意义,则应挂起设备。然而,通常情况下,PCI 子系统并不真正知道设备是否真的可以被挂起,因此它通过运行设备的 pm->runtime_idle() 回调函数来让设备驱动程序做出决定。
2.4. 系统范围的电源转换¶
如 设备电源管理基础 中所述,有几种不同类型的系统范围的电源转换。每种转换都需要以特定的方式处理设备,PM 核心会为此目的执行子系统级的电源管理回调函数。这些回调函数分阶段执行,每个阶段都包括在下一个阶段开始之前,为属于给定子系统的每个设备执行相同的子系统级回调函数。这些阶段总是在任务被冻结后运行。
2.4.1. 系统挂起¶
当系统进入一种会保留内存内容的睡眠状态时,例如 ACPI 睡眠状态 S1-S3 中的一种,其阶段为
prepare、suspend、suspend_noirq。
以下 PCI 总线类型的回调函数分别在这些阶段中使用
pci_pm_prepare()
pci_pm_suspend()
pci_pm_suspend_noirq()
pci_pm_prepare() 例程首先使用 pm_runtime_resume() 将设备置于“完全功能”状态。然后,如果定义了设备驱动程序的 pm->prepare() 回调函数(即,如果驱动程序的 struct dev_pm_ops
对象存在并且该对象中的 prepare 指针有效),则执行该回调函数。
pci_pm_suspend() 例程首先检查设备的驱动程序是否实现了传统的 PCI 挂起例程(请参阅第 3 节),如果实现了,则执行驱动程序的传统挂起回调函数(如果存在),并返回其结果。接下来,如果设备的驱动程序没有提供 struct dev_pm_ops
对象(包含指向驱动程序回调函数的指针),则会调用 pci_pm_default_suspend(),该函数只是关闭设备的总线主控功能并运行 pcibios_disable_device() 来禁用它,除非该设备是桥接器(此例程会忽略 PCI 桥接器)。接下来,执行设备驱动程序的 pm->suspend() 回调函数(如果已定义),如果失败,则返回其结果。最后,调用 pci_fixup_device() 以在必要时应用与设备相关的硬件挂起怪癖。
请注意,挂起阶段是为 PCI 设备异步执行的,因此,对于任何一对不以已知方式相互依赖的 PCI 设备(即,从根桥到叶设备的设备树中的任何路径都不包含这两者),可以并行执行 pci_pm_suspend() 回调函数。
pci_pm_suspend_noirq() 例程是在调用 suspend_device_irqs() 之后执行的,这意味着在运行此例程时不会调用设备驱动程序的中断处理程序。它首先检查设备的驱动程序是否实现了传统的 PCI 挂起例程(第 3 节),如果实现了,则调用传统的后期挂起例程并返回其结果(如果驱动程序的回调函数没有这样做,则会保存设备的标准配置寄存器)。其次,如果设备驱动程序的 struct dev_pm_ops
对象不存在,则保存设备的标准配置寄存器,并且该例程返回成功。否则,执行设备驱动程序的 pm->suspend_noirq() 回调函数(如果存在),如果失败,则返回其结果。接下来,如果尚未保存设备的标准配置寄存器(之前执行的设备驱动程序的回调函数之一可能会执行此操作),则 pci_pm_suspend_noirq() 会保存它们,准备设备以发出唤醒信号(如有必要),并将其置于低功耗状态。
将设备置于其中的低功耗状态是最低功耗(最高编号)状态,从该状态可以发出唤醒信号,同时系统处于目标睡眠状态。就像上面描述的运行时 PM 情况一样,发出唤醒信号的机制是系统相关的,并且由 PCI 子系统确定,PCI 子系统还负责准备设备以从系统的目标睡眠状态发出唤醒信号(如适用)。
通常,不希望 PCI 设备驱动程序(不实现传统的电源管理回调函数)准备设备以发出唤醒信号或将其置于低功耗状态。但是,如果驱动程序的某个挂起回调函数 (pm->suspend() 或 pm->suspend_noirq()) 保存了设备的标准配置寄存器,则 pci_pm_suspend_noirq() 将假定该设备已准备好发出唤醒信号并由驱动程序置于低功耗状态(然后假定该驱动程序为此目的使用了 PCI 子系统提供的辅助函数)。不鼓励 PCI 设备驱动程序执行此操作,但在某些极少数情况下,在驱动程序中执行此操作可能是最佳方法。
2.4.2. 系统恢复¶
当系统正在从会保留内存内容的睡眠状态(例如 ACPI 睡眠状态 S1-S3 中的一种)转换到工作状态 (ACPI S0) 时,其阶段为
resume_noirq、resume、complete。
以下 PCI 总线类型的回调函数分别在这些阶段中执行
pci_pm_resume_noirq()
pci_pm_resume()
pci_pm_complete()
pci_pm_resume_noirq() 例程首先将设备置于全功率状态,恢复其标准配置寄存器,并在必要时应用与设备相关的早期恢复硬件怪癖。这是无条件执行的,无论设备的驱动程序是否实现了传统的 PCI 电源管理回调函数(这样,当在恢复期间首次调用它们的中断处理程序时,所有 PCI 设备都处于全功率状态并且已恢复其标准配置寄存器,这允许内核避免因设备仍处于挂起状态的驱动程序处理共享中断而导致的问题)。如果设备的驱动程序实现了传统的 PCI 电源管理回调函数(请参阅第 3 节),则会执行传统的早期恢复回调函数并返回其结果。否则,执行设备驱动程序的 pm->resume_noirq() 回调函数(如果已定义),并返回其结果。
pci_pm_resume() 例程首先检查设备的标准配置寄存器是否已恢复,如果未恢复则恢复它们(这仅在挂起失败时的错误路径中是必要的)。接下来,应用与设备相关的恢复硬件怪癖(如有必要),并且如果设备的驱动程序实现了传统的 PCI 电源管理回调函数(请参阅第 3 节),则执行驱动程序的传统恢复回调函数并返回其结果。否则,将阻止设备的唤醒信号机制,并执行其驱动程序的 pm->resume() 回调函数(如果已定义)(然后返回回调函数的结果)。
与上面描述的挂起阶段一样,恢复阶段是为 PCI 设备异步执行的,这意味着如果两个 PCI 设备不以已知的方式相互依赖,则可以并行执行这两个设备的 pci_pm_resume() 例程。
pci_pm_complete() 例程仅执行设备驱动程序的 pm->complete() 回调函数(如果已定义)。
2.4.3. 系统休眠¶
系统休眠比系统挂起更复杂,因为它需要创建系统映像并将其写入持久存储介质。映像是原子创建的,并且所有设备在创建映像之前都会被静止或冻结。
设备冻结是在释放足够的内存后进行的(在撰写本文时,映像创建需要至少 50% 的系统 RAM 是空闲的),分三个阶段进行
prepare、freeze、freeze_noirq
它们对应于 PCI 总线类型的回调函数
pci_pm_prepare()
pci_pm_freeze()
pci_pm_freeze_noirq()
这意味着 prepare 阶段与系统挂起的 prepare 阶段完全相同。然而,其他两个阶段是不同的。
pci_pm_freeze() 例程与 pci_pm_suspend() 非常相似,但它运行设备驱动程序的 pm->freeze() 回调函数(如果已定义),而不是 pm->suspend(),并且不应用与挂起相关的硬件怪癖。它是为不以已知方式相互依赖的不同 PCI 设备异步执行的。
反过来,pci_pm_freeze_noirq() 例程类似于 pci_pm_suspend_noirq(),但它调用设备驱动程序的 pm->freeze_noirq() 例程,而不是 pm->suspend_noirq()。它也不尝试准备设备以发出唤醒信号并将其置于低功耗状态。不过,如果设备的标准配置寄存器尚未由驱动程序的某个回调函数保存,它仍会保存它们。
创建映像后,必须将其保存。但是,此时所有设备都处于冻结状态,无法处理 I/O,而它们处理 I/O 的能力对于映像保存显然是必要的。因此,必须将它们恢复到完全功能状态,这会在以下阶段中完成
thaw_noirq、thaw、complete
使用以下 PCI 总线类型的回调函数
pci_pm_thaw_noirq()
pci_pm_thaw()
pci_pm_complete()
分别。
第一个,pci_pm_thaw_noirq(),类似于 pci_pm_resume_noirq()。它将设备置于全功率状态并恢复其标准配置寄存器。它还执行设备驱动程序的 pm->thaw_noirq() 回调函数(如果已定义),而不是 pm->resume_noirq()。
pci_pm_thaw() 例程类似于 pci_pm_resume(),但它运行设备驱动程序的 pm->thaw() 回调函数,而不是 pm->resume()。它是为不以已知方式相互依赖的不同 PCI 设备异步执行的。
complete 阶段与系统恢复的 complete 阶段相同。
保存映像后,在系统可以进入目标睡眠状态(基于 ACPI 的系统的 ACPI S4)之前,需要关闭设备的电源。这分三个阶段完成
prepare、poweroff、poweroff_noirq
其中 prepare 阶段与系统挂起的 prepare 阶段完全相同。其他两个阶段分别类似于 suspend 和 suspend_noirq 阶段。它们对应的 PCI 子系统级回调函数
pci_pm_poweroff()
pci_pm_poweroff_noirq()
的工作方式与 pci_pm_suspend() 和 pci_pm_suspend_noirq() 的工作方式类似,尽管它们不尝试保存设备的标准配置寄存器。
2.4.4. 系统还原¶
系统还原需要将休眠映像加载到内存中,并在恢复休眠前的系统活动之前还原休眠前的内存内容。
正如设备电源管理基础中所述,休眠镜像由一个新的内核实例(称为引导内核)加载到内存中,而引导内核又由引导加载程序以通常的方式加载和运行。引导内核加载镜像后,需要将其自身的代码和数据替换为镜像中存储的“休眠”内核的代码和数据,称为镜像内核。为此,所有设备都会像休眠期间创建镜像之前一样被冻结,在
prepare、freeze、freeze_noirq
上面描述的各个阶段中。但是,受这些阶段影响的设备仅限于在引导内核中拥有驱动程序的设备;其他设备将仍然处于引导加载程序使它们处于的任何状态。
如果预休眠内存内容的恢复失败,引导内核将使用 thaw_noirq、thaw 和 complete 阶段(只会影响在引导内核中拥有驱动程序的设备)执行上面描述的“解冻”过程,然后继续正常运行。
如果预休眠内存内容成功恢复(这是通常情况),控制权将传递给镜像内核,然后由镜像内核负责将系统恢复到工作状态。为实现这一点,它必须恢复设备在休眠前的功能,这很像从内存睡眠状态唤醒,尽管它涉及不同的阶段
restore_noirq, restore, complete
前两个阶段分别类似于上面描述的 resume_noirq 和 resume 阶段,并对应于以下 PCI 子系统回调
pci_pm_restore_noirq()
pci_pm_restore()
这些回调的工作方式类似于 pci_pm_resume_noirq() 和 pci_pm_resume(),但它们会执行设备驱动程序的 pm->restore_noirq() 和 pm->restore() 回调(如果可用)。
complete 阶段的执行方式与系统恢复期间完全相同。
3. PCI 设备驱动程序和电源管理¶
3.1. 电源管理回调¶
PCI 设备驱动程序通过提供由上面描述的 PCI 子系统的电源管理例程执行的回调,并通过控制其设备的运行时电源管理来参与电源管理。
在撰写本文时,有两种方法可以为 PCI 设备驱动程序定义电源管理回调,推荐的方法是基于使用设备电源管理基础中描述的 dev_pm_ops 结构,以及“旧”方法,其中使用了struct pci_driver
中的 .suspend() 和 .resume() 回调。但是,旧方法不允许定义运行时电源管理回调,并且不适用于任何新的驱动程序。因此,本文档不涵盖此内容(请参阅源代码以了解更多信息)。
建议所有 PCI 设备驱动程序定义一个struct dev_pm_ops
对象,其中包含指向电源管理 (PM) 回调的指针,这些回调将在各种情况下由 PCI 子系统的 PM 例程执行。指向驱动程序的struct dev_pm_ops
对象的指针必须分配给其struct pci_driver
对象中的 driver.pm 字段。一旦发生这种情况,struct pci_driver
中的“旧”PM 回调将被忽略(即使它们不是 NULL)。
struct dev_pm_ops
中的 PM 回调不是强制性的,如果未定义(即struct dev_pm_ops
的相应字段未设置),PCI 子系统将以简化的默认方式处理设备。但是,如果定义了它们,则应按以下小节中所述的方式运行。
3.1.1. prepare()¶
prepare() 回调在系统挂起期间、休眠期间(当即将创建休眠镜像时)、在保存休眠镜像后关机期间以及在系统还原期间(当休眠镜像刚被加载到内存中时)执行。
仅当驱动程序的设备具有通常可以在任何时间注册的子设备时,才需要此回调。在这种情况下,prepare() 回调的作用是防止设备的新的子设备在 resume_noirq()、thaw_noirq() 或 restore_noirq() 回调之一运行之前被注册。
除此之外,prepare() 回调还可以执行一些操作,为挂起设备做准备,尽管它不应分配内存(如果需要额外的内存来挂起设备,则必须提前预分配,例如在挂起/休眠通知程序中描述的挂起/休眠通知程序中)。
3.1.2. suspend()¶
suspend() 回调仅在系统挂起期间执行,在系统中所有设备的 prepare() 回调都执行完毕后执行。
此回调预计会使设备静止,并使其准备好由 PCI 子系统将其置于低功耗状态。PCI 驱动程序的 suspend() 回调不需要(实际上甚至不建议)保存设备的标准配置寄存器、为唤醒系统做准备或将其置于低功耗状态。所有这些操作都可以由 PCI 子系统很好地完成,而无需驱动程序的参与。
但是,在某些罕见的情况下,在 PCI 驱动程序中执行这些操作很方便。然后,应使用pci_save_state()
、pci_prepare_to_sleep()
和pci_set_power_state()
来分别保存设备的标准配置寄存器、为系统唤醒(如果需要)做准备以及将其置于低功耗状态。此外,如果驱动程序调用pci_save_state()
,PCI 子系统将不会为其设备执行pci_prepare_to_sleep()
或pci_set_power_state()
,因此驱动程序有责任以适当的方式处理设备。
在执行 suspend() 回调时,可以调用驱动程序的 中断处理程序来处理来自设备的中断,因此,所有依赖于驱动程序处理中断能力的与挂起相关的操作都应在此回调中执行。
3.1.3. suspend_noirq()¶
suspend_noirq() 回调仅在系统挂起期间执行,在系统中所有设备的 suspend() 回调都执行完毕后,以及在 PM 内核禁用设备中断之后执行。
suspend_noirq() 和 suspend() 之间的区别在于,在 suspend_noirq() 运行时,不会调用驱动程序的中断处理程序。因此,suspend_noirq() 可以执行如果在 suspend() 中执行会导致出现竞争条件的操作。
3.1.4. freeze()¶
freeze() 回调是休眠特定的,在两种情况下执行,一种是在休眠期间,在系统中所有设备的 prepare() 回调都执行完毕后,为创建系统镜像做准备,另一种是在还原期间,在系统镜像从持久存储加载到内存后,并且在所有设备的 prepare() 回调都执行完毕后执行。
此回调的作用类似于上面描述的 suspend() 回调的作用。事实上,它们只需要在驱动程序承担将设备置于低功耗状态的责任的罕见情况下有所不同。
在这些情况下,freeze() 回调不应准备设备系统唤醒或将其置于低功耗状态。尽管如此,它或 freeze_noirq() 都应使用pci_save_state()
保存设备的标准配置寄存器。
3.1.5. freeze_noirq()¶
freeze_noirq() 回调是休眠特定的。它在休眠期间执行,在系统中所有设备的 prepare() 和 freeze() 回调都执行完毕后,为创建系统镜像做准备,并且在还原期间执行,在系统镜像加载到内存后,并且在所有设备的 prepare() 和 freeze() 回调都执行完毕后执行。它总是在 PM 内核禁用设备中断后执行。
此回调的作用类似于上面描述的 suspend_noirq() 回调的作用,并且很少需要定义 freeze_noirq()。
freeze_noirq() 和 freeze() 之间的差异类似于 suspend_noirq() 和 suspend() 之间的差异。
3.1.6. poweroff()¶
poweroff() 回调是休眠特定的。它在系统即将关闭电源之前执行,在将休眠镜像保存到持久存储之后。在调用 poweroff() 之前,会为所有设备执行 prepare() 回调。
此回调的作用类似于上面描述的 suspend() 和 freeze() 回调的作用,尽管它不需要保存设备寄存器的内容。特别是,如果驱动程序希望自己将设备置于低功耗状态,而不是允许 PCI 子系统这样做,则 poweroff() 回调应使用pci_prepare_to_sleep()
和pci_set_power_state()
来为系统唤醒准备设备并将其置于低功耗状态,但它不需要保存设备的标准配置寄存器。
3.1.7. poweroff_noirq()¶
poweroff_noirq() 回调是休眠特定的。它在系统中所有设备的 poweroff() 回调都执行完毕后执行。
此回调的作用类似于上面描述的 suspend_noirq() 和 freeze_noirq() 回调的作用,但它不需要保存设备寄存器的内容。
poweroff_noirq() 和 poweroff() 之间的区别类似于 suspend_noirq() 和 suspend() 之间的区别。
3.1.8. resume_noirq()¶
resume_noirq() 回调仅在系统恢复期间执行,在 PM 核心启用非引导 CPU 之后。在 resume_noirq() 运行时,不会调用驱动程序的 中断处理程序,因此此回调可以执行可能与中断处理程序竞争的操作。
由于 PCI 子系统在系统恢复的 resume_noirq 阶段无条件地将所有设备置于完全电源状态,并恢复其标准配置寄存器,因此通常不需要 resume_noirq()。一般来说,它应该只用于执行由 resume() 执行可能会导致竞争条件的操作。
3.1.9. resume()¶
resume() 回调仅在系统恢复期间执行,在系统中所有设备都执行了 resume_noirq() 回调且设备中断已由 PM 核心启用之后。
此回调负责恢复设备在挂起之前的配置,并使其恢复到完全功能状态。在 resume() 返回后,设备应该能够以通常的方式处理 I/O。
3.1.10. thaw_noirq()¶
thaw_noirq() 回调是休眠特定的。它在创建系统映像并且 PM 核心在休眠的 thaw_noirq 阶段启用非引导 CPU 后执行。如果在系统还原期间加载休眠映像失败,它也可能会被执行(然后在启用非引导 CPU 后执行)。在 thaw_noirq() 运行时,不会调用驱动程序的中断处理程序。
此回调的作用类似于 resume_noirq() 的作用。这两个回调之间的区别在于,thaw_noirq() 在 freeze() 和 freeze_noirq() 之后执行,因此一般来说,它不需要修改设备寄存器的内容。
3.1.11. thaw()¶
thaw() 回调是休眠特定的。它在系统中所有设备都执行了 thaw_noirq() 回调之后,以及在 PM 核心启用设备中断之后执行。
此回调负责恢复设备在冻结之前的配置,以便它在 thaw() 返回后能够以通常的方式工作。
3.1.12. restore_noirq()¶
restore_noirq() 回调是休眠特定的。它在休眠的 restore_noirq 阶段执行,当引导内核将控制权传递给映像内核并且映像内核的 PM 核心已启用非引导 CPU 时。
此回调类似于 resume_noirq(),但有一个例外,它不能对设备之前的状态做出任何假设,即使已知 BIOS(或通常是平台固件)会在挂起-恢复周期中保留该状态。
对于绝大多数 PCI 设备驱动程序,resume_noirq() 和 restore_noirq() 之间没有区别。
3.1.13. restore()¶
restore() 回调是休眠特定的。它在系统中所有设备都执行了 restore_noirq() 回调之后,以及在 PM 核心启用设备驱动程序的中断处理程序被调用之后执行。
此回调类似于 resume(),正如 restore_noirq() 类似于 resume_noirq() 一样。因此,restore_noirq() 和 restore() 之间的区别类似于 resume_noirq() 和 resume() 之间的区别。
对于绝大多数 PCI 设备驱动程序,resume() 和 restore() 之间没有区别。
3.1.14. complete()¶
complete() 回调在以下情况下执行
在系统恢复期间,在所有设备都执行了 resume() 回调之后,
在休眠期间,在保存系统映像之前,在所有设备都执行了 thaw() 回调之后,
在系统还原期间,当系统返回到休眠之前的状态时,在所有设备都执行了 restore() 回调之后。
如果在将休眠映像加载到内存中失败的情况下,它也可能会被执行(在这种情况下,它在所有在引导内核中具有驱动程序的设备都执行了 thaw() 回调之后运行)。
此回调是完全可选的,但如果 prepare() 回调执行了需要反转的操作,则可能是必要的。
3.1.15. runtime_suspend()¶
runtime_suspend() 回调特定于设备运行时电源管理(运行时 PM)。当设备即将挂起(即静止并进入低功耗状态)时,由 PM 核心的运行时 PM 框架执行。
此回调负责冻结设备并准备将其置于低功耗状态,但它必须允许 PCI 子系统执行挂起设备所需的所有 PCI 特定操作。
3.1.16. runtime_resume()¶
runtime_resume() 回调特定于设备运行时 PM。当设备即将恢复(即置于完全电源状态并编程为正常处理 I/O)时,由 PM 核心的运行时 PM 框架执行。
此回调负责在 PCI 子系统将设备置于完全电源状态后恢复设备的正常功能。在 runtime_resume() 返回后,设备应该能够以通常的方式处理 I/O。
3.1.17. runtime_idle()¶
runtime_idle() 回调特定于设备运行时 PM。每当根据 PM 核心的信息可能需要挂起设备时,由 PM 核心的运行时 PM 框架执行。特别是,在设备恢复是由于虚假事件发生的情况下,它会在 runtime_resume() 返回后立即自动执行。
此回调是可选的,但如果未实现或返回 0,PCI 子系统将为设备调用 pm_runtime_suspend(),这将导致驱动程序的 runtime_suspend() 回调被执行。
3.1.18. 将多个回调指针指向一个例程¶
虽然原则上前面小节中描述的每个回调都可以定义为单独的函数,但通常将 struct dev_pm_ops
的两个或多个成员指向同一个例程很方便。有一些方便的宏可以用于此目的。
DEFINE_SIMPLE_DEV_PM_OPS() 声明一个 struct dev_pm_ops
对象,其中一个挂起例程由 .suspend()、.freeze() 和 .poweroff() 成员指向,一个恢复例程由 .resume()、.thaw() 和 .restore() 成员指向。此 struct dev_pm_ops
中的其他函数指针未设置。
DEFINE_RUNTIME_DEV_PM_OPS() 与 DEFINE_SIMPLE_DEV_PM_OPS() 类似,但它还另外将 .runtime_resume() 指针设置为 pm_runtime_force_resume(),将 .runtime_suspend() 指针设置为 pm_runtime_force_suspend()。
SYSTEM_SLEEP_PM_OPS() 可以在 struct dev_pm_ops
的声明中使用,以指示一个挂起例程由 .suspend()、.freeze() 和 .poweroff() 成员指向,一个恢复例程由 .resume()、.thaw() 和 .restore() 成员指向。
3.1.19. 电源管理的驱动程序标志¶
PM 核心允许设备驱动程序设置标志,这些标志会影响核心本身和包括 PCI 总线类型在内的中间层代码对设备的电源管理的处理。这些标志应在驱动程序探测时通过 dev_pm_set_driver_flags() 函数设置一次,并且之后不应直接更新。
DPM_FLAG_NO_DIRECT_COMPLETE 标志阻止 PM 核心使用直接完成机制,如果在系统挂起开始时设备处于运行时挂起状态,则允许跳过设备挂起/恢复回调。这也会影响设备的所有祖先,因此只有在绝对必要时才应使用此标志。
DPM_FLAG_SMART_PREPARE 标志导致 PCI 总线类型仅在设备的驱动程序提供的 ->prepare 回调返回正值时才从 pci_pm_prepare() 返回正值。这允许驱动程序动态地选择不使用直接完成机制(而设置 DPM_FLAG_NO_DIRECT_COMPLETE 表示永久选择不使用)。
DPM_FLAG_SMART_SUSPEND 标志告诉 PCI 总线类型,从驱动程序的角度来看,在系统挂起期间可以将设备安全地保留在运行时挂起状态。这会导致 pci_pm_suspend()、pci_pm_freeze() 和 pci_pm_poweroff() 避免从运行时挂起恢复设备,除非有 PCI 特定的原因这样做。此外,如果设备在正在进行的系统范围转换的“后期”阶段仍处于运行时挂起状态,它会导致 pci_pm_suspend_late/noirq() 和 pci_pm_poweroff_late/noirq() 提前返回。此外,如果设备在 pci_pm_resume_noirq() 或 pci_pm_restore_noirq() 中处于运行时挂起状态,其运行时 PM 状态将更改为“活动”(因为它将进入 D0)。
设置 DPM_FLAG_MAY_SKIP_RESUME 标志意味着驱动程序允许在系统范围转换到工作状态后,如果设备可以保持挂起状态,则跳过其“noirq”和“早期”恢复回调。PM 核心会考虑此标志以及设备的 power.may_skip_resume 状态位,该状态位由 pci_pm_suspend_noirq() 在某些情况下设置。如果 PM 核心确定应跳过驱动程序的“noirq”和“早期”恢复回调,dev_pm_skip_resume() 辅助函数将返回“true”,这将导致 pci_pm_resume_noirq() 和 pci_pm_resume_early() 预先返回,而不接触设备并执行驱动程序回调。
3.2. 设备运行时电源管理¶
除了提供设备电源管理回调外,PCI 设备驱动程序还负责控制其设备的运行时电源管理(运行时 PM)。
PCI 设备运行时 PM 是可选的,但建议 PCI 设备驱动程序至少在存在可靠方法验证设备未被使用的情况下(例如,当网线从以太网适配器断开或没有设备连接到 USB 控制器时)实现它。
为了支持 PCI 运行时 PM,驱动程序首先需要实现 runtime_suspend() 和 runtime_resume() 回调函数。它可能还需要实现 runtime_idle() 回调函数,以防止设备在 runtime_resume() 回调函数返回后立即再次被挂起(或者,runtime_suspend() 回调函数必须检查设备是否真的应该被挂起,如果不是,则返回 -EAGAIN)。
PCI 设备的运行时 PM 默认由 PCI 核心启用。PCI 设备驱动程序不需要启用它,也不应该尝试这样做。但是,它会被运行 pm_runtime_forbid() 辅助函数的 pci_pm_init() 阻止。此外,每个 PCI 设备的运行时 PM 使用计数器会在执行设备驱动程序提供的 probe 回调函数之前,由 local_pci_probe() 递增。
如果 PCI 驱动程序实现了运行时 PM 回调函数,并打算使用 PM 核心和 PCI 子系统提供的运行时 PM 框架,则需要在其 probe 回调函数中递减设备的运行时 PM 使用计数器。如果不这样做,则设备的计数器将始终不为零,并且永远不会运行时挂起。最简单的方法是调用 pm_runtime_put_noidle(),但是如果驱动程序想要立即调度自动挂起,例如,可以为此目的调用 pm_runtime_put_autosuspend()。一般来说,它只需要从其 probe 例程中调用一个递减设备使用计数器的函数,以使运行时 PM 对该设备起作用。
重要的是要记住,驱动程序的 runtime_suspend() 回调函数可能会在使用计数器递减后立即执行,因为用户空间可能已经通过 sysfs 导致 pm_runtime_allow() 辅助函数运行,从而解除设备的运行时 PM 阻止,因此驱动程序必须准备好应对这种情况。
但是,驱动程序本身不应该调用 pm_runtime_allow()。相反,它应该让用户空间或某些特定于平台的代码来完成(如上所述,用户空间可以通过 sysfs 来完成),但它必须准备好在调用 pm_runtime_allow() 时(这可能在任何时间发生,甚至在驱动程序加载之前)正确处理设备的运行时 PM。
当驱动程序的 remove 回调函数运行时,它必须平衡在 probe 时递减的设备运行时 PM 使用计数器。因此,如果它在其 probe 回调函数中递减了计数器,则必须在其 remove 回调函数中运行 pm_runtime_get_noresume()。[由于核心在运行驱动程序的 remove 回调函数之前执行设备的运行时恢复并增加设备的使用计数器,因此设备的运行时 PM 在 remove 执行期间被有效地禁用,并且所有增加设备使用计数器的运行时 PM 辅助函数实际上等效于 pm_runtime_get_noresume()。]
运行时 PM 框架通过处理挂起或恢复设备的请求,或检查它们是否空闲(在这种情况下,随后请求挂起它们是合理的)来工作。这些请求由放入电源管理工作队列 pm_wq 中的工作项表示。虽然在某些情况下,PM 核心会自动将电源管理请求排队(例如,在处理恢复设备的请求后,PM 核心会自动将检查设备是否空闲的请求排队),但设备驱动程序通常负责为其设备排队电源管理请求。为此,它们应该使用 PM 核心提供的运行时 PM 辅助函数,这些函数在 I/O 设备的运行时电源管理框架 中讨论。
设备也可以同步地挂起和恢复,而无需将请求放入 pm_wq 中。在大多数情况下,这也是由其驱动程序完成的,这些驱动程序为此目的使用 PM 核心提供的辅助函数。
有关设备运行时 PM 的更多信息,请参阅 I/O 设备的运行时电源管理框架。
4. 资源¶
PCI 本地总线规范,修订版 3.0
PCI 总线电源管理接口规范,修订版 1.2
高级配置和电源接口(ACPI)规范,修订版 3.0b
PCI Express 基本规范,修订版 2.0