文章對(duì) Linux 系統(tǒng)下進(jìn)程的幾種狀態(tài)進(jìn)行介紹,并對(duì)系統(tǒng)出現(xiàn)大量僵尸進(jìn)程和不可中斷進(jìn)程的場(chǎng)景進(jìn)行分析,使用常用的幾種工具進(jìn)行問題分析定位。
1、進(jìn)程狀態(tài)
top 和 ps 是最常用的查看進(jìn)程狀態(tài)的工具,下面是 top 命令輸出的示例,S 列(也就是 Status 列)表示進(jìn)程的狀態(tài)。
上面數(shù)據(jù)的 S 列可以看到 R、D、S、I 、Z 幾個(gè)狀態(tài),下面對(duì)進(jìn)程的這幾種狀態(tài)進(jìn)行介紹。
R 狀態(tài):R 是 Running 或 Runnable 的縮寫,表示進(jìn)程在 CPU 的就緒隊(duì)列中,正在運(yùn)行或者正在等待運(yùn)行。
D 狀態(tài):D 是 Disk Sleep 的縮寫,也就是不可中斷狀態(tài)睡眠(Uninterruptible Sleep),一般表示進(jìn)程正在跟硬件交互,并且交互過程不允許被其他進(jìn)程或中斷打斷。
Z 狀態(tài):Z 是 Zombie 的縮寫,它表示僵尸進(jìn)程,也就是進(jìn)程實(shí)際上已經(jīng)結(jié)束了,但是父進(jìn)程還沒有回收它的資源(比如進(jìn)程的描述符、PID 等)。
S 狀態(tài):S 是 Interruptible Sleep 的縮寫,也就是可中斷狀態(tài)睡眠,表示進(jìn)程因?yàn)榈却硞€(gè)事件而被系統(tǒng)掛起。當(dāng)進(jìn)程等待的事件發(fā)生時(shí),它會(huì)被喚醒并進(jìn)入 R 狀態(tài)。
I 狀態(tài):I 是 Idle 的縮寫,也就是空閑狀態(tài),用在不可中斷睡眠的內(nèi)核線程上。硬件交互導(dǎo)致的不可中斷進(jìn)程用 D 表示,但對(duì)某些內(nèi)核線程來說,它們有可能實(shí)際上并沒有任何負(fù)載,用 Idle 正是為了區(qū)分這種情況。要注意,D 狀態(tài)的進(jìn)程會(huì)導(dǎo)致平均負(fù)載升高, I 狀態(tài)的進(jìn)程卻不會(huì)。
進(jìn)程的狀態(tài)除了上面的狀態(tài)之外,還有 T 和 t 狀態(tài),這兩種狀態(tài)都表示進(jìn)程處于停止?fàn)顟B(tài),但使得進(jìn)程停止的原因有所差異,如下所示。
T 狀態(tài):由信號(hào)觸發(fā)的停止?fàn)顟B(tài),比如向一個(gè)進(jìn)程發(fā)送 SIGSTOP 信號(hào),它就會(huì)因響應(yīng)這個(gè)信號(hào)變成暫停狀態(tài)(Stopped);再向它發(fā)送 SIGCONT 信號(hào),進(jìn)程又會(huì)恢復(fù)運(yùn)行(如果進(jìn)程是終端里直接啟動(dòng)的,則需要你用 fg 命令,恢復(fù)到前臺(tái)運(yùn)行)。
t 狀態(tài):由調(diào)試跟蹤觸發(fā)的停止?fàn)顟B(tài),當(dāng)使用調(diào)試器(如 gdb)調(diào)試一個(gè)進(jìn)程時(shí),在使用斷點(diǎn)中斷進(jìn)程后,進(jìn)程就會(huì)變成跟蹤狀態(tài),這其實(shí)也是一種特殊的暫停狀態(tài),只不過可以用調(diào)試器來跟蹤并按需要控制進(jìn)程的運(yùn)行。
2、案例分析
本案例模擬多進(jìn)程對(duì)磁盤進(jìn)行讀取,使用相關(guān)的性能分析工具對(duì)例程出現(xiàn)的性能問題進(jìn)行分析,案例源碼在文章后面,編譯出的可執(zhí)行程序名稱為 app。
2.1 異常信息分析
在 Ubuntu 系統(tǒng)中打開一個(gè)終端,執(zhí)行 app 進(jìn)程,打開第二個(gè)終端,使用 top 命令查看進(jìn)程運(yùn)行狀態(tài),如下所示。
對(duì)上面圖片中的信息進(jìn)行分析,找出系統(tǒng)可能存在的異常情況。
第一行中過去 15 分鐘、5 分鐘、1 分鐘的平均負(fù)載依次升高,說明平均負(fù)載正在升高,過去 1 分鐘的平均負(fù)載已經(jīng)快接近 CPU 的個(gè)數(shù),說明系統(tǒng)可能已經(jīng)出現(xiàn)了性能瓶頸。
第二行中存在較多的僵尸進(jìn)程,說明子進(jìn)程退出時(shí)沒有被回收。
查看 CPU 信息,用戶 CPU 使用率較低,系統(tǒng) CPU 使用率和 iowait 有所上升。
查看每個(gè)進(jìn)程的信息,當(dāng)前運(yùn)行 app 進(jìn)程的 CPU 使用率為 20% 左右,且這兩個(gè)進(jìn)程的狀態(tài)為 D 狀態(tài),說明該進(jìn)程有可能正在等待 I/O。
對(duì)以上分析進(jìn)行總結(jié),得出以下兩點(diǎn)結(jié)論。
系統(tǒng)中僵尸進(jìn)程在不斷增加,說明程序沒有正確回收子進(jìn)程的資源。
系統(tǒng)平均負(fù)載在升高,CPU 使用率和 iowait 有所上升,說明平均負(fù)載的升高可能是 iowait 引起的,iowait 與系統(tǒng)調(diào)用有關(guān)。
2.2 iowait 分析
根據(jù)上面得出的結(jié)論,系統(tǒng)平均負(fù)載的上升可能是由于 iowait 引起的,這里推薦一個(gè)查看系統(tǒng) I/O 情況的工具,dstat 可以同時(shí)查看 CPU 和 I/O 這兩種資源的使用情況,便于對(duì)比分析,以 1 秒為間隔,連續(xù)輸出10組數(shù)據(jù),如下所示。
從 dstat 的輸出可以看到,當(dāng) iowait(wai)高時(shí),磁盤的讀請(qǐng)求(read)都會(huì)很大。這說明 iowait 的升高跟磁盤的讀請(qǐng)求有關(guān),很可能就是磁盤讀導(dǎo)致的。
接下來,需要找出讀取磁盤的進(jìn)程,上面 top 的輸出中,進(jìn)程 id 為 6758 和 6759 的進(jìn)程處于 D 狀態(tài),使用 pidstat 查看這兩個(gè)進(jìn)程的情況,-d 參數(shù)可以顯示 I/O 的使用情況,如下所示。
上圖進(jìn)程 ID 為 6758 和 6759 的進(jìn)程 kB_rd/s 數(shù)據(jù)值為 0,說明不是這兩個(gè)進(jìn)程在讀取磁盤數(shù)據(jù),繼續(xù)使用 pidstat 查看所有進(jìn)程的 I/O 使用情況,如下所示(以 1 秒為間隔,輸出 3 組數(shù)據(jù))。
上圖中 app 進(jìn)程的 kB_rd/s 的值較大,最大達(dá)到 211MB/s,說明 iowait 升高就是 app 進(jìn)程在讀取數(shù)據(jù)導(dǎo)致的。進(jìn)程分為用戶態(tài)和內(nèi)核態(tài),進(jìn)程想要訪問磁盤,就必須使用系統(tǒng)調(diào)用,這也是通過 top 查看時(shí),系統(tǒng) CPU 和 iowait 一起升高的原因,接下來的重點(diǎn)就是找出 app 進(jìn)程的系統(tǒng)調(diào)用。
strace 工具可以查看進(jìn)程的系統(tǒng)調(diào)用棧,指定 pid 查看進(jìn)程的系統(tǒng)調(diào)用棧,如下所示。
上圖操作顯示無權(quán)限,但是當(dāng)前終端權(quán)限已經(jīng)為 root,檢查一下進(jìn)程的狀態(tài)是否正常,如下所示。
根據(jù)圖片信息可以看到,當(dāng)前進(jìn)程 ID 為 6945 的 app 進(jìn)程狀態(tài)為僵尸狀態(tài),僵尸進(jìn)程是已經(jīng)退出的進(jìn)程,所以就沒法兒繼續(xù)分析它的系統(tǒng)調(diào)用。
之前提到的 perf 工具,也可以查看進(jìn)程的調(diào)用關(guān)系,使用"perf record -g"命令記錄調(diào)用關(guān)系,等待 10 秒左右,使用"perf report"命令報(bào)告調(diào)用關(guān)系,展開 app 進(jìn)程的調(diào)用關(guān)系,如下所示。
app 通過系統(tǒng)調(diào)用 sys_read() 讀取數(shù)據(jù),從 new_sync_read 和 blkdev_direct_IO 可以看出,進(jìn)程正在對(duì)磁盤進(jìn)行直接讀,也就是繞過了系統(tǒng)緩存,每個(gè)讀請(qǐng)求都會(huì)從磁盤直接讀,這其實(shí)就是導(dǎo)致 iowait 升高的原因,查看 app.c 中磁盤的打開方式,如下所示。
直接讀寫磁盤,是直接控制磁盤的讀寫,不借助系統(tǒng)緩存的優(yōu)化進(jìn)行操作,這會(huì)導(dǎo)致讀寫性能受到 I/O 瓶頸的限制,一般直接讀寫只應(yīng)用于對(duì) I/O 敏感型的場(chǎng)景,比如數(shù)據(jù)庫系統(tǒng)。但在大部分情況下,最好還是通過系統(tǒng)緩存來優(yōu)化磁盤 I/O,在例程中刪除 O_DIRECT 這個(gè)選項(xiàng)。
修改后啟動(dòng) app 進(jìn)程,然后使用 top 工具查看系統(tǒng)信息,如下所示,當(dāng)前平均負(fù)載、CPU 使用率和 iowait 已經(jīng)非常低了,說明剛才的改動(dòng)已經(jīng)成功修復(fù)了 iowait 高導(dǎo)致系統(tǒng)平均負(fù)載升高的問題。
2.3 僵尸進(jìn)程問題分析
僵尸進(jìn)程是因?yàn)楦高M(jìn)程沒有回收子進(jìn)程的資源造成的,因此,要解決僵尸進(jìn)程的問題,就要找出父進(jìn)程,然后在父進(jìn)程里等待回收子進(jìn)程,可以通過 pstree 找出子進(jìn)程對(duì)應(yīng)的父進(jìn)程,如下所示(-a 表示顯示命令行選項(xiàng),p表PID,s表示指定進(jìn)程的父進(jìn)程)。
通過上面圖片看到,進(jìn)程 id 為 3574 進(jìn)程的父進(jìn)程是 3571,還是 app 進(jìn)程,接下來查看 app 應(yīng)用程序的代碼,分析子進(jìn)程結(jié)束的處理是否正確,比如有沒有調(diào)用 wait() 或 waitpid() ,抑或是,有沒有注冊(cè) SIGCHLD 信號(hào)的處理函數(shù)。app 應(yīng)用程序?qū)ψ舆M(jìn)程的創(chuàng)建和清理代碼如下。
這段代碼雖然看起來調(diào)用了 wait() 函數(shù)等待子進(jìn)程結(jié)束,但卻錯(cuò)誤地把 wait() 放到了 for 死循環(huán)的外面,wait() 函數(shù)實(shí)際上并沒被調(diào)用到,把它挪到 for 循環(huán)的里面就可以了。修改后,可以使用 top 命令檢查一下,發(fā)現(xiàn)系統(tǒng)的僵尸進(jìn)程數(shù)量一直為 0。
2.4 例程源碼
評(píng)論