Linux系統R、S、D、T、Z集中程式狀態的解析

天府雲創發表於2018-10-23

Linux的程式存在多種狀態,如TASK_RUNNING的執行態、EXIT_DEAD的停止態和TASK_INTERRUPTIBLE的接收訊號的等待狀態等等(可在include/linux/sched.h中檢視)。

linux top程式狀態D

什麼是D狀態

執行在KVM虛擬機器裡的一些程式突然出了問題,這些出了問題的程式無法用kill殺掉,使用ps可以看到這些程式處於D狀態:

[build@build-shengwei ~]$ ps -a -o pid,ppid,stat,command
 PID  PPID STAT COMMAND
17009     1 Ds   -bash
17065     1 D    ls --color=tty -al
17577     1 D    /usr/java/jdk1.8.0_17/bin/java -Xmx512m -classpath /usr/local/a
17629     1 D    /usr/java/jdk1.8.0_17/bin/java -Xmx512m -classpath /usr/local/a

ps 的手冊裡說D狀態是uninterruptible sleep.

Linux程式有兩種睡眠狀態:

  1. 一種是interruptible sleep,處在這種睡眠狀態的程式是可以通過給它發訊號來喚醒的,比如發HUP訊號給nginx的master程式可以讓nginx重新載入配置檔案而不需要重新啟動nginx程式;
  2. 另外一種睡眠狀態是uninterruptible sleep,處在這種狀態的程式不接受外來的任何訊號,這也是為什麼之前我無法用kill殺掉這些處於D狀態的程式,無論是”kill”, “kill -9″還是”kill -15″,因為它們壓根兒就不受這些訊號的支配。

下面解釋集中不同的程式狀態

R (TASK_RUNNING),可執行狀態。

只有在該狀態的程式才可能在CPU上執行。同一時刻可能有多個程式處於可執行狀態,這些程式的task_struct結構(程式控制塊)被放入對應CPU的可執行佇列中(一個程式最多隻能出現在一個CPU的可執行佇列中)。程式排程器從各個CPU的可執行佇列中分別選擇一個程式在該CPU上執行。

正在CPU上執行的程式定義為RUNNING狀態、可執行但尚未被排程執行的程式定義為READY狀態,這兩種狀態統一為 TASK_RUNNING狀態。

S (TASK_INTERRUPTIBLE),可中斷的睡眠狀態。

處於這個狀態的程式,因為等待某某事件的發生(比如等待socket連線、等待訊號量),而被掛起。這些程式的task_struct結構被放入對應事件的等待佇列中。當這些事件發生時(由外部中斷觸發、或由其他程式觸發),對應的等待佇列中的一個或多個程式將被喚醒。

程式列表中的絕大多數程式都處於TASK_INTERRUPTIBLE狀態。CPU就這麼一兩個,程式動輒幾十上百個,如果不是絕大多數程式都在睡眠,CPU將會響應不過來。

D (TASK_UNINTERRUPTIBLE),不可中斷的睡眠狀態。

程式處於睡眠狀態,但是此刻程式是不可中斷的。不可中斷,指的並不是CPU不響應外部硬體的中斷,而是指程式不響應非同步訊號。絕大多數情況下,程式處在睡眠狀態時,總是應該能夠響應非同步訊號的。

而TASK_UNINTERRUPTIBLE狀態存在的意義在於,程式對某些硬體進行操作時(比如程式呼叫read系統呼叫對某個裝置檔案進行讀操作,而read系統呼叫最終執行到對應裝置驅動的程式碼,並與對應的物理裝置進行互動),可能需要使用TASK_UNINTERRUPTIBLE狀態對程式進行保護,以避免程式與裝置互動的過程被打斷,造成裝置陷入不可控的狀態。這種情況下的TASK_UNINTERRUPTIBLE狀態總是非常短暫的,通過ps命令基本上不可能捕捉到。

linux系統中也存在容易捕捉的TASK_UNINTERRUPTIBLE狀態。執行vfork系統呼叫後,父程式將進入TASK_UNINTERRUPTIBLE狀態,直到子程式呼叫exit或exec。通過下面的程式碼就能得到處於TASK_UNINTERRUPTIBLE狀態的程式:

#include <stdio.h>
        #include <unistd.h>
        void main() 
        {
                 if (!vfork());
                 sleep(100);
                 ruturn 0;
        }

