排程器25—程序凍結
基於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; }
凍結所有核心執行緒:
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. 凍結的本質就是先將任務設定為 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