Hungtask 原理及分析

一、簡介

Linux 系統在運行過程中,可能發生各種各樣的卡死情況。有的表現爲某個或某些 CPU 無法調度其他進程或無法響應中斷,如正在 CPU 上運行的進程禁止了搶佔或禁止了本地中斷後,但其需要的資源一直無法獲得(如發生了死鎖等情況),而一直佔據着 CPU;有的表現爲某些重要進程一直不能運行,雖然不至於使某個或某些 CPU 上無法調度其他進程,但由於重要進程運行異常,系統已無法正常進行業務處理,例如重要進程長期處於 uninterruptible sleep 狀態(也就是常說的 D 狀態)或 android systemserver 的 watchdog 超時等情況。

本文主要討論進程長期處於 D 狀態或重要進程異常卡住的檢測方法,即 hungtask detect 機制。而恢復機制,一般就是在檢測到異常時,直接觸發整機重啓。

二、hungtask detect 原理及流程

hungtask detect 方法有多種,原理都很簡單。比如:

A、可以定時輪詢系統中的所有 task,然後判斷處於 D 狀態的 task 的上下文切換次數是否和之前輪詢時的相等,如果相等則表明該 task 兩個輪詢間隔期間一直處於 D 狀態,可以認爲該 task 有 hang 的情況。當然,task hang 住的情況,對於有些 task 來說沒有關係,可能其本身的邏輯就是如此,不會對系統中其它 task 產生影響;但對於一些我們認爲重要的進程,如 android 中的 systemserver、surfaceflinger 等進程,如果發生 hang 的情況,則一定會對用戶使用產生影響;還有 task 長時間處於 io wait 狀態,同樣是一種異常狀態,因爲一般來說 io 應儘快結束,而時間過長則表明 io 子系統很可能已經異常。

B、如果只是判斷系統中的重要進程是否卡住,也可以不檢查系統中所有 task 的狀態,只需要關注重要進程的運行情況。可以讓這個重要進程在規定時間內模擬餵狗操作,若發現沒有及時餵狗,則認爲該重要進程已經卡住。

以下分別討論上面所述的兩種 hungtask detect 實現方式,所列代碼均爲開源代碼,代碼鏈接見附錄參考文檔。

1、輪詢系統中的所有任務

這裏對輪詢系統中的所有任務的 hungtask detect 方式進行分析,代碼見參考文檔 1,主要涉及代碼:

kernel\hung_task.c      (linux 系統默認實現)

drivers\soc\qcom\hung_task_enh.c  (在默認實現上進行 vendor hook)

KCONFIG

lib\Kconfig.debug (對應 hung_task.c )

圖片

圖片

drivers\soc\qcom\Kconfig (對應 hung_task_enh.c)

圖片

代碼分析

kernel\hung_task.c

圖片

A. 將 panic_block(notifier_block 結構體)掛到 panic_notifier_list 通知鏈,當系統發生 panic 時,會通過該通知鏈通知註冊到該鏈的所有 notifier_block,調用每個 notifier_block 的 notifier_call 成員函數。

對於這裏的 hungtask,就是在 panic 時調用 hung_task_panic 函數置 did_panic 爲 1,在 hungtask 檢測流程中發現 did_panic 爲 1,則直接退出。

圖片

B. hungtask_pm_notify_nb(notifier_block 結構體)掛到 pm_chain_head 通知鏈,當系統發生 pm 狀態變化時調用 hungtask_pm_notify,設置 hung_detector_suspended 變量。

圖片

C. 起內核線程,運行 D 狀態檢測函數 watchdog(),下面分析。

圖片

A. 取 sysctl_hung_task_timeout_secs 和 sysctl_hung_task_check_interval_secs 最小值作爲檢測時的 interval,加上次檢測時間 hung_last_checked,如達到或超過當前時間 jiffies 則進行 hungtask check。

B. hungtask check 函數,下面詳細分析。