編譯執行,然後ps一下:

njs@njs:~/test$ ps -ax | grep a\.out 
        4371 pts/0 D+ 0:00 ./a.out 
        4372 pts/0 S+ 0:00 ./a.out 
        4374 pts/1 S+ 0:00 grep a.out

然後我們可以試驗一下TASK_UNINTERRUPTIBLE狀態的威力。不管kill還是kill -9,這個TASK_UNINTERRUPTIBLE狀態的父程式依然屹立不倒。

T (TASK_STOPPED or TASK_TRACED),暫停狀態或跟蹤狀態。

向程式傳送一個SIGSTOP訊號,它就會因響應該訊號而進入TASK_STOPPED狀態(除非該程式本身處於TASK_UNINTERRUPTIBLE狀態而不響應訊號)。(SIGSTOP與SIGKILL訊號一樣,是非常強制的。不允許使用者程式通過signal系列的系統呼叫重新設定對應的訊號處理函式。)向程式傳送一個SIGCONT訊號,可以讓其從TASK_STOPPED狀態恢復到TASK_RUNNING狀態。

Z (TASK_DEAD - EXIT_ZOMBIE),退出狀態,程式成為殭屍程式。

程式在退出的過程中,處於TASK_DEAD狀態。

在這個退出過程中,程式佔有的所有資源將被回收,除了task_struct結構(以及少數資源)以外。於是程式就只剩下task_struct這麼個空殼,故稱為殭屍。之所以保留task_struct,是因為task_struct裡面儲存了程式的退出碼、以及一些統計資訊。而其父程式很可能會關心這些資訊。釋放掉task_struct,則需要建立一些新的資料結構,以便讓父程式找到它的子程式的退出資訊。

父程式可以通過wait系列的系統呼叫(如wait4、waitid)來等待某個或某些子程式的退出,並獲取它的退出資訊。然後wait系列的系統呼叫會順便將子程式的屍體(task_struct)也釋放掉。子程式在退出的過程中,核心會給其父程式傳送一個訊號,通知父程式來“收屍”。這個訊號預設是SIGCHLD,但是在通過clone系統呼叫建立子程式時,可以設定這個訊號。

通過下面的程式碼能夠製造一個EXIT_ZOMBIE狀態的程式:

#include <stdio.h>
        #include <unistd.h>
        void main() 
        {
                 if (fork());
                 while(1)
                 sleep(100);
        }

編譯執行,然後ps一下:

njs@njs:~/test$ ps -ax | grep a\.out 
        10410 pts/0 S+ 0:00 ./a.out 
        10411 pts/0 Z+ 0:00 [a.out] 
        10413 pts/1 S+ 0:00 grep a.out

只要父程式不退出,這個殭屍狀態的子程式就一直存在。那麼如果父程式退出了呢,誰又來給子程式“收屍”?當程式退出的時候,會將它的所有子程式都託管給別的程式(使之成為別的程式的子程式)。託管給誰呢?可能是退出程式所在程式組的下一個程式(如果存在的話),或者是1號程式。所以每個程式、每時每刻都有父程式存在。除非它是1號程式。

1號程式,pid為1的程式,又稱init程式。linux系統啟動後,第一個被建立的使用者態程式就是init程式。它有兩項使命:1、執行系統初始化指令碼,建立一系列的程式(它們都是init程式的子孫);2、在一個死迴圈中等待其子程式的退出事件,並呼叫waitid系統呼叫來完成“收屍”工作;init程式不會被暫停、也不會被殺死(這是由核心來保證的)。它在等待子程式退出的過程中處於TASK_INTERRUPTIBLE狀態,“收屍”過程中則處於TASK_RUNNING狀態。

關於ZOMBIE程式:

這些程式已經死亡,但沒有釋放系統資源,包括記憶體和一些一些系統表等,如果這樣的程式很多,會引發系統問題。用ps -el看出的程式狀態如果是Z,就是殭屍程式。
ps -ef|grep defunc可以找出殭屍程式.
有些ZOMBIE程式時用kill -9也不能殺死,而且消耗了很多系統資源不能釋放,如果系統在shutdown時發出資訊:some process wouldn’t die. 這就意味這有些程式不能被reboot發出的kill –9殺掉,這些很可能就是殭屍程式。

