進程凍結學習筆記


一、內核進程凍結文檔Documentation\power\freezing-of-tasks.txt翻譯

任務凍結
(C)2007拉斐爾·J·懷索克<rjw@sisk.pl>,GPL

I.什么是任務凍結?

任務凍結是一種機制,在休眠或系統掛起(在某些體系結構上)期間,通過該機制可以控制用戶空間進程和某些內核線程。

二.  它是如何工作的?

每個任務有三個標志 PF_NOFREEZEPF_FROZENPF_FREEZER_SKIP(最后一個是輔助標志)。 未設置 PF_NOFREEZE 的任務(所有用戶空間進程和某些內核線程)被視為“可凍結的”,並在系統進入掛起狀態之前以及在創建休眠映像之前以特殊方式進行處理(在此之后,我們僅考慮休眠,但說明也適用於暫停)。即,作為休眠過程的第一步,將調用函數 freeze_processes()(在kernel/power/process.c中定義)。

系統范圍的變量 system_freezing_cnt(與每個任務的標志相反)用於指示系統是否在進行凍結操作。然后 freeze_processes()設置此變量。此后,它將執行try_to_freeze_tasks(),該函數會向所有用戶空間進程發送虛假信號(fake signal),並喚醒所有內核線程。所有可凍結的任務必須通過調用 try_to_freeze()來對此作出反應,會導致對 __refrigerator()(在kernel/freezer.c中定義)的調用,該調用會設置任務的 PF_FROZEN 標志,將任務狀態更改為 TASK_UNINTERRUPTIBLE 並使其循環直到 PF_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/freezer.h中定義)將可中斷睡眠與檢查是否要凍結任務並調用 try_to_freeze()結合在一起。 可凍結內核線程的主循環可能如下所示:

set_freezable();
do {
    hub_events();
    wait_event_freezable(khubd_wait, !list_empty(&hub_event_list) || kthread_should_stop());
} while (!kthread_should_stop() || !list_empty(&hub_event_list));

(from drivers/usb/core/hub.c::hub_thread()).

如果在凍結器啟動凍結操作后,可凍結的內核線程未能調用 try_to_freeze(),則凍結任務將失敗,並且整個休眠操作將被撤銷。 因此,可凍結內核線程必須在某個地方調用 try_to_freeze() 或使用 wait_event_freezable() 和 wait_event_freezable_timeout()宏之一

從休眠映像還原系統內存狀態並重新初始化設備后,將調用函數 thaw_processes() 以便為每個凍結的任務清除 PF_FROZEN 標志。 然后,已凍結的任務退出 __refrigerator()並繼續運行。

處理任務凍結和解凍的功能背后的原理:

freeze_processes(): /* -僅凍結用戶空間任務 */

freeze_kernel_threads(): /* -凍結所有任務(包括內核線程),因為如果不凍結用戶空間任務就無法凍結內核線程 */

thaw_kernel_threads(): /* -僅解凍內核線程; 如果我們需要在解凍內核線程和解凍用戶空間任務之間做任何特殊的事情,
或者如果我們想推遲解凍用戶空間任務,這將特別有用。
*/ thaw_processes(): /* -解凍所有任務(包括內核線程),因為我們必須解凍內核線程才能解凍用戶空間任務 */

三. 哪些內核線程是可凍結的?

默認情況下,內核線程不可凍結。 但是,內核線程可以通過調用 set_freezable() 自行清除 PF_NOFREEZE(不允許直接重置PF_NOFREEZE)。 從這一點來看,它被視為可凍結的,必須在適當的位置調用 try_to_freezee()。

四、我們為什么要這樣做?

一般來說,使用任務凍結有兩個原因:

1.主要原因是為了防止文件系統在休眠后損壞。 目前,我們還沒有簡單的方法來檢查文件系統,因此,如果對磁盤上的文件系統數據和/或元數據進行了任何修改,我們將無法使其恢復到修改之前的狀態。 同時,每個休眠映像都包含一些與文件系統相關的信息,這些信息必須與從映像還原系統內存狀態后必須與磁盤上數據和元數據的狀態一致(否則,文件系統將受到嚴重破壞,通常使它們幾乎無法修復)。 因此,我們凍結了這些可能會在創建休眠映像之后以及最終關閉系統電源之前修改磁盤文件系統的數據和元數據的任務。 其中大多數是用戶空間進程,但是如果任何內核線程可能導致這種情況發生,則它們必須是可凍結的。