C. 進入可中斷休眠,如有信號提前中斷喚起該線程,會在 A 處的時間判斷中確定是否進行 hungtask check。

圖片

A. 限制進行 hung check 的 task 數量,本輪檢測的 task 數量達到該值後退出。

B. 如已經運行了 HUNG_TASK_LOCK_BREAK 時間,調用 rcu_lock_break() 短暫退出 rcu 臨界區並調度出去,避免一次 rcu grace period 的時間過長,之後再調度回來時再次進入 rcu 臨界區。由於調度出去再回來時,正在檢測的 task 可能已經釋放,所以在調度出去之前,需要使用 get_task_struct 增加 task 的 task_struct 結構體的引用計數,防止其被釋放,在通過 pid_alive 判斷 task 是否 dead 後,再調用 put_task_struct 減小引用計數。如果調度回來時發現 task 已經 dead,則退出本輪 hung check。

圖片

C. 爲符合 GKI 規範,此處通過 vendor hook 函數,調用 vendor 實現的 hook 函數,這裏的實現是調用 register 函數註冊對應 hook 函數,qcom_before_check_tasks() 和 qcom_check_tasks_done(),後面會有分析,主要就是判斷該 task 是否需要 hungtask 檢查,並獲得當前 iowait task 的數量。

vendor hook 函數註冊如下所示:

drivers\soc\qcom\hung_task_enh.c

圖片

D. 根據 C 處返回的 need_check,如判斷需要進行 hungtask 檢查,則調用 check_hung_task(),後面會有分析。

E. 此處調用 qcom_check_tasks_done,判斷在對所有 task 進行 hung_task_enh.max_iowait_timeout_cnt 輪的檢測,如果連續地每輪都有大於等於 hung_task_enh.max_iowait_task_cnt 數量的 task 處於 iowait 狀態,則直接觸發 panic。

F. 之後的流程就是在本輪 hungtask 檢測結束後,跟蹤標誌狀態顯示 task 的鎖狀態及當前各 CPU 上的棧。

接下來看下 hook 函數的具體內容。

drivers\soc\qcom\hung_task_enh.c

圖片

A. 一個 task 根據其 task_struct 中的 walt_task_struct 的 hung_detect_status 成員判斷,如果在白名單(白名單模式)或不在黑名單(黑名單模式),則置 need_check 標誌,然後繼續判斷是否要增加 iowait task 數量的統計值。

B. 如果 task 處於 iowait 狀態,且爲 D 狀態、暫停狀態、跟蹤狀態之一,且到了檢查 hungtask 的時間,且爲用戶空間進程主線程,則增加 iowait task 數量的統計值。

接下來看 check_hung_task() 的具體內容。

圖片

圖片

A. 如果 task 已凍結或爲調用 vfork 的進程(會處於 D 狀態直到等子進程調用 exit 或 exec)則跳過 hungtask 檢查。

B. task 的自願(nvcsw )和非自願(nivcsw)上下文切換次數的和如果在檢測 interval 之間變動過,則說明該 task 沒有 hung 住,即使 task 當前爲 D 狀態。直接返回,跳過該 task。

C. 打印 sysctl_hung_task_warnings 次 task block 信息後就不再打印,也就是說,更多的 hungtask 信息有可能不會再被看到。打印 task block 信息時,會置 hung_task_show_lock 和 hung_task_show_all_bt 標誌,在退出本輪所有 task 的 hungtask 檢查後,會根據這些標誌打印 task 的鎖情況以及各 CPU 的 backtrace。之後就退出了本輪的所有 task 的 hungtask 檢查。

2. 只關注重要進程

這裏對第二種 hungtask detect 實現方式進行分析,只判斷系統中的重要進程是否卡住,代碼見參考文檔 2,主要涉及驅動代碼:

drivers\misc\mediatek\monitor_hang\hang_detect.c

KCONFIG 定義

drivers\misc\mediatek\monitor_hang\Kconfig (對應 hung_task.c )

圖片

代碼分析

drivers\misc\mediatek\monitor_hang\hang_detect.c

圖片

A. 註冊 hang monitor 的 misc device,名稱爲 RT_Monitor,通過其 write 接口控制 hang monitor 的使能,通過 ioctl 設置(類似 watchdog kick 操作)hang_detect_counter(後面分析的 hang_detect 線程會定時遞減這個 counter,也就是說,如果不重置,一定時間之後就會認爲重要任務 hang 住了)和 hang monitor 的使能(hd_detect_enabled)。

圖片

圖片

B. 啓動 hang_detect 和 hang_detect1 線程。hang_detect 線程爲檢測線程,下面分析。hang_detect1 用來在檢測到 hang 時 dump 系統狀態。

圖片

繼續看下 hang_detect 線程的工作。

圖片

圖片

A 處啓動循環,當 hang detect 使能,且白名單中的 task 均在系統中時,每 HD_INTER 秒(默認爲 30 秒)會對 hang_detect_counter 減一,減一前會檢查 hang_detect_counter,當小於等於 0 時,會 dump 系統狀態或觸發 BUG 死機。

當系統持續 hang 住時,hang_detect_counter 會先減到 0,這時 Hang_first_done 是 false(表示 hang 後的第一次處理還沒完成),所以會運行 wake_up_dump()(hang_detect 線程)喚醒 hang_detect1 線程,hang_detect1 線程 dump 系統狀態之後,置 dump_bt_done 爲 1,表示已經完成 dump backtrace,再喚醒 wake_up_dump()(hang_detect 線程),之後 E 處設置 Hang_first_done 爲 true,表示 hang 後的第一次處理已經完成,之後 hang_detect_counter 會減到 - 1。

若系統還持續 hang 住,會走到 B 處,此處判斷條件時的意思是,如果之前 hang_detect1 線程 dump 系統狀態沒有正常執行完成,則這裏會再啓動 hang_detect2;否則,還會再喚醒 hang_detect1 線程,再次 dump 系統狀態,方便和之前的進行對比。之後走到 D 處,調用 BUG 觸發死機重啓以復位系統,否則關鍵進程可能一直卡住而不能自動恢復。

wake_up_dump()

圖片

hang_detect1 線程

圖片

hang_detect2 線程

圖片

三、問題分析

上面介紹了兩種 hungtask 檢測的原理及方法,我們知道了不同系統可以如何判斷 task 已經 hang 住,進而觸發系統去顯示或保存現場狀態(如發現重要 task 持續 hang 住時,會多次打印 D 狀態 task 的棧信息、或系統最終因爲 hungtask 無法恢復而重啓時保存 ramdump)及異常恢復。

一般可以通過 kernel log 中的 task 棧信息打印,看到 hang 住的關鍵 task 及其對應的棧,並對比相隔一定時間的多次該 task 的棧情況,明確該 task 確實已經異常,之後就可以根據棧的情況推測及尋找線索。如果開啓了 ramdump,異常重啓時保存的 ramdump 也會對問題分析產生很大的幫助。

實際遇到的大多數問題,通過 log 中的 task 棧信息,一般只能粗略知道 task hang 住的大致現場及方向,還需要結合 log 中的其他信息、ramdump 等進行分析,可能還需要判斷問題發生場景、編譯調試版本復現問題抓取更多信息,或者排查可疑修改。

由 hungtask 原理可知,產生 hungtask 異常的直接原因是所關注的 task 長時間處於 D 狀態或無法調度運行,一般就是 task 本身有異常或系統有異常影響到了該 task。以系統異常的情況居多,常見的可能原因有內存不足、內存分配異常、UFS 器件異常、文件系統異常、spinlock 或 rwsem 等各種鎖死鎖、中斷風暴等等;task 本身的異常,可能是其本身邏輯問題,需要具體分析。針對每種原因,大都有對應的判斷及定位方法,可以輸出相應的調試版本壓測復現分析。

以下舉兩個實際的例子看下 hungtask 問題發生的現象及分析方法。

