掛起程式碼 (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 聯機熱插拔任務,因此任務凍結失敗。

    由於此任務凍結失敗,因此掛起操作被中止。