2.接下來,要創建休眠映像,我們需要釋放足夠的內存(大約50%的可用RAM),並且需要在停用設備之前執行此操作,因為通常需要將它們換出。 然后,在釋放映像的內存之后,我們不希望任務分配額外的內存,我們通過更早凍結它們來防止它們這樣做。 [當然,這還意味着設備驅動程序在休眠之前不應從其.suspend() 回調中分配大量內存,但這是一個單獨的問題。

3.第三個原因是為了防止用戶空間進程和某些內核線程干擾設備的掛起和恢復。 例如,當我們掛起設備時,在第二個CPU上運行的用戶空間進程可能會很麻煩,並且如果不凍結任務,我們將需要一些保護措施來防止在這種情況下可能發生的競爭情況。

盡管Linus Torvalds不喜歡凍結任務,但他在有關LKML的討論之一中表示了這一點(http://lkml.org/lkml/2007/4/27/608):

“ RJW:>為什么我們完全凍結任務或為什么凍結內核線程?

Linus:從很多方面來說,都是“。

我了解了IO請求隊列問題,實際上我們無法對DMA中間的某些設備執行s2ram。 因此,我們希望能夠避免* that *,這毫無疑問。 而且我懷疑停止用戶線程然后等待同步實際上是更簡單的方法之一。

因此,在實踐中,“全部”可能會變成“為什么凍結內核線程?” 凍結用戶線程,我認為這並不令人反感。”

仍然有一些內核線程可能需要凍結。 例如,如果屬於設備驅動程序的內核線程直接訪問設備,則原則上它需要知道設備何時掛起,以便它在那時不再嘗試訪問它。 但是,如果內核線程是可凍結的,它將在執行驅動程序的.suspend()回調之前被凍結,而在驅動程序的.resume()回調運行后將解凍,因此其不會在設備掛起時訪問他們。

4.凍結任務的另一個原因是防止用戶空間進程意識到發生了休眠(或掛起)操作。 理想情況下,用戶空間進程不應注意到發生了這種系統范圍的操作,並且應在還原(或從掛起狀態恢復)后繼續運行而沒有任何問題。 不幸的是,在最一般的情況下,如果不凍結任務,很難做到這一點。 例如,考慮一個依賴於所有CPU在運行時處於聯機狀態的進程。 由於我們需要在休眠期間禁用非引導CPU,因此如果此過程未凍結,則此進程可能會注意到CPU的數量已更改,因此可能會開始無法正常工作。

五.是否有與任務凍結有關的問題?

是的,有。

首先,如果內核線程彼此依賴,凍結它們可能會很棘手。 例如,如果內核線程A等待需要由可凍結的內核線程B完成的完成量(處於TASK_UNINTERRUPTIBLE狀態),並且B同時凍結,則A將被阻塞直到B解凍為止,這可能是不希望的。 這就是默認情況下內核線程不可凍結的原因。

其次,存在以下兩個與凍結用戶空間進程有關的問題:
1.使進程進入不間斷的睡眠會扭曲平均負載。
2.現在我們有了FUSE,再加上在用戶空間中執行設備驅動程序的框架,它變得更加復雜,因為某些用戶空間進程現在正在執行內核線程要做的事情
(https://lists.linux-foundation.org/pipermail/linux-pm/2007-May/012309.html)。

問題1.似乎是可以解決的,盡管到目前為止尚未解決。 另一個比較嚴重,但是似乎可以通過使用休眠(和掛起)通知程序來解決它(但是,在那種情況下,我們將無法避免用戶空間進程感知到發生了休眠)。

任務凍結往往會暴露出來一些與任務凍結無直接關系的問題。 例如,如果從設備驅動程序的.resume()例程調用request_firmware(),則它將超時並最終失敗,因為此時應響應請求的用戶空間進程是被凍結狀態的。 因此,表面上的失敗似乎是由於任務凍結。但是,假設固件文件位於只能通過尚未resume的設備訪問的文件系統上。 在這種情況下,無論是否使用任務凍結,request_firmware()都會失敗。 因此,該問題與凍結任務並沒有真正的關系,因為無論如何它通常都存在。

在調用suspend()之前,驅動程序必須在RAM中擁有其可能需要的所有固件。 如果保留它們不可行(例如由於它們的大小),則必須使用 Documentation/driver-api/pm/notifiers.rst 中描述的suspend通知鏈API提早請求它們。

六.有什么預防措施可以防止凍結失敗?

是的,有。

首先,不鼓勵持有 'system_transition_mutex' 鎖來從系統范圍的睡眠中互斥一段代碼,例如暫停/休眠。 如果可能的話,那段代碼必須改為掛接到掛起/休眠通知程序上,以實現互斥。 請查看CPU熱插拔代碼(kernel/cpu.c)作為示例。

但是,如果這樣做不可行,並且認為必須使用 “system_transition_mutex”,則強烈不建議您直接調用mutex_[un]lock(&system_transition_mutex),因為這可能會導致凍結失敗,因為如果suspend/hibernate代碼成功獲取了“system_transition_mutex”鎖,因此其他實體無法獲取該鎖,則該任務將在 TASK_UNINTERRUPTIBLE 狀態下被阻塞。 結果,冷凍器將無法凍結該任務,從而導致凍結失敗。

但是,在這種情況下,[un]lock_system_sleep() API是安全的,因為它們要求冷凍器跳過凍結此任務,因為無論如何它已經“凍結”了,因為它已在 “system_transition_mutex”上被阻塞住了, 此鎖僅在完成整個掛起/休眠流程之后才釋放。 因此,總而言之,請使用  [un]lock_system_sleep() 而不是直接使用互斥鎖 mutex_[un]lock(&system_transition_mutex)。 這樣可以防止凍結失敗。

七. 雜項
/sys/power/pm_freeze_timeout 控制以毫秒為單位凍結所有用戶空間進程或所有可凍結內核線程最多花費的時間。 默認值為20000,取值范圍為無符號整數。

 

補充總結:

1. 用戶進程默認是可以被凍結的,借用信號處理機制實現;內核線程和work_queue默認是不能被凍結的,少數內核線程和work_queue在創建時指定了freezable標志,這些任務需要對freeze狀態進行判斷,當系統進入freezing時,主動暫停運行。

2. kernel threads可以通過調用 kthread_freezable_should_stop() 來判斷freezing狀態,並主動調用 __refrigerator()進入凍結;work_queue通過判斷 max_active 屬性,如果 max_active=0,則不能入隊新的work,所有work延后執行。

3. 標記系統freeze狀態的有三個重要的全局變量:pm_freezing、system_freezing_cnt和pm_nosig_freezing,如果全為0,表示系統未進入凍結;

system_freezing_cnt > 0  //表示系統進入凍結,
pm_freezing = true  //表示凍結用戶進程,
pm_nosig_freezing = true  //表示凍結內核線程和workqueue。

它們會在 freeze_processes 和 freeze_kernel_threads 中置位,在 thaw_processes 和 thaw_kernel_threads 中清零。

4. fake_signal_wake_up() 函數巧妙的利用了信號處理機制,只設置任務的 TIF_SIGPENDING 位,但不傳遞任何信號,然后喚醒任務;這樣任務在返回用戶態
時會進入信號處理流程,檢查系統的freeze狀態,並做相應處理。

TODO: 確認一下 lock_system_sleep() 在哪里有額外調用嗎?

 

二、代碼分析

 

三、Debug總結

1. 進程凍結debug log打印

freeze_processes/freeze_kernel_threads //kernel/power/process.c
    try_to_freeze_tasks(bool user_only)
        //若有喚醒源持鎖導致休眠失敗打印:
        pr_err("Freezing of tasks aborted after %d.%03d seconds", elapsed_msecs / 1000, elapsed_msecs % 1000);
        //若有凍結失敗的會打印
        pr_err("Freezing of tasks failed after %d.%03d seconds (%d tasks refusing to freeze, wq_busy=%d):\n", elapsed_msecs/1000, elapsed_msecs%1000, todo-wq_busy, wq_busy);
        //若凍結失敗還會打印凍結失敗的進程
        for_each_process_thread(g, p) {
            if (p != current && !freezer_should_skip(p) && freezing(p) && !frozen(p))
                sched_show_task(p); /*打印格式eg: BuglyThread-1   R  running task        0 14555    789 0x00400809*/
            }

有個超時時間為20s的凍結死循環,不斷等待進程凍結完畢,會打印出凍結失敗進程的信息,也會打印出整個凍結過程持續的時間。

四、相關測試

1. 通過信號凍結解凍測試

(1) 測試代碼

#include <stdio.h>
#include <unistd.h>

void main()
{
    int count = 0;
    while(1) {
        printf("count=%d\n", count++);
        sleep(1);
    }
}

(2) 進行凍結解凍

root@ubuntu:/work/freeze# ps -A | grep pp
  2784 ?        00:00:00 indicator-appli
  2852 ?        00:00:19 nm-applet
  4365 pts/6    00:00:00 pp
root@ubuntu:/work/freeze# 
root@ubuntu:/work/freeze# kill -19 4365 // 19) SIGSTOP
root@ubuntu:/work/freeze# 
root@ubuntu:/work/freeze# kill -18 4365 // 18) SIGCONT

(3) 結果打印

root@ubuntu:/work/freeze# ./pp
count=0
count=1
......
count=11
count=12

[1]+  Stopped                 ./pp
root@ubuntu:/work/freeze# count=13  //解凍后接着執行
count=14
count=15

注意:這里只是模擬進程被凍結的狀態,實際進程凍結並不是給進程發任何信號,而是只設置任務的 TIF_SIGPENDING 位,讓進程返回用戶控件之前檢查是否要凍結。

 


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM