本文是 Systrace 執行緒 CPU 執行狀態分析技巧系列的第三篇,本文主要講了使用 Systrace 分析 CPU 狀態時遇到的 Sleep 與 Uninterruptible Sleep 狀態的原因排查方法與最佳化方法,這兩個狀態導致效能變差機率非常高,而且排查起來也比較費勁,網上也沒有系統化的文件。
Linux 中的 Sleep 狀態是什麼
TASK_INTERUPTIBLE vs TASK_UNINTERRUPTIBLE
一個執行緒的狀態不屬於 Running 或者 Runnable 的時候,那就是 Sleep 狀態了(嚴謹來說,還有其他狀態,不過對效能分析來說不常見,比如 STOP、Trace 等)。
在 Linux 中的Sleep 狀態可以細分為 3 個狀態:
- TASK_INTERUPTIBLE → 可中斷
- TASK_UNINTERRUPTIBLE → 不可中斷
- TASK_KILLABLE → 等同於 TASK_WAKEKILL | TASK_UNINTERRUPTIBLE
在 Systrace/Perfetto 中,Sleep 狀態指的是 Linux 中的TASK_INTERUPTIBLE,trace 中的顏色為白色。Uninterruptible Sleep 指的是 Linux 中的 TASK_UNINTERRUPTIBLE,trace 中的顏色為橙色。
本質上他們都是處於睡眠狀態,拿不到 CPU的時間片,只有滿足某些條件時才會拿到時間片,即變為 Runnable,隨後是 Running。
TASK_INTERRUPTIBLE 與 TASK_UNINTERRUPTIBLE 本質上都是 Sleep,區別在於前者是可以處理 Signal 而後者不能,即使是 Kill 型別的Signal。因此,除非是拿到自己等待的資源之外,沒有其他方法可以喚醒它們。 TASK_WAKEKILL 是指可以接受 Kill 型別的Signal 的TASK_UNINTERRUPTIBLE。
Android 中的 Looper、Java/Native 鎖等待都屬於 TAKS_INTERRUPTIBLE,因為他們可以被其他程序喚醒,應該說絕大部分的程式都處於 TAKS_INTERRUPTIBLE 狀態,即 Sleep 狀態。 看看 Systrace 中的一大片程序的白色狀態就知道了(trace 中表現為白色塊),它們絕大部分時間都是在 Runnning 跟 Sleep 狀態之間轉換,零星會看到幾個 Runnable 或者 UninterruptibleSleep,即藍色跟橙色。
TASK_UNINTERRUPTIBLE 作用
似乎看來 TASK_INTERUPTIBLE 就可以了,那為什麼還要有 TASK_UNINTERRUPTIBLE 狀態呢?
中斷來源有兩個,一個是硬體,另一個就是軟體。硬體中斷是外圍控制晶片直接向 CPU 傳送了中斷訊號,被 CPU 捕獲並呼叫了對應的硬體處理函式。軟體中斷,前面說的 Signal、驅動程式裡的 softirq 機制,主要用來在軟體層面觸發執行中斷處理程式,也可以用作程序間通訊機制。
一個程序可以隨時處理軟中斷或者硬體中斷,他們的執行是在當前程序的上下文上,意味著共享程序的堆疊。但是在某種情況下,程式不希望有任何打擾,它就想等待自己所等待的事情執行完成。比如與硬體驅動打交道的流程,如 IO 等待、網路操作。 這是為了保護這段邏輯不會被其他事情所干擾,避免它進入不可控的狀態
。
Linux 處理硬體排程的時候也會臨時關閉中斷控制器、排程的時候會臨時關閉搶佔功能,本質上為了 防止程式流程進入不可控的狀態
。這類狀態本身執行時間非常短,但系統出異常、執行壓力較大的時候可能會影響到效能。
https://elixir.bootlin.com/linux/latest/ident/TASK_UNINTERRUPTIBLE
可以看到核心中使用此狀態的情況,典型的有 Swap 讀資料、訊號量機制、mutex 鎖、記憶體慢路徑回收等場景。
分析時候的注意點
首先要認識到 TASK_INTERUPTIBLE、TASK_UNINTERRUPTIBLE 狀態的出現是正常的,但是如果這些這些狀態的累計佔比達到了一定程度,就要引起注意了。特別是在關鍵操作路徑上這類狀態的佔比較多的時候,需要排查原因之後做相應的最佳化。 分析問題以及做最佳化的時候需要牢牢把握兩個關鍵點,它類似於內功心法一樣:
- 原因的排查方法
- 最佳化方法論
你需要知道是什麼原因導致了這次睡眠,是主動的還是被動的?如果是主動的,透過走讀程式碼調查是否是正常的邏輯。如果是被動的,故事的源頭是什麼? 這需要你對系統有足夠多的認識,以及分析問題的經驗,你需要經常看案例以增強自己的知識。
以下把 TASK_INTERUPTIBLE 稱之為 Sleep,TASK_UNINTERRUPTIBLE稱之為 UninterruptibleSleep,目的是與 Systrac 中的用詞保持一致。
初期分析 Sleep 與 UninterruptibleSleep 狀態的經驗不足時你會感到困惑,這種困惑主要是來自於對系統的不瞭解。你需要讀大量的框架層、核心層的程式碼才能從 Trace 中找出蛛絲馬跡。目前並沒有一種 Trace 工具能把整個邏輯鏈路描述的很清楚,而且他們有時候還有不準的時候,比如 Systrace 中的 wakeup_from 資訊。只有廣泛的系統執行原理做為支援儲備,再結合 Trace 工具分析問題,才能做到準確定位問題根因。否則就是我經常說的「效能最佳化流氓」,你說什麼是什麼,別人也沒法證偽。反覆折磨測試同學複測,沒測出來之後,這個問題也就不了了之了。
本文沒辦法列舉完所有狀態的原因,因此只能列舉最為常見的型別,以及典型的實際案例。更重要的是,你需要掌握診斷方法,並結合原始碼來定位問題。
Trace 中的視覺化效果
Sleep 狀態分析
診斷方法
透過 wakeup from tid: ***
檢視喚醒執行緒
Sleep 最常見的有圖 1(UIThread 與 RenderThread 同步)的情況與圖 2(Binder 呼叫)的情況。 Sleep 狀態一般是由程式主動等待某個事件的發生而造成的,比如鎖等待,因此它有個比較明確的喚醒源。
比如圖 1,UIThread 等待的是 RenderThread,你可以透過閱讀程式碼來了解這種多執行緒之間的互動關係。雖然最直接,但是對開發者的要求非常高,因為這需要你熟讀圖形棧的程式碼。這可不是一般的難度,是追求的目標,但不具備普適性。
如果:
Wakeup @ 02:22:07.991671000 on CPU 2 by P: Thread-10 [3083] T: Thread-10 [5726]
Wakeup @
- 表示這是一個執行緒被喚醒的事件02:22:07.991671000
- 事件發生的具體時間戳,精確到納秒CPU 2
- 該事件發生在 CPU 2 核心上by P: Thread-10 [3083]
- 喚醒操作是由 PID 為 3083 的名為 "Thread-10" 的執行緒執行的T: Thread-10 [5726]
- 被喚醒的目標執行緒是 PID 為 5726 的名為 "Thread-10" 的執行緒
進入到 Runnable 有兩種方式,
- 一種是 Running 中的程式被搶佔了,暫時進入到 Runnable。
- 還有一種是由另外一個執行緒將此執行緒(處於 Sleep 的執行緒)變成了 Runnable。
需要特別注意的是 wakeupfrom
這個有時候不準,原因是跟具體的 tracepoint 型別有關。分析的時候要注意甄別,不要一味地相信這個資料是對的。
其他方法
- Simpleperf 還原始碼執行流
- 在 Systrace 尋找時間點對齊的事件
方法 1 適合用來看程式到底在執行什麼操作進入到這種狀態,是 IO 還是鎖等待?球裡連載 Simpleperf 工具的使用方法,其中「Simpleperf 分析篇 (1): 使用 Firefox Profiler 可視分析 Simpleperf 資料」介紹了可以按時間順序看函式呼叫的視覺化方法。其他使用也會陸續更新,直接搜關鍵字即可。
方法 2 是個比較笨的方法,但有時候也可以透過它找到蛛絲馬跡,不過缺點是錯誤率比較高。
耗時過長的常見原因
- Binder 操作 → 透過開啟 Binder 對應的 trace,可方便地觀察到呼叫到遠端的 Binder 執行執行緒。如果 Binder 耗時長,要分析遠端的 Binder 執行情況,是否是鎖競爭?得不到CPU 時間片?要具體問題具體分析
- Java\futex鎖競爭等待 → 最常見也是最容易引起效能問題,當負載較高時候特別容易出現,特別是在 SystemServer 程序中。這是 Binder 多執行緒並行化或搶佔公共資源導致的弊端。
- 主動等待 → 執行緒主動進入 Sleep 狀態,等待其它執行緒的喚醒,比如等待訊號量的釋放。最佳化建議:需要看程式碼邏輯分析等待是否合理,不合理就要最佳化掉。
- 等待 GPU 執行完畢 → 等 GPU 任務執行完畢,Trace 中可以看到等 GPU fence 時間。常見的原因有渲染任務過重、 GPU 能力弱、GPU 頻率低等。最佳化建議:提升 GPU 頻率、降低渲染任務複雜度,比如精簡 Shader、降低渲染解析度、降低Texture 畫質等。
UninterruptibleSleep 狀態分析
診斷方法
本質上UninterruptibleSleep 也是一種 Sleep,因此分析 Sleep 狀態時用到的方法也是通用的。不過此狀態有兩個特殊點與 Sleep 不同,因此在此特別說明。
- UninterruptibleSleep 分為 IOWait 與 Non-IOWait
- UninterruptibleSleep 有 Block reason
UninterruptibleSleep 分為 IOWait 與 Non-IOWait
IO 等待好理解,就是程式執行了 IO 操作。最簡單的,程式如果沒法從 PageCache 快取裡快速拿到資料,那就要與裝置進行 IO 操作。CPU 內部快取的訪問速度是最快的,其次是記憶體,最後是磁碟。它們之間的延遲差異是數量級差異,因此係統越是從磁碟中讀取資料,對整體效能的影響就越大。
非 IO 等待主要是指核心級別的鎖等待,或者驅動程式中人為設定的等待。Linux 核心中某些路徑是熱點區域,因此不得不拿鎖來進行保護。比如Binder 驅動,當負載大到一定程度,Binder 的內部的鎖競爭導致的效能瓶頸就會呈現出來。
Block Reason
谷歌的 Riley Andrews(riandrews@google.com) 15年左右往核心裡提交了一個 tracepoint 補丁,用於記錄當發生 UninterruptibleSleep 的時候是否是 IO 等待、呼叫函式等資訊。Systrace 中的展示的 IOWait 與 BlockReason,就是透過解析這條 tracepoint 而來的。這條程式碼提交的介紹如下(由於這筆提交未合入到 Linux 上游主線,因此要注意你用的核心是否單獨帶了此補丁):
sched: add sched blocked tracepoint which dumps out context of sleep.
Decare war on uninterruptible sleep. Add a tracepoint which
walks the kernel stack and dumps the first non-scheduler function
called before the scheduler is invoked.
Change-Id: [I19e965d5206329360a92cbfe2afcc8c30f65c229](https://android-review.googlesource.com/#/q/I19e965d5206329360a92cbfe2afcc8c30f65c229)
Signed-off-by: Riley Andrews [riandrews@google.com](mailto:riandrews@google.com)
在 ftrace(Systrace 使用的資料抓取機制) 中的被記錄為
sched_blocked_reason: pid=30235 iowait=0 caller=get_user_pages_fast+0x34/0x70
這句話被 Systrace 視覺化的效果為:
主執行緒中有一段 Uninterruptible Sleep 狀態,它的 BlockReason 是 get_user_pages_fast
。它是一個 Linux 核心中函式的名字,代表著是執行緒是被它切換到了 UninterruptibleSleep 狀態。為了檢視具體的原因,需要檢視這個函式的具體實現。
/**
* get_user_pages_fast() - pin user pages in memory
* @start: starting user address
* @nr_pages: number of pages from start to pin
* @gup_flags: flags modifying pin behaviour
* @pages: array that receives pointers to the pages pinned.
* Should be at least nr_pages long.
*
* Attempt to pin user pages in memory without taking mm->mmap_lock.
* If not successful, it will fall back to taking the lock and
* calling get_user_pages().
*
* Returns number of pages pinned. This may be fewer than the number requested.
* If nr_pages is 0 or negative, returns 0. If no pages were pinned, returns
* -errno.
*/
int get_user_pages_fast(unsigned long start, int nr_pages,
unsigned int gup_flags, struct page **pages)
{
if (!is_valid_gup_flags(gup_flags))
return -EINVAL;
/*
* The caller may or may not have explicitly set FOLL_GET; either way is
* OK. However, internally (within mm/gup.c), gup fast variants must set
* FOLL_GET, because gup fast is always a "pin with a +1 page refcount"
* request.
*/
gup_flags |= FOLL_GET;
return internal_get_user_pages_fast(start, nr_pages, gup_flags, pages);
}
EXPORT_SYMBOL_GPL(get_user_pages_fast);
從函式解釋上可以看到,函式首先是透過無鎖的方式pin 應用側的 pages,如果失敗的時候不得不嘗試持鎖後走慢速執行路徑。此時,無法持鎖的時候那就要等待了,直到先前持鎖的人釋放鎖。那之前被誰持有了呢?這時候可以利用之前介紹的Sleep 診斷方法,如下圖。
UninterruptibleSleep 狀態相比 Sleep 有點複雜,因為它涉及到 Linux 內部的實現。可能是核心本身的機制有問題,也有可能是應用層使用不對,因此要聯合上層的行為綜合診斷才行。畢竟核心也不是萬能的,它也有自己的能力邊界,當應用層的使用超過其邊界的時候,就會出現影響效能的現象。
IOWait 常見原因與最佳化方法
1. 主動IO 操作
- 程式進行頻繁、大量的讀或者寫 IO 操作,這是最常見的情況。
- 多個應用同時下發 IO 操作,導致器件的壓力較大。同時執行的程式多的時候 IO 負載高的可能性也大。
- 器件本身的 IO 效能較差,可透過 IO Benchmark 來進行排查。 常見的原因有磁碟碎片化、器件老化、剩餘空間較少(越是低端機越明顯)、讀放大、寫放大等等。
- 檔案系統特性,比如有些檔案系統的內部操作會表現為 IO 等待。
- 開啟 Swap 機制的核心下,資料從 Swap 中讀取。
最佳化方法
- 調優 Readahead 機制
- 指定檔案到 PageCache,即 PinFile 機制
- 調整 PageCache 回收策略
- 調優清理垃圾檔案策略
2. 低記憶體導致的 IO 變多
記憶體是個非常有意思的東西,由於它的速度比磁碟快,因此 OS 設計者們把記憶體當做磁碟的快取,透過它來避免了部分IO操作的請求,非常有效的提升了整體 IO 效能。有兩個極端情況,當系統記憶體特別大的時候,絕大部分操作都可以在記憶體中執行,此時整體 IO 效能會非常好。當系統記憶體特別低,以至於沒辦法快取 IO 資料的時候,幾乎所有的 IO 操作都直接與器件打交道,這時候整體效能相比記憶體多的時候而言是非常差的。
所以系統中的記憶體較少的時候 IO 等待的機率也會變高。所以,這個問題就變成了如何讓系統中有足夠多的記憶體?如何調節磁碟快取的淘汰演算法?
最佳化方法
- 關鍵路徑上減少 IO 操作
- 透過Readahead 機制讀資料
- 將熱點資料儘量聚集在一起,使被 Readahead 機制命中的機率高
- 最後一個老生常談的,減少大量的記憶體分配、記憶體浪費等操作
系統中的記憶體是被各個程序所共用。當app 只考慮自己,肆無忌憚的使用計算資源,必然會影響到其他程式。這時候系統還是會回來壓制你,到頭來虧損的還是自己。 不過能想到這一步的開發者比較少,也不現實。明文化的執行系統約定,可能是個終極解決方案。
Non-IOWait 常見原因
- 低記憶體導致等待 → 低記憶體的時候要回收其他程式或者快取上的記憶體。
- Binder 等待 → 有大量 Binder 操作的時候出現機率較高。
- 各種各樣的核心鎖,不勝列舉。結合「診斷方法」來分析。
系統排程與 UninterruptibleSleep 耦合的問題
當執行緒處於 UninterruptibleSleep 非 IO等待狀態(即核心鎖),而持有該鎖的其他執行緒因 CPU 排程原因,較長時間處於 Runnable 狀態。這時候就出現了有意思的現象,即使被等待的執行緒處於高優先順序,它的依賴方沒有被排程器及時的識別到,即使是非常短的鎖持有,也會出現較長時間的等待。
規避或者徹底解決這類問題都是件比較難的事情,不同廠家實現了不同的解決方案,也是比較考慮廠家技術能力的一個問題。
附錄
Linux 執行緒狀態釋義
執行緒狀態 | 描述 |
---|---|
S | SLEEPING |
R、R+ | RUNNABLE |
D | UNINTR_SLEEP |
T | STOPPED |
t | DEBUG |
Z | ZOMBIE |
X | EXIT_DEAD |
x | TASK_DEAD |
K | WAKE_KILL |
W | WAKING |
D | K |
D | W |
案例: 從 Swap 讀取資料時的等待
案例: 同程序的多個執行緒進行 mmap
共享同一個 mm_struct 的執行緒同時執行 mmap() 系統呼叫進行 vma 分配時發生鎖競爭。
mmap_write_lock_killable() 與 mmap_write_unlock() 包起來的區域就是由鎖受保護的區域。
unsigned long vm_mmap_pgoff(struct file *file, unsigned long addr,
unsigned long len, unsigned long prot,
unsigned long flag, unsigned long pgoff)
{
unsigned long ret;
struct mm_struct *mm = current->mm;
unsigned long populate;
LIST_HEAD(uf);
ret = security_mmap_file(file, prot, flag);
if (!ret) {
if (mmap_write_lock_killable(mm))
return -EINTR;
ret = do_mmap(file, addr, len, prot, flag, pgoff, &populate,
&uf);
mmap_write_unlock(mm);
userfaultfd_unmap_complete(mm, &uf);
if (populate)
mm_populate(ret, populate);
}
return ret;
}