挂起代码 (S3) 与 CPU 热插拔基础设施的交互

  1. 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 中)从用户空间请求微代码映像。]

  1. 当所有 CPU 都相同时

    这是最常见的情况,并且非常简单:我们希望将相同的微代码修订应用于每个 CPU。 以 x86 为例,定义在 arch/x86/kernel/microcode_core.c 中的 collect_cpu_info() 函数有助于发现 CPU 的类型,从而将正确的微代码修订应用于它。 但请注意,内核不维护所有 CPU 的通用微代码映像,以便处理下面描述的“b”情况。

  2. 当某些 CPU 与其余 CPU 不同时

    在这种情况下,由于我们可能需要将不同的微代码修订应用于不同的 CPU,因此内核为每个 CPU 维护正确的微代码映像的副本(在使用 collect_cpu_info() 等函数进行适当的 CPU 类型/型号发现之后)。

  3. 当 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。

  4. 处理挂起/休眠期间的微代码更新

    严格来说,在不涉及物理移除或插入 CPU 的 CPU 热插拔操作期间,CPU 在 CPU 脱机期间实际上并未断电。 它们只是被置于尽可能低的 C 状态。 因此,在这种情况下,当 CPU 重新联机时,实际上没有必要重新应用微代码,因为它们在 CPU 脱机操作期间不会丢失映像。

    这是在挂起后恢复期间遇到的常见情况。 但是,在休眠的情况下,由于所有 CPU 都完全断电,因此在还原期间,必须将微代码映像应用于所有 CPU。

    [请注意,我们不希望有人在挂起恢复或休眠/还原周期之间物理拔出节点并插入具有不同类型 CPU 的节点。]

    但是,在内核的当前设计中,在作为挂起/休眠周期的一部分的 CPU 脱机操作期间(cpuhp_tasks_frozen 设置为 true),内核中现有微代码映像的副本不会被释放。 并且在 CPU 联机操作期间(在恢复/还原期间),由于内核发现它已经拥有所有 CPU 的微代码映像的副本,因此它只是将它们应用于 CPU,避免了任何 CPU 类型/型号的重新发现,也避免了验证微代码修订是否适合 CPU 的需要(由于上述假设,即物理 CPU 热插拔不会在挂起/恢复或休眠/还原周期之间完成)。

三、已知问题

当常规 CPU 热插拔和挂起相互竞争时,是否存在任何已知问题?

是的,它们在下面列出

  1. 在调用常规 CPU 热插拔时,传递给 _cpu_down() 和 _cpu_up() 函数的 ‘tasks_frozen’ 参数始终为 0。 这可能无法反映系统的真实当前状态,因为任务可能已被带外事件(例如正在进行的挂起操作)冻结。 因此,cpuhp_tasks_frozen 变量将不会反映冻结状态,并且评估该变量的 CPU 热插拔回调可能会执行错误的代码路径。

  2. 如果常规 CPU 热插拔压力测试恰好与 freezer 竞争,因为同时正在进行挂起操作,那么我们可能会遇到以下情况

    • 常规 CPU 联机操作继续从用户空间进入内核,因为冻结尚未开始。

    • 然后 freezer 开始工作并冻结用户空间。

    • 如果 CPU 联机现在尚未完成微代码更新,它现在将开始在 TASK_UNINTERRUPTIBLE 状态下等待冻结的用户空间,以便获取微代码映像。

    • 现在 freezer 继续并尝试冻结剩余的任务。 但是由于上面提到的等待,freezer 将无法冻结 CPU 联机热插拔任务,因此任务冻结失败。

    由于此任务冻结失败,因此挂起操作被中止。