任務凍結¶
2007 Rafael J. Wysocki <rjw@sisk.pl>, GPL
I. 什麼是任務凍結?¶
任務凍結是一種機制,用於在休眠或系統範圍掛起(在某些架構上)期間控制使用者空間程序和一些核心執行緒。
II. 它如何工作?¶
有一個每個任務的標誌(PF_NOFREEZE)和三個每個任務的狀態(TASK_FROZEN,TASK_FREEZABLE 和 __TASK_FREEZABLE_UNSAFE)用於此目的。未設定 PF_NOFREEZE 的任務(所有使用者空間任務和一些核心執行緒)被視為“可凍結”,並在系統進入睡眠狀態之前以及建立休眠映象之前以特殊方式處理(休眠直接包含在以下內容中,但描述也適用於系統範圍的掛起)。
也就是說,作為休眠過程的第一步,呼叫函式 freeze_processes()(在 kernel/power/process.c 中定義)。系統範圍的靜態鍵 freezer_active(與每個任務的標誌或狀態相反)用於指示系統是否將進行凍結操作。freeze_processes() 設定此靜態鍵。之後,它執行 try_to_freeze_tasks(),向所有使用者空間程序傳送一個偽訊號,並喚醒所有核心執行緒。所有可凍結任務必須透過呼叫 try_to_freeze() 來對此做出反應,這會導致呼叫 __refrigerator()(在 kernel/freezer.c 中定義),這會將任務的狀態更改為 TASK_FROZEN,並使其迴圈直到它被顯式的 TASK_FROZEN 喚醒。然後,該任務被視為“凍結”,因此處理此機制的函式集被稱為“冷凍器”(這些函式在 kernel/power/process.c,kernel/freezer.c 和 include/linux/freezer.h 中定義)。使用者空間任務通常在核心執行緒之前被凍結。
__refrigerator() 不得直接呼叫。而是使用 try_to_freeze() 函式(在 include/linux/freezer.h 中定義),該函式檢查任務是否要被凍結,並使任務進入 __refrigerator()。
對於使用者空間程序,try_to_freeze() 從訊號處理程式碼自動呼叫,但可凍結的核心執行緒需要在適當的位置顯式呼叫它,或者使用 wait_event_freezable() 或 wait_event_freezable_timeout() 宏(在 include/linux/wait.h 中定義),這些宏使任務進入睡眠狀態(TASK_INTERRUPTIBLE)或凍結它(TASK_FROZEN)如果設定了 freezer_active。可凍結核心執行緒的主迴圈可能如下所示
set_freezable();
while (true) {
struct task_struct *tsk = NULL;
wait_event_freezable(oom_reaper_wait, oom_reaper_list != NULL);
spin_lock_irq(&oom_reaper_lock);
if (oom_reaper_list != NULL) {
tsk = oom_reaper_list;
oom_reaper_list = tsk->oom_reaper_list;
}
spin_unlock_irq(&oom_reaper_lock);
if (tsk)
oom_reap_task(tsk);
}
(來自 mm/oom_kill.c::oom_reaper())。
如果在冷凍器啟動凍結操作後,可凍結核心執行緒未進入凍結狀態,則任務凍結將失敗,並且整個系統範圍的轉換將被取消。因此,可凍結核心執行緒必須在某處呼叫 try_to_freeze() 或使用 wait_event_freezable() 和 wait_event_freezable_timeout() 宏之一。
在系統記憶體狀態從休眠映象恢復並且裝置重新初始化後,呼叫函式 thaw_processes() 以喚醒每個凍結的任務。然後,已被凍結的任務離開 __refrigerator() 並繼續執行。
處理任務凍結和解凍的函式背後的原理¶
- freeze_processes()
僅凍結使用者空間任務
- freeze_kernel_threads()
凍結所有任務(包括核心執行緒),因為我們無法在不凍結使用者空間任務的情況下凍結核心執行緒
- thaw_kernel_threads()
僅解凍核心執行緒;如果我們需要在解凍核心執行緒和解凍使用者空間任務之間做任何特殊的事情,或者如果我們想推遲解凍使用者空間任務,這尤其有用
- thaw_processes()
解凍所有任務(包括核心執行緒),因為我們無法在不解凍核心執行緒的情況下解凍使用者空間任務
III. 哪些核心執行緒是可凍結的?¶
預設情況下,核心執行緒不可凍結。但是,核心執行緒可以透過呼叫 set_freezable() 為自己清除 PF_NOFREEZE(不允許直接重置 PF_NOFREEZE)。從這一點開始,它被認為是可凍結的,必須在適當的位置呼叫 try_to_freeze() 或 wait_event_freezable() 的變體。
IV. 我們為什麼要這樣做?¶
一般來說,使用任務凍結有兩個原因
主要原因是防止檔案系統在休眠後損壞。目前,我們沒有簡單的方法來檢查檔案系統,因此如果對磁碟上的檔案系統資料和/或元資料進行了任何修改,我們無法將它們恢復到修改前的狀態。與此同時,每個休眠映象都包含一些與檔案系統相關的資訊,這些資訊必須與從映象恢復系統記憶體狀態後磁碟上資料和元資料的狀態一致(否則檔案系統將以一種糟糕的方式損壞,通常使它們幾乎不可能修復)。因此,我們凍結可能導致磁碟上的檔案系統資料和元資料在建立休眠映象之後和系統最終關閉電源之前被修改的任務。這些任務中的大多數是使用者空間程序,但如果任何核心執行緒可能導致類似情況發生,則它們必須是可凍結的。
接下來,要建立休眠映象,我們需要釋放足夠的記憶體(大約 50% 的可用 RAM),並且我們需要在裝置停用之前執行此操作,因為我們通常需要它們來進行交換。然後,在釋放映象的記憶體後,我們不希望任務分配額外的記憶體,我們透過更早地凍結它們來防止它們這樣做。[當然,這也意味著裝置驅動程式不應在休眠之前從其 .suspend() 回撥中分配大量記憶體,但這是一個單獨的問題。]
第三個原因是防止使用者空間程序和一些核心執行緒干擾裝置的掛起和恢復。例如,當我們在掛起裝置時,在第二個 CPU 上執行的使用者空間程序可能會很麻煩,如果沒有任務凍結,我們需要採取一些保護措施來防止在這種情況下可能發生的競爭條件。
雖然 Linus Torvalds 不喜歡任務凍結,但他在 LKML 的一次討論中說了以下內容(https://lore.kernel.org/r/alpine.LFD.0.98.0704271801020.9964@woody.linux-foundation.org)
“RJW:> 我們為什麼要凍結任務或為什麼要凍結核心執行緒?
Linus:在很多方面,“完全”。
我確實意識到 IO 請求佇列問題,並且我們實際上無法在 DMA 中使用某些裝置執行 s2ram。所以我們希望能夠避免這種情況,這毫無疑問。我懷疑停止使用者執行緒然後等待同步實際上是做到這一點更簡單的方法之一。
所以在實踐中,“完全”可能會變成“為什麼要凍結核心執行緒?”,而我發現凍結使用者執行緒真的沒什麼異議。”
儘管如此,仍然有一些核心執行緒可能希望是可凍結的。例如,如果屬於裝置驅動程式的核心執行緒直接訪問裝置,原則上它需要知道裝置何時掛起,以便它不會嘗試在該時間訪問它。但是,如果核心執行緒是可凍結的,它將在驅動程式的 .suspend() 回撥執行之前被凍結,並在驅動程式的 .resume() 回撥執行後被解凍,因此在裝置掛起時它不會訪問該裝置。
凍結任務的另一個原因是防止使用者空間程序意識到休眠(或掛起)操作正在發生。理想情況下,使用者空間程序不應注意到發生了這種系統範圍的操作,並且應在恢復(或從掛起恢復)後繼續執行而沒有任何問題。不幸的是,在大多數情況下,如果沒有任務凍結,這很難實現。例如,考慮一個程序,它依賴於在執行時所有 CPU 都線上。由於我們需要在休眠期間停用非啟動 CPU,如果此程序未被凍結,它可能會注意到 CPU 的數量已更改,並且可能會因此開始錯誤地工作。
VI. 是否應採取任何預防措施以防止凍結失敗?¶
是的,有。
首先,不鼓勵獲取“system_transition_mutex”鎖以使一段程式碼與系統範圍的睡眠(如掛起/休眠)互斥。如果可能,該程式碼段必須掛接到掛起/休眠通知程式以實現互斥。有關示例,請參閱 CPU 熱插拔程式碼 (kernel/cpu.c)。
但是,如果這不可行,並且認為獲取“system_transition_mutex”是必要的,則強烈建議不要直接呼叫 mutex_[un]lock(&system_transition_mutex),因為這可能導致凍結失敗,因為如果掛起/休眠程式碼成功獲取了“system_transition_mutex”鎖,因此另一個實體未能獲取該鎖,那麼該任務將被阻塞在 TASK_UNINTERRUPTIBLE 狀態。因此,冷凍器將無法凍結該任務,從而導致凍結失敗。
但是,[un]lock_system_sleep() API 在此場景中可以安全使用,因為它們會要求冷凍器跳過凍結此任務,因為它無論如何都“足夠凍結”,因為它被阻塞在“system_transition_mutex”上,只有在整個掛起/休眠序列完成後才會釋放。因此,總而言之,請使用 [un]lock_system_sleep() 而不是直接使用 mutex_[un]lock(&system_transition_mutex)。這將防止凍結失敗。
V. 雜項¶
/sys/power/pm_freeze_timeout 控制凍結所有使用者空間程序或所有可凍結核心執行緒最多需要多長時間,單位為毫秒。預設值為 20000,範圍為無符號整數。