挂起代码 (S3) 与 CPU 热插拔基础设施的交互¶
2011 - 2014 Srivatsa S. Bhat <srivatsa.bhat@linux.vnet.ibm.com>
一、CPU 热插拔和挂起到内存之间的差异¶
常规 CPU 热插拔代码与挂起到内存基础设施内部使用它的方式有何不同? 它们在哪里共享公共代码?
好吧,一图胜千言……所以下面是 ASCII 艺术 :-)
[这描述了内核中的当前设计,并且仅关注涉及 freezer 和 CPU 热插拔的交互,并且还尝试解释所涉及的锁定。 它概述了所涉及的通知。 但请注意,此处仅说明了调用路径,目的是描述它们在哪里采用不同的路径以及在哪里共享代码。 此处未描述常规 CPU 热插拔和挂起到内存彼此竞争时会发生什么。]
在较高的层面上,挂起恢复周期如下所示
|Freeze| -> |Disable nonboot| -> |Do suspend| -> |Enable nonboot| -> |Thaw |
|tasks | | cpus | | | | cpus | |tasks|
更多细节如下
Suspend call path
-----------------
Write 'mem' to
/sys/power/state
sysfs file
|
v
Acquire system_transition_mutex lock
|
v
Send PM_SUSPEND_PREPARE
notifications
|
v
Freeze tasks
|
|
v
freeze_secondary_cpus()
/* start */
|
v
Acquire cpu_add_remove_lock
|
v
Iterate over CURRENTLY
online CPUs
|
|
| ----------
v | L
======> _cpu_down() |
| [This takes cpuhotplug.lock |
Common | before taking down the CPU |
code | and releases it when done] | O
| While it is at it, notifications |
| are sent when notable events occur, |
======> by running all registered callbacks. |
| | O
| |
| |
v |
Note down these cpus in | P
frozen_cpus mask ----------
|
v
Disable regular cpu hotplug
by increasing cpu_hotplug_disabled
|
v
Release cpu_add_remove_lock
|
v
/* freeze_secondary_cpus() complete */
|
v
Do suspend
恢复也是如此,对应的部分是(按照恢复期间的执行顺序)
thaw_secondary_cpus(),涉及
| Acquire cpu_add_remove_lock | Decrease cpu_hotplug_disabled, thereby enabling regular cpu hotplug | Call _cpu_up() [for all those cpus in the frozen_cpus mask, in a loop] | Release cpu_add_remove_lock v
解冻任务
发送 PM_POST_SUSPEND 通知
释放 system_transition_mutex 锁。
需要注意的是,system_transition_mutex 锁是在一开始就获取的,即当我们刚开始挂起时,并且只有在整个周期完成(即,挂起 + 恢复)后才会释放。
Regular CPU hotplug call path
-----------------------------
Write 0 (or 1) to
/sys/devices/system/cpu/cpu*/online
sysfs file
|
|
v
cpu_down()
|
v
Acquire cpu_add_remove_lock
|
v
If cpu_hotplug_disabled > 0
return gracefully
|
|
v
======> _cpu_down()
| [This takes cpuhotplug.lock
Common | before taking down the CPU
code | and releases it when done]
| While it is at it, notifications
| are sent when notable events occur,
======> by running all registered callbacks.
|
|
v
Release cpu_add_remove_lock
[That's it!, for
regular CPU hotplug]
因此,从两个图中可以看出(标记为“通用代码”的部分),常规 CPU 热插拔和挂起代码路径在 _cpu_down() 和 _cpu_up() 函数处收敛。 它们传递给这些函数的参数不同,即在常规 CPU 热插拔期间,‘tasks_frozen’ 参数传递为 0。但是在挂起期间,由于任务在非启动 CPU 脱机或联机时已经被冻结,因此调用 _cpu_*() 函数时,‘tasks_frozen’ 参数设置为 1。 [请参阅下文了解有关此问题的一些已知问题。]
重要文件和函数/入口点:¶
kernel/power/process.c : freeze_processes(), thaw_processes()
kernel/power/suspend.c : suspend_prepare(), suspend_enter(), suspend_finish()
kernel/cpu.c: cpu_[up|down](), _cpu_[up|down](), [disable|enable]_nonboot_cpus()
二、CPU 热插拔涉及哪些问题?¶
CPU 热插拔和 CPU 上的微代码更新涉及一些有趣的情况,如下所述
[请记住,内核使用 request_firmware()
函数(定义在 drivers/base/firmware_loader/main.c 中)从用户空间请求微代码映像。]
当所有 CPU 都相同时
这是最常见的情况,并且非常简单:我们希望将相同的微代码修订应用于每个 CPU。 以 x86 为例,定义在 arch/x86/kernel/microcode_core.c 中的 collect_cpu_info() 函数有助于发现 CPU 的类型,从而将正确的微代码修订应用于它。 但请注意,内核不维护所有 CPU 的通用微代码映像,以便处理下面描述的“b”情况。
当某些 CPU 与其余 CPU 不同时
在这种情况下,由于我们可能需要将不同的微代码修订应用于不同的 CPU,因此内核为每个 CPU 维护正确的微代码映像的副本(在使用 collect_cpu_info() 等函数进行适当的 CPU 类型/型号发现之后)。
当 CPU 被物理热插拔并且一个新的(并且可能是不同类型的)CPU 热插拔到系统中时
在内核的当前设计中,每当 CPU 在常规 CPU 热插拔操作期间脱机时,在收到 CPU 热插拔代码发送的 CPU_DEAD 通知后,微代码更新驱动程序针对该事件的回调通过释放内核中该 CPU 的微代码映像的副本做出反应。
因此,当一个新的 CPU 上线时,由于内核发现它没有微代码映像,因此它会重新进行 CPU 类型/型号发现,然后向用户空间请求该 CPU 的适当微代码映像,然后将其应用。
例如,在 x86 中,mc_cpu_callback() 函数(它是为 CPU 热插拔事件注册的微代码更新驱动程序的回调)调用 microcode_update_cpu(),在这种情况下,它将调用 microcode_init_cpu(),而不是 microcode_resume_cpu(),因为它发现内核没有有效的微代码映像。 这确保在从用户空间获取 CPU 后,执行 CPU 类型/型号发现并将正确的微代码应用于 CPU。
处理挂起/休眠期间的微代码更新
严格来说,在不涉及物理移除或插入 CPU 的 CPU 热插拔操作期间,CPU 在 CPU 脱机期间实际上并未断电。 它们只是被置于尽可能低的 C 状态。 因此,在这种情况下,当 CPU 重新联机时,实际上没有必要重新应用微代码,因为它们在 CPU 脱机操作期间不会丢失映像。
这是在挂起后恢复期间遇到的常见情况。 但是,在休眠的情况下,由于所有 CPU 都完全断电,因此在还原期间,必须将微代码映像应用于所有 CPU。
[请注意,我们不希望有人在挂起恢复或休眠/还原周期之间物理拔出节点并插入具有不同类型 CPU 的节点。]
但是,在内核的当前设计中,在作为挂起/休眠周期的一部分的 CPU 脱机操作期间(cpuhp_tasks_frozen 设置为 true),内核中现有微代码映像的副本不会被释放。 并且在 CPU 联机操作期间(在恢复/还原期间),由于内核发现它已经拥有所有 CPU 的微代码映像的副本,因此它只是将它们应用于 CPU,避免了任何 CPU 类型/型号的重新发现,也避免了验证微代码修订是否适合 CPU 的需要(由于上述假设,即物理 CPU 热插拔不会在挂起/恢复或休眠/还原周期之间完成)。
三、已知问题¶
当常规 CPU 热插拔和挂起相互竞争时,是否存在任何已知问题?
是的,它们在下面列出
在调用常规 CPU 热插拔时,传递给 _cpu_down() 和 _cpu_up() 函数的 ‘tasks_frozen’ 参数始终为 0。 这可能无法反映系统的真实当前状态,因为任务可能已被带外事件(例如正在进行的挂起操作)冻结。 因此,cpuhp_tasks_frozen 变量将不会反映冻结状态,并且评估该变量的 CPU 热插拔回调可能会执行错误的代码路径。
如果常规 CPU 热插拔压力测试恰好与 freezer 竞争,因为同时正在进行挂起操作,那么我们可能会遇到以下情况
常规 CPU 联机操作继续从用户空间进入内核,因为冻结尚未开始。
然后 freezer 开始工作并冻结用户空间。
如果 CPU 联机现在尚未完成微代码更新,它现在将开始在 TASK_UNINTERRUPTIBLE 状态下等待冻结的用户空间,以便获取微代码映像。
现在 freezer 继续并尝试冻结剩余的任务。 但是由于上面提到的等待,freezer 将无法冻结 CPU 联机热插拔任务,因此任务冻结失败。
由于此任务冻结失败,因此挂起操作被中止。