可以用ps 的 – l 選項,得到更詳細的程式資訊. 
F(Flag):一系列數字的和,表示程式的當前狀態。這些數字的含義為: 
00:若單獨顯示,表示此程式已被終止。 
01:程式是核心程式的一部分,常駐於系統主存。如:    sched、 vhand 、bdflush 等。 
02:Parent is tracing process. 
04 :Tracing parent's signal has stopped the process; the parent is waiting ( ptrace(S)). 
10:程式在優先順序低於或等於25時,進入休眠狀態,而且不能用訊號喚醒,例如在等待一個inode被建立時    
20:程式被裝入主存(primary memory) 
40:程式被鎖在主存,在事務完成前不能被置換   e 
S(state of the process ) 
O:程式正在處理器執行  
S:休眠狀態(sleeping) 
R:等待執行(runable)    
I:空閒狀態(idle) 
Z:殭屍狀態(zombie)    
T:跟蹤狀態(Traced) 
B:程式正在等待更多的記憶體頁 
C(cpu usage):cpu利用率的估算值

清除ZOMBIE(殭屍)程式可以使用如下方法:
1> kill –18 PPID (PPID是其父程式)
這個訊號是告訴父程式,該子程式已經死亡了,請收回分配給他的資源。
2>如果不行則看能否終止其父程式(如果其父程式不需要的話)。先看其父程式又無其他子程式,如果有,可能需要先kill其他子程式,也就是兄弟程式。方法是:
kill –15 PID1 PID2(PID1,PID2是殭屍程式的父程式的其它子程式)。
然後再kill父程式:kill –15 PPID

這樣殭屍程式就可能被完全殺掉了。

程式為什麼會被置於uninterruptible sleep狀態呢?

處於uninterruptible sleep狀態的程式通常是在等待IO,比如磁碟IO,網路IO,其他外設IO,如果程式正在等待的IO在較長的時間內都沒有響應,那麼就很會不幸地被 ps看到了,同時也就意味著很有可能有IO出了問題,可能是外設本身出了故障,也可能是比如掛載的遠端檔案系統已經不可訪問了,我這裡遇到的問題就是由 down掉的NFS伺服器引起的。

正是因為得不到IO的相應,程式才進入了uninterruptible sleep狀態,所以要想使程式從uninterruptible sleep狀態恢復,就得使程式等待的IO恢復,比如如果是因為從遠端掛載的NFS卷不可訪問導致程式進入uninterruptible sleep狀態的,那麼可以通過恢復該NFS卷的連線來使程式的IO請求得到滿足,除此之外,要想幹掉處在D狀態程式就只能重啟整個Linux系統了。

看到有人說如果要想殺掉D狀態的程式,通常可以去殺掉它的父程式(通常是shell,我理解的這種情況是在shell下直接執行的該程式,之後該程式轉入了D狀態),於是我就照做了,之後就出現了上面的狀態:他們的父程式被殺掉了,但是他們的父程式PID都變成了1,也就是init程式,這下可如何是好?此時我這些D狀態的程式已經影響到其他一些程式的執行,而已經無法訪問的NFS卷又在段時間內無法恢復,那麼,只好重新啟動了,root不是玉皇大帝,也有無奈的時候。

跟czhang說起這個事,覺得Linux如果有這麼一個專用的垃圾回收程式就好了:系統自動或者使用者手動把殭屍程式,和比如之前我遇到的D狀態程式的PPID設為這個垃圾回收程式,那麼通過幹掉這個垃圾回收程式來清理這些殭屍們,這樣該有多美好…

長期生活在 Linux 環境裡,漸漸地就有一種環保意識油然而生。比如,我們會在登入提示裡寫上“悟空,我跟你說過叫你不要亂扔東西,亂扔東西是不對的。哎呀我話沒說完你怎麼把棍子扔掉了?月光寶盒是寶物,亂扔它會汙染環境,要是砸到小朋友怎麼辦?就算砸不到小朋友,砸到了花花草草也不好嘛...”;在使用者預設目錄裡放一個題為 “自覺保護環境 請勿堆放垃圾”的空檔案,並用 chattr +i 設為不可修改;看到垃圾檔案就立即掃入 /tmp 目錄,然後發廣播通知垃圾製造者自己去 /tmp 認領,且警告其下不為例...我們深知,系統環境的整潔有利於系統管理員保持良好的心情、清晰的思路和穩定的工作狀態。

