挂起代码 (S3) 与 CPU 热插拔基础设施的交互¶
2011 - 2014 Srivatsa S. Bhat <srivatsa.bhat@linux.vnet.ibm.com>
I. CPU 热插拔和挂起到 RAM 之间的区别¶
常规 CPU 热插拔代码与挂起到 RAM 基础设施在内部使用它的方式有何不同?它们在哪里共享公共代码?
嗯,一图胜千言... 所以接下来是 ASCII 艺术 :-)
[这描绘了内核中的当前设计,并且仅关注涉及 freezer 和 CPU 热插拔的交互,并且还试图解释所涉及的锁定。它还概述了所涉及的通知。但请注意,这里仅说明了调用路径,目的是描述它们在何处采用不同的路径以及在哪里共享代码。当常规 CPU 热插拔和挂起到 RAM 相互竞争时会发生什么,这里没有描述。]
在较高层面上,挂起-恢复周期如下所示
|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()
II. CPU 热插拔中涉及哪些问题?¶
以下讨论了一些涉及 CPU 热插拔和 CPU 上微代码更新的有趣情况
[请记住,内核使用在 drivers/base/firmware_loader/main.c 中定义的 request_firmware()
函数从用户空间请求微代码映像]
当所有 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 脱机期间实际上不会断电。它们只是被置于可能的最低 C 状态。因此,在这种情况下,当 CPU 重新联机时,实际上没有必要重新应用微代码,因为它们在 CPU 脱机操作期间不会丢失映像。
这是在挂起后恢复期间遇到的通常情况。但是,在休眠的情况下,由于所有 CPU 都完全断电,因此在恢复期间有必要将微代码映像应用于所有 CPU。
[请注意,我们不希望有人在挂起-恢复或休眠/恢复周期之间物理拔出节点并插入具有不同类型 CPU 的节点。]
但是,在内核的当前设计中,在作为挂起/休眠周期的一部分的 CPU 脱机操作期间(设置了 cpuhp_tasks_frozen),内核中现有的微代码映像副本不会被释放。在 CPU 联机操作(在恢复/还原期间)期间,由于内核发现它已经拥有所有 CPU 的微代码映像副本,因此它只是将它们应用于 CPU,避免了任何 CPU 类型/型号的重新发现,并且避免了验证微代码版本是否适合 CPU 的需要(由于上述假设,即物理 CPU 热插拔不会在挂起/恢复或休眠/恢复周期之间完成)。
III. 已知问题¶
当常规 CPU 热插拔和挂起相互竞争时,是否存在任何已知问题?
是的,它们在下面列出
当调用常规 CPU 热插拔时,传递给 _cpu_down() 和 _cpu_up() 函数的 ‘tasks_frozen’ 参数始终为 0。这可能无法反映系统的真实当前状态,因为任务可能已被带外事件(例如正在进行的挂起操作)冻结。因此,cpuhp_tasks_frozen 变量将不会反映冻结状态,并且评估该变量的 CPU 热插拔回调可能会执行错误的代码路径。
如果常规 CPU 热插拔压力测试恰好与由于同时正在进行的挂起操作而导致 freezer 竞争,那么我们可能会遇到下面描述的情况
常规 CPU 联机操作从用户空间继续进入内核,因为冻结尚未开始。
然后 freezer 开始工作并冻结用户空间。
如果 cpu 联机到目前为止尚未完成微代码更新工作,它现在将开始在 TASK_UNINTERRUPTIBLE 状态下等待冻结的用户空间,以便获取微代码映像。
现在 freezer 继续并尝试冻结剩余的任务。但是由于上面提到的等待,freezer 将无法冻结 cpu 联机热插拔任务,因此冻结任务失败。
由于此任务冻结失败,挂起操作被中止。