1、開關機測試時死機

在開關機測試及各項功能測試時均出現了低概率死機問題,由於故障機分散,判斷爲非硬件個體問題。查看故障機 kernel log,發現死機之前,有 hungtask 打印。下面是其中一例死機前輸出的 hungtask 情況。

圖片

hungtask 檢測輸出的 hang 住的其中幾個 task 的棧情況如下,該例中 init 進程阻塞在了 io 上。

圖片

分析多例故障現場,hang 住的 task 不一致,用 T32 分析對應的 ramdump,task 會卡在對不同文件的 io 操作;出問題時 hang 住的 task 數量均較多。

解析 ramdump 得到 cpu 上的任務隊列情況,

圖片

發現一個 swr irq(音頻功放註冊的中斷)在運行,一個 swr irq 在 pending,還有多個 cfs task 也在 pending。查看系統中斷狀態,觀察到開機的短時間內,產生了大量中斷,引起中斷風暴,可能會嚴重影響系統中其他任務的執行,包括 io 操作,由此導致比較隨機的開機時不同任務卡 io 的問題的發生。

之後去掉該音頻功放註冊的中斷(實際上這個中斷對應的音頻功放並沒有使用,中斷引腳輸入狀態不定導致隨機的中斷異常),進行開關機壓測,沒有復現問題。

2、Monkey 測試時死機

某項目發現低概率 hungtask 問題,過一段時間就會有一、兩例出現,在 Money 測試中概率有所上升,每輪中能穩定出現一到兩例。每次出現問題的 task 不一定相同。下面舉一個例子:

圖片

這裏時 surfaceflinger 出現了 hungtask 情況,其一直在嘗試獲取 kworker/u16:14 進程所持的 mutex。再查看 kworker/u16:14,

圖片

可以看到該 task 在等 rpm(處理系統中時鐘和電源請求的子系統)操作的 completion,但這裏 rpm ack 一直沒有返回。查看其它異常時現場,共性都是 rpm ack 一直沒有返回。最後分析到,雖然系統還有足夠的 H memory(HighAtomic),但 ap 和 rpm 通信用的 glink 分配內存時無法使用這種遷移類型的 memory;系統同時有許多 zs_malloc 失敗的情況,因爲 zram 功能本身在進行內存分配時,也沒有使用 H memory,從而導致 anon page 回收異常。

最後,調整內存回收參數,該問題得以優化。

四、總結

本文主要描述了什麼是 hungtask、hungtask 檢測方法以及 hungtask 產生的原因,並通過兩個案例,展示了具體問題分析方法。

hungtask 表現爲某些重要進程一直不能運行,如長期處於 uninterruptible sleep 狀態(也就是常說的 D 狀態。可以採取多種方法檢測:定時輪詢系統中的所有 task,然後判斷處於 D 狀態的 task 的上下文切換次數是否和之前輪詢時的相等,如果相等則表明該 task 兩個輪詢間隔期間一直處於 D 狀態,可以認爲該 task 有 hang 的情況;或只關注重要進程的運行情況,讓這個重要進程在規定時間內模擬餵狗操作,若發現沒有及時餵狗,則認爲其有 hang 的情況。產生 hungtask 的直接原因是所關注的 task 長時間處於 D 狀態或無法調度運行,task 本身有異常或系統有異常影響到了該 task:對於系統異常,常見的可能原因有內存不足、內存分配異常、UFS 器件異常、文件系統異常、spinlock 或 rwsem 等各種鎖死鎖、中斷風暴等等;task 本身的異常,爲其本身邏輯問題。

參考文檔

  1. https://github.com/oppo-source/android_kernel_5.10_oppo_mt6983

  2. https://github.com/OnePlusOSS/android_kernel_msm-5.10_oneplus_sm8450

本文由 Readfog 進行 AMP 轉碼,版權歸原作者所有。
來源https://mp.weixin.qq.com/s/m5mClWhpRlxCleCMEbVmLA