有一類垃圾卻並非這麼容易打掃,那就是我們常見的狀態為D (Uninterruptible sleep),以及狀態為 Z (Zombie)的垃圾程式。這些垃圾程式要麼是求而不得,像怨婦一般等待資源(D),要麼是僵而不死,像冤魂一樣等待超度(Z),它們在 CPU run_queue 裡滯留不去,把Load Average 弄的老高老高,沒看過我前一篇blog的國際友人還以為這兒民怨沸騰又出了什麼大事呢。怎麼辦?開槍!kill -9!看你們走是不走。但這兩種垃圾程式偏偏是刀槍不入的,不管換哪種槍法都殺不掉它們。無奈,只好reboot,像剿滅禽流感那樣不分青紅皁白地一律撲殺!

悟空,我們所運維的可是24*7全天候對外部客戶服務的系統,怎麼能動不動就 reboot ?我們的考核指標可是4個9(99.99%,全年計劃外當機時間不得超過52分鐘34秒),又不是4個8,你稍微遇到點事就reboot,還要不要可用性了?再說,現在社會都開始奔和諧去了,我們對於 D 和 Z 這兩種垃圾程式,也該儘可能採取慈悲手段,能解決其困難的,就創造條件,解決其實際困難,能消除其冤結的,就誦經燒紙,消除其前世冤結,具體問題應具體分析具體解決,濫殺無辜只會導致冤冤相報因果迴圈...

貧僧還是回來說正題。怨婦 D,往往是由於 I/O 資源得不到滿足,而引發等待,在核心原始碼 fs/proc/array.c 裡,其文字定義為“ "D (disk sleep)", /* 2 */ ”(由此可知 D 原是Disk的打頭字母),對應著 include/linux/sched.h 裡的“ #define TASK_UNINTERRUPTIBLE 2 ”

舉個例子,當 NFS 服務端關閉之時,若未事先 umount 相關目錄,在 NFS 客戶端執行 df 就會掛住整個登入會話,按Ctrl+C 、Ctrl+Z都無濟於事。斷開連線再登入,執行 ps axf 則看到剛才的 df 程式狀態位已變成了 D ,kill -9 無法殺滅。正確的處理方式,是馬上恢復 NFS 服務端,再度提供服務,剛才掛起的 df 程式發現了其苦苦等待的資源,便完成任務,自動消亡。若 NFS 服務端無法恢復服務,在 reboot 之前也應將 /etc/mtab 裡的相關 NFS mount 項刪除,以免 reboot 過程例行呼叫 netfs stop 時再次發生等待資源,導致系統重啟過程掛起。

冤魂 Z 之所以殺不死,是因為它已經死了,否則怎麼叫 Zombie(殭屍)呢?冤魂不散,自然是生前有結未解之故。在UNIX/Linux中,每個程式都有一個父程式,程式號叫PID(Process ID),相應地,父程式號就叫PPID(Parent PID)。當程式死亡時,它會自動關閉已開啟的檔案,捨棄已佔用的記憶體、交換空間等等系統資源,然後向其父程式返回一個退出狀態值,報告死訊。如果程式有 bug,就會在這最後一步出問題。兒子說我死了,老子卻沒聽見,沒有及時收棺入殮,兒子便成了殭屍。在UNIX/Linux中消滅殭屍的手段比較殘忍,執行 ps axjf 找出殭屍程式的父程式號(PPID,第一列),先殺其父,然後再由程式天子 init(其PID為1,PPID為0)來一起收拾父子殭屍,超度亡魂,往生極樂。注意,子程式變成殭屍只是礙眼而已,並不礙事,如果殭屍的父程式當前有要務在身,則千萬不可貿然殺之。

關於ZOMBIE程式

這些程式已經死亡,但沒有釋放系統資源,包括記憶體和一些一些系統表等,如果這樣的程式很多,會引發系統問題。用ps -el看出的程式狀態如果是Z,就是殭屍程式。
ps -ef|grep defunc可以找出殭屍程式.
有些ZOMBIE程式時用kill -9也不能殺死,而且消耗了很多系統資源不能釋放,如果系統在shutdown時發出資訊:some process wouldn’t die. 這就意味這有些程式不能被reboot發出的kill –9殺掉,這些很可能就是殭屍程式。
可以用ps 的 – l 選項,得到更詳細的程式資訊.
F(Flag):一系列數字的和,表示程式的當前狀態。這些數字的含義為:

