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

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

  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。

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

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

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

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

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

III. 已知问题

当常规 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 联机热插拔任务,因此冻结任务失败。

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