基於linux-5.10
一、任務凍結概述
進程凍結是當系統hibernate或者suspend時,對進程進行暫停掛起的一種機制,本質上是對先將任務狀態設置為 TASK_UNINTERRUPTIBLE,然后再調用schedule()將任務切走。主要用於配合系統的suspend和resume相關機制,當然freezer cgroup也提供了對一批進程進行凍結的機制。用戶空間進程默認可以被凍結,內核線程默認不能被動凍結。
1. 有3個 per-task 的flag用於描述進程凍結狀態:
PF_NOFREEZE:表示此任務是否允許被凍結,1表示不可凍結,0表示可凍結
PF_FROZEN:表示進程是否已經處於被凍結狀態
PF_FREEZER_SKIP:凍結時跳過此任務,freeze_task()和系統休眠流程中的凍結判斷有此標志位的任務就會跳過它,使用 freezer_should_skip()來判斷此標志位
2. 有3個相關的全局變量:
system_freezing_cnt:大於0表示系統進入了凍結狀態
pm_freezing: true表示用戶進程被凍結
pm_nosig_freezing: true表示內核進程和workqueue被凍結
賦值調用路徑:
state_store //用戶echo mem > state pm_suspend //suspend.c 觸發系統休眠的入口函數,autosleep.c hiberation.c中也有調用 enter_state //suspend.c suspend_prepare //suspend.c 其它位置也有調用,系統掛起流程中最先執行 ###### suspend_freeze_processes power.h freeze_processes //process.c atomic_inc(&system_freezing_cnt); pr_info("Freezing user space processes ... "); pm_freezing = true; freeze_kernel_threads pr_info("Freezing remaining freezable tasks ... "); pm_nosig_freezing = true; ------喚醒------ suspend_finish //suspend.c 系統喚醒流程中最后執行 ####### suspend_thaw_processes thaw_processes atomic_dec(&system_freezing_cnt); pm_freezing = false; pm_nosig_freezing = false; pr_info("Restarting tasks ... ");
報錯后回退的執行路徑,非主要路徑:
suspend_prepare //suspend.c suspend_freeze_processes //power.h freeze_kernel_threads //只有執行try_to_freeze_tasks()返回錯誤時才執行 thaw_kernel_threads pm_nosig_freezing = false; pr_info("Restarting kernel threads ... ");
注:由於沒有使能 CONFIG_HIBERNATION 和 CONFIG_HIBERNATION_SNAPSHOT_DEV,因此 kernel/power/user.c 和 kernel/power/hibernate.c 是不使用的。
3. 凍結和解凍的主要函數:
freeze_processes(): - 僅凍結用戶空間任務 freeze_kernel_threads(): - 凍結所有任務(包括內核線程),因為無法在不凍結用戶空間任務的情況下凍結內核線程 thaw_kernel_threads(): - 僅解凍內核線程;如果需要在內核線程的解凍和用戶空間任務的解凍之間做一些特殊的事情,或者如果想推遲用戶空間任務的解凍,這將特別有用 thaw_processes(): - 解凍所有任務(包括內核線程),因為無法在不解凍內核線程的情況下解凍用戶空間任務
4. 需要凍結的原因
(1) 防止文件系統在休眠后被損壞。目前我們沒有簡單的檢查點文件系統的方法,所以如果休眠流程執行后對磁盤上的文件系統數據和/或元數據進行了任何修改,我們無法將它們恢復到修改之前的狀態。
(2) 防止為創建休眠鏡像喚出內存后進程重新分配內存。
(3) 防止用戶空間進程和一些內核線程干擾設備的掛起和恢復。盡管如此,還是有一些內核線程想要被凍結,比如驅動的內核線程原則上需要知道設備合適掛起以便不再訪問它們。若其內核線程是可凍結的,就可以做到在其.suspend()回調之前凍結,並在其.resume() 回調之后解凍。
(4) 防止用戶空間進程意識到發生了休眠(或掛起)操作。
二、凍結實現機制
1. 凍結核心函數之 __refrigerator()
//kernel/freezer.c bool __refrigerator(bool check_kthr_stop) { /* Hmm, should we be allowed to suspend when there are realtime processes around? */ bool was_frozen = false; long save = current->state; pr_debug("%s entered refrigerator\n", current->comm); for (;;) { set_current_state(TASK_UNINTERRUPTIBLE); spin_lock_irq(&freezer_lock); current->flags |= PF_FROZEN; //標記任務被凍結 //若是不運行凍結當前任務,或在要求檢查內核線程should_stop且是should_stop時,取消凍結 if (!freezing(current) || (check_kthr_stop && kthread_should_stop())) current->flags &= ~PF_FROZEN; //取消任務被凍結標記 trace_android_rvh_refrigerator(pm_nosig_freezing); spin_unlock_irq(&freezer_lock); //若是當前線程不允許被凍結,就退出 if (!(current->flags & PF_FROZEN)) break; was_frozen = true; //將當前任務切走,resume后從這里繼續開始執行 schedule(); } pr_debug("%s left refrigerator\n", current->comm); /* * Restore saved task state before returning. The mb'd version * needs to be used; otherwise, it might silently break * synchronization which depends on ordered task state change. */ set_current_state(save); //current->state=save //返回是否被凍結的狀態 return was_frozen; } EXPORT_SYMBOL(__refrigerator); //include/linux/freezer.h 檢查是否允許凍結此任務 static inline bool freezing(struct task_struct *p) { //若是系統沒有處於凍結流程中,直接不允許凍結 if (likely(!atomic_read(&system_freezing_cnt))) return false; return freezing_slow_path(p); } //kernel/freezer.c 檢測一個任務是否應該被凍結的慢速路徑 bool freezing_slow_path(struct task_struct *p) { /* * 若此線程不運行被凍結 或 是執行freeze_processes() * 的那個任務,就不應該被凍結,畢竟不應該凍結自己, * 否則怎么繼續執行suspend流程呢。 */ if (p->flags & (PF_NOFREEZE | PF_SUSPEND_TASK)) return false; //被 OOM killer 干掉的任務不應該被凍結 if (test_tsk_thread_flag(p, TIF_MEMDIE)) return false; /* * 系統suspend流程已經走到凍結內核線程了或是任務所在的 * cgroup進行的凍結,那允許凍結. */ if (pm_nosig_freezing || cgroup_freezing(p)) return true; /* * 系統suspend流程已經走到凍結用戶空間任務了但是還沒有 * 走到凍結內核線程那里,若非內核線程,也就是用戶空間進 * 程,就允許凍結 */ if (pm_freezing && !(p->flags & PF_KTHREAD)) return true; //否則不運行凍結 return false; } EXPORT_SYMBOL(freezing_slow_path);
可以看到,對任務進行凍結的本質就是將任務的狀態設置為 TASK_UNINTERRUPTIBLE,然后將任務切走。但注意這里判斷的只是current線程,使用上有限制。
2. 任務凍結
/* * kernel/freezer.c * * freeze_task - 向給定任務發送凍結請求 * @p: 向此任務發送請求 * * 如果@p 正在凍結,則通過發送假信號(如果它不是內核線程)或喚醒它(如果它是內核線程)來發送凍結請求。 * * 返回:%false,如果@p 沒有凍結或已經凍結; 否則返回%true */ bool freeze_task(struct task_struct *p) { unsigned long flags; /* * This check can race with freezer_do_not_count, but worst case that * will result in an extra wakeup being sent to the task. It does not * race with freezer_count(), the barriers in freezer_count() and * freezer_should_skip() ensure that either freezer_count() sees * freezing == true in try_to_freeze() and freezes, or * freezer_should_skip() sees !PF_FREEZE_SKIP and freezes the task * normally. */ //跳過標記為 PF_FREEZER_SKIP 的任務 if (freezer_should_skip(p)) return false; spin_lock_irqsave(&freezer_lock, flags); //如果不允許凍結或已經被凍結,則返回false if (!freezing(p) || frozen(p)) { spin_unlock_irqrestore(&freezer_lock, flags); return false; } //用戶進程和內核線程的凍結機制不同 if (!(p->flags & PF_KTHREAD)) fake_signal_wake_up(p); //通過一個假信號喚醒用戶進程,注意也是只喚醒 INTERRUPTIBLE 類型的用戶進程 else wake_up_state(p, TASK_INTERRUPTIBLE); //喚醒內核線程,注意只喚醒 INTERRUPTIBLE 類型的內核線程 spin_unlock_irqrestore(&freezer_lock, flags); return true; }
(1) 凍結用戶進程
//kernel/freezer.c static void fake_signal_wake_up(struct task_struct *p) { unsigned long flags; if (lock_task_sighand(p, &flags)) { signal_wake_up(p, 0); //通過信號喚醒任務 unlock_task_sighand(p, &flags); } } static inline void signal_wake_up(struct task_struct *t, bool resume) { signal_wake_up_state(t, resume ? TASK_WAKEKILL : 0); //上面傳參是0,這里也是0 } void signal_wake_up_state(struct task_struct *t, unsigned int state) { //設置 TIF_SIGPENDING=bit0, check的時候位與 _TIF_SIGPENDING=(1<<0) set_tsk_thread_flag(t, TIF_SIGPENDING); if (!wake_up_state(t, state | TASK_INTERRUPTIBLE)) //state傳的是0,也是只喚醒INTERRUPTIBLE類型的任務 kick_process(t); }
接着會去走任務喚醒流程,由於休眠是發生在內核空間,最終肯定會調用 ret_to_user 來返回用戶空間。
ret_to_user //arm64/kernel/entry.S work_pending do_notify_resume //arm64/kernel/signal.c do_signal(regs) //if(thread_flags & _TIF_SIGPENDING) 為真調用 get_signal(&ksig) try_to_freeze //include/linux/freeze.h
try_to_freeze 函數:
static inline bool try_to_freeze(void) { if (!(current->flags & PF_NOFREEZE)) debug_check_no_locks_held(); return try_to_freeze_unsafe(); } static inline bool try_to_freeze_unsafe(void) { //指示當前函數可能睡眠 might_sleep(); //判斷當前進程是否需要凍結 if (likely(!freezing(current))) return false; //進行實際的凍結 return __refrigerator(false); }
(2) 凍結內核線程
對內核線程的凍結,主要是喚醒 TASK_INTERRUPTIBLE 狀態的內核線程,然后由內核線程自己進行凍結自己的操作。例如 freezing-of-tasks.rst 中舉的一個例子:
//例1: 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)); //例2: static int tps65090_charger_poll_task(void *data) { set_freezable(); while (!kthread_should_stop()) { schedule_timeout_interruptible(POLL_INTERVAL); try_to_freeze(); tps65090_charger_isr(-1, data); } return 0; }
由於內核線程默認是不可被凍結的,因此希望自己被凍結的內核線程需要先調用 set_freezable() 即 current->flags &= ~PF_NOFREEZE 清除PF_NOFREEZE標志位,將自己設置為可凍結的。然后在輪詢邏輯中調用 try_to_freeze() 以便在系統suspend流程中喚醒內核線程時能自己凍結自己。
3. 系統suspend時全局凍結
在系統suspend早期,suspend_prepare()時調用 suspend_freeze_processes() 進行任務凍結
static inline int suspend_freeze_processes(void) { int error; error = freeze_processes(); //凍結用戶進程 /* * freeze_processes() automatically thaws every task if freezing * fails. So we need not do anything extra upon error. */ if (error) return error; error = freeze_kernel_threads(); //凍結內核線程 /* * freeze_kernel_threads() thaws only kernel threads upon freezing * failure. So we have to thaw the userspace tasks ourselves. */ if (error) thaw_processes(); return error; }
凍結所有的用戶進程:
/* * freeze_processes - 向用戶空間進程發出信號以進入凍結。當前線程不會被凍結。 * 調用 freeze_processes() 的這個進程必須在之后調用 thaw_processes()。 * * 成功時,返回 0。失敗時,-errno 和系統完全解凍。 */ int freeze_processes(void) { int error; //固件加載時有使用到這一機制 error = __usermodehelper_disable(UMH_FREEZING); if (error) return error; /* Make sure this task doesn't get frozen */ current->flags |= PF_SUSPEND_TASK; //在freezing_slow_path()中會判斷 if (!pm_freezing) atomic_inc(&system_freezing_cnt); //標記開始凍結用戶空間進程 pm_wakeup_clear(true); pr_info("Freezing user space processes ... "); pm_freezing = true; error = try_to_freeze_tasks(true); if (!error) { __usermodehelper_set_disable_depth(UMH_DISABLED); //凍結失敗后的恢復操作 pr_cont("done."); } pr_cont("\n"); BUG_ON(in_atomic()); /* * Now that the whole userspace is frozen we need to disable * the OOM killer to disallow any further interference with * killable tasks. There is no guarantee oom victims will * ever reach a point they go away we have to wait with a timeout. */ if (!error && !oom_killer_disable(msecs_to_jiffies(freeze_timeout_msecs))) error = -EBUSY; if (error) thaw_processes(); //凍結失敗后的恢復操作 return error; } //參數user_only為假就只凍結用戶空間進程,若是為真內核線程也凍結。 static int try_to_freeze_tasks(bool user_only) { struct task_struct *g, *p; unsigned long end_time; unsigned int todo; bool wq_busy = false; ktime_t start, end, elapsed; unsigned int elapsed_msecs; bool wakeup = false; int sleep_usecs = USEC_PER_MSEC; start = ktime_get_boottime(); /* * 允許凍結所有用戶空間進程或所有可凍結內核線程最多花費多長時間,來自 /sys/power/pm_freeze_timeout, * 單位毫秒,默認值為 20000ms. */ end_time = jiffies + msecs_to_jiffies(freeze_timeout_msecs); if (!user_only) freeze_workqueues_begin(); //開始凍結內核工作隊列 //死循環去輪詢,直到沒有需要被凍結的任務了,或超時了,或有喚醒事件觸發了。 while (true) { todo = 0; //每一輪都是從0開始 ###### read_lock(&tasklist_lock); //需要持有這個鎖進行遍歷所有任務 //對每一個任務都執行 for_each_process_thread(g, p) { //freeze_task中進行實際的凍結,對於用戶進程發信號,對於內核線程是喚醒 if (p == current || !freeze_task(p)) continue; //跳過標記了 PF_FREEZER_SKIP 標記的任務 if (!freezer_should_skip(p)) todo++; //統計的需要凍結的線程數量 } read_unlock(&tasklist_lock); if (!user_only) { //只要有一個 pool_workqueue::nr_active 不為0,就返回true wq_busy = freeze_workqueues_busy(); todo += wq_busy; } //若沒有需要凍結的任務了,或超時了,就退出 if (!todo || time_after(jiffies, end_time)) break; //有pending的喚醒事件,就要退出系統的休眠流程 if (pm_wakeup_pending()) { wakeup = true; break; } /* * We need to retry, but first give the freezing tasks some * time to enter the refrigerator. Start with an initial * 1 ms sleep followed by exponential backoff until 8 ms. */ //睡眠 0.5ms-- 8ms, 避免輪詢的太頻繁導致高負載 usleep_range(sleep_usecs / 2, sleep_usecs); if (sleep_usecs < 8 * USEC_PER_MSEC) sleep_usecs *= 2; } //使用單調增的boottime時鍾記錄上面輪詢持續的時間 end = ktime_get_boottime(); elapsed = ktime_sub(end, start); elapsed_msecs = ktime_to_ms(elapsed); if (wakeup) { //由喚醒事件導致的輪詢退出 pr_cont("\n"); pr_err("Freezing of tasks aborted after %d.%03d seconds", elapsed_msecs/1000, elapsed_msecs%1000); } else if (todo) { //由超時導致的輪詢退出 pr_cont("\n"); 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); //若是在沒能凍結的任務中有workqueue,還會打印出workqueue的狀態 if (wq_busy) show_workqueue_state(); /* * 若是使能了 CONFIG_PM_SLEEP_DEBUG,可由/sys/power/pm_debug_messages來控制,否則只能通過"pm_debug_messages" * 這個啟動參數來使能這個debug開關。也可以通過改代碼的形式默認設為true,無其它依賴。 */ if (pm_debug_messages_on) { read_lock(&tasklist_lock); for_each_process_thread(g, p) { /* * 遍歷系統中的每一個任務,若此任務不是正在執行凍結的任務,又不是凍結需要跳過的任務,又是需要被凍結的 * 任務,但是又沒有被凍結,則會打印這類任務的信息。其中信息包括:任務名、任務狀態、父任務、任務此時flags、 * 若是workqueue中的worker線程還會打印workqueue信息、棧回溯。 */ if (p != current && !freezer_should_skip(p) && freezing(p) && !frozen(p)) { sched_show_task(p); trace_android_vh_try_to_freeze_todo_unfrozen(p); } } read_unlock(&tasklist_lock); } trace_android_vh_try_to_freeze_todo(todo, elapsed_msecs, wq_busy); } else { //需要凍結的任務都凍結成功了 //打印凍結持續的時間 pr_cont("(elapsed %d.%03d seconds) ", elapsed_msecs/1000, elapsed_msecs%1000); } //若返回非0,則整個休眠流程會終止。 return todo ? -EBUSY : 0; }
最終成功退出的條件是:所有沒有標記 PF_FREEZER_SKIP 的線程全部被凍結了。凍結失敗的原因是:超時了還沒全部凍結,或有喚醒源事件產生。
凍結所有內核線程:
int freeze_kernel_threads(void) { int error; pr_info("Freezing remaining freezable tasks ... "); pm_nosig_freezing = true; error = try_to_freeze_tasks(false); if (!error) pr_cont("done."); pr_cont("\n"); BUG_ON(in_atomic()); if (error) thaw_kernel_threads(); return error; }
和凍結所有用戶進程的 freeze_processes() 相比較可以發現,主要是 try_to_freeze_tasks() 傳的參數不同,而參數導致的差異也只是是否凍結workqueue。
三、任務解凍
在系統喚醒的后期,會執行 suspend_thaw_processes() 來喚醒所有任務
static inline void suspend_thaw_processes(void) { thaw_processes(); } void thaw_processes(void) { struct task_struct *g, *p; struct task_struct *curr = current; trace_suspend_resume(TPS("thaw_processes"), 0, true); if (pm_freezing) atomic_dec(&system_freezing_cnt); //計算減1,表示退出系統suspend pm_freezing = false; pm_nosig_freezing = false; oom_killer_enable(); pr_info("Restarting tasks ... "); __usermodehelper_set_disable_depth(UMH_FREEZING); thaw_workqueues(); //解凍workqueue cpuset_wait_for_hotplug(); read_lock(&tasklist_lock); //對系統中的每個任務都執行 for_each_process_thread(g, p) { /* No other threads should have PF_SUSPEND_TASK set */ //只有當前執行凍結/解凍的線程才能有 PF_SUSPEND_TASK 標志 WARN_ON((p != curr) && (p->flags & PF_SUSPEND_TASK)); __thaw_task(p); } read_unlock(&tasklist_lock); WARN_ON(!(curr->flags & PF_SUSPEND_TASK)); curr->flags &= ~PF_SUSPEND_TASK; //執行完解凍清除當前任務的 PF_SUSPEND_TASK 標志。 usermodehelper_enable(); schedule(); //將當前線程切走,當前線程就是echo mem > /sys/power/suspend的線程,即spspend hal線程 pr_cont("done.\n"); trace_suspend_resume(TPS("thaw_processes"), 0, false); }
喚醒不再區分是內核線程還是用戶空間進程,統一執行 __thaw_task(p),而這個函數只是簡單的執行 wake_up_process(p),執行喚醒任務的動作。
void __thaw_task(struct task_struct *p) { unsigned long flags; const struct cpumask *mask = task_cpu_possible_mask(p); spin_lock_irqsave(&freezer_lock, flags); /* * Wake up frozen tasks. On asymmetric systems where tasks cannot * run on all CPUs, ttwu() may have deferred a wakeup generated * before thaw_secondary_cpus() had completed so we generate * additional wakeups here for tasks in the PF_FREEZER_SKIP state. */ //若任務p是被凍結狀態的,或是凍結或跳過凍結的且其親和性不是所有的possible cpu,就喚醒任務P if (frozen(p) || (frozen_or_skipped(p) && mask != cpu_possible_mask)) /* * 注意這里喚醒的是 TASK_NORMAL 也即是 INTERRUPTIBLE 和 UNINTERRUPTIBLE 的任務, * 而凍結時喚醒的只是 INTERRUPTIBLE 類型的任務。 */ wake_up_process(p); spin_unlock_irqrestore(&freezer_lock, flags); }
既然凍結和解凍都是喚醒線程,區別是什么呢。區別就是 system_freezing_cnt、pm_freezing、pm_nosig_freezing 三個變量的值不同,在系統suspend時的凍結流程中,它們為true,在 freezing() 函數中判斷為需要凍結,而在系統resume時的解凍流程中 freezing() 函數中判斷為不需要凍結,則會進行解凍。
四、實驗
1. 凍結一個線程
(1) 創建一個sh線程
/dev/freezer/pp # mkdir pp /dev/freezer/pp #cd pp/ /dev/freezer/pp # let i=0; while true; do let i=i+1; done & [1] 11073 /dev/freezer/pp # while true; do echo FROZEN > freezer.state; sleep 0.5; echo THAWED > freezer.state; sleep 0.5; done
(2) 抓trace看
Running process: sh (pid 11073) Running thread: UI Thread State: Uninterruptible Sleep Start: 1,261.150 ms Duration: 529.232 ms Args: {kernel callsite when blocked:: "__refrigerator+0xfc/0x2e4"}
會看到線程 11073 運行0.5s,D狀態0.5s。阻塞在 __refrigerator 函數中。
2. 凍結抖音app
/dev/freezer/pp # ps -AT | grep ugc.aweme u0_a114 31990 31990 728 1496340 198400 do_epoll_wait 0 S droid.ugc.aweme /dev/freezer/pp # echo 31990 > cgroup.procs /dev/freezer/pp # echo FROZEN > freezer.state /dev/freezer/pp # echo THAWED > freezer.state /dev/freezer/pp # ...交替往復執行
tarce上可以看到抖音正在運行的線程齊刷刷的進入D狀態和退出D狀態。
五、總結
1. 凍結的本質就是先將任務設置為 UNINTERRUPTABLE 狀態,然后再將任務切走。
2. 凍結和解凍都是靠喚醒任務實現的,根據 system_freezing_cnt、pm_freezing、pm_nosig_freezing 三個變量的值不同來決定是凍結還是解凍。
3. 用戶進程默認是可凍結的,系統suspend流程中會自動凍結用戶進程。而內核進程默認是不可以被凍結的,若是凍結指定的內核線程需要內核線程自己先清除自己的 PF_NOFREEZE 標志位,然后調用 try_to_freeze()函數凍結自己。系統凍結框架做的僅僅是喚醒內核線程而已。
4. 用戶進程的凍結借助假信號來完成,只是設置 TIF_SIGPENDING 標志位置,在喚醒進程返回用戶空間的過程中發現在系統系統在凍結用戶進程,就會調用 try_to_freeze()函數凍結自己。
5. 驅動的內核線程,在設備驅動的.suspend 回調調用之后不再支持訪問的,需要將其內核線程接入到凍結/解凍機制中。
6. freeze cgroup 對用戶空間提供了凍結/解凍進程的機制,可以根據自己系統的特性進行優化拓展。
參考:
Documentation/power/freezing-of-tasks.rst 翻譯
Cgroup內核文檔翻譯(5)——Documentation/cgroup-v1/freezer-subsystem.txt
TODO: binder_wait_for_work 中的 binder_wait_for_work()/freezer_count() 組合