00:若單獨顯示,表示此程式已被終止。 
01:程式是核心程式的一部分,常駐於系統主存。如:    sched、 vhand 、bdflush 等。 
02:Parent is tracing process. 
04 :Tracing parent's signal has stopped the process; the parent is waiting ( ptrace(S)). 
10:程式在優先順序低於或等於25時,進入休眠狀態,而且不能用訊號喚醒,例如在等待一個inode被建立時    
20:程式被裝入主存(primary memory) 
40:程式被鎖在主存,在事務完成前不能被置換   e 
S(state of? the process ) 
O:程式正在處理器執行  ms這個狀態從來木見過, 倒是R常見
S:休眠狀態(sleeping) 
R:等待執行(runable)    R Running or runnable (on run queue) 程式處於執行或就緒狀態
I:空閒狀態(idle) 
Z:殭屍狀態(zombie)    
T:跟蹤狀態(Traced) 
B:程式正在等待更多的記憶體頁 
D:不可中斷的深度睡眠,一般由IO引起,同步IO在做讀或寫操作時,cpu不能做其它事情,只能等待,這時程式處於這種狀態,如果程式採用非同步IO,這種狀態應該就很少見到了
C(cpu usage):cpu利用率的估算值

清除ZOMBIE(殭屍)程式可以使用如下方法:

  1. kill –18 PPID (PPID是其父程式)

這個訊號是告訴父程式,該子程式已經死亡了,請收回分配給他的資源。

  1. 如果不行則看能否終止其父程式(如果其父程式不需要的話)。先看其父程式又無其他子程式,如果有,可能需要先kill其他子程式,也就是兄弟程式。方法是:
    kill –15 PID1 PID2(PID1,PID2是殭屍程式的父程式的其它子程式)。
    然後再kill父程式:kill –15 PPID 這樣殭屍程式就可能被完全殺掉了。

如何殺掉D狀態的程式?

基本想法就是修改核心,遍歷程式列表,找到處於D狀態的程式,將其狀態轉換為別的狀態就可以kill掉了。
這是一種比較粗魯的方法,可能會引起一些不良後果,暫時沒有考慮。對於確切知道已經沒有什麼用處,不用做清理工作的,處於D狀態怎麼也殺不死的程式來說,確是很有效。
核心模組程式碼:

----------------killd.c----------------
#include 
#include 
#include  //for_each_process
MODULE_LICENSE("BSD");
static int pid = -1;
module_param(pid, int, S_IRUGO);
static int killd_init(void)
{
   struct task_struct * p;
   printk(KERN_ALERT "killd: force D status process to death\n");
   printk(KERN_ALERT "killd: pid=%d\n", pid);
   //read_lock(&tasklist_lock);
   for_each_process(p){
       if(p->pid == pid){
           printk("killd: found\n");
           set_task_state(p, TASK_STOPPED);
           printk(KERN_ALERT "killd: aha, dead already\n");
           return 0;
       }
   }
   printk("not found");
   //read_unlock(&tasklist_lock);
   return 0;
}
static void killd_exit(void)
{
   printk(KERN_ALERT "killd: bye\n");
}
module_init(killd_init);
module_exit(killd_exit);
-----Makefile------------
obj-m := killd.o

編譯模組

make -C yourkerneltree M=`pwd` modules

插入模組的時候提供D狀態的程式號,就可以將其轉換為stopped狀態,使用普通kill就可以殺死。

./insmod ./killd.ko pid=1234

如何看各個狀態的程式

程式級別

#ps axwf -eo pid,stat | grep D 

執行緒級別

ps -eL -eo pid,stat,pcpu   | grep D

【參考文件】

1、Linux核心除錯技術—程式D狀態死鎖檢測  https://blog.csdn.net/luckyapple1028/article/details/51931210 

2、如何分析D狀態程式-部落格-雲棲社群-阿里雲 https://yq.aliyun.com/articles/35704

3、centos系統,nginx+php環境,CPU消耗過高,出現較多D狀態程式怎麼辦?https://www.2cto.com/ask/question/524

PHP 如何建立守護(daemon)程式 - 52php - 部落格園 http://www.cnblogs.com/52php/p/6285284.html

相關文章