Linux進程狀態解析之R、S、D、T、Z、X

Linux是一個多用戶,多任務的系統,可以同時運行多個用戶的多個程序,就必然會產生很多的進程,而每個進程會有不同的狀態。

Linux進程狀態:R (TASK_RUNNING),可執行狀態。

只有在該狀態的進程纔可能在CPU上運行。而同一時刻可能有多個進程處於可執行狀態,這些進程的task_struct結構(進程控制塊)被放入對應CPU的可執行隊列中(一個進程最多隻能出現在一個CPU的可執行隊列中)。進程調度器的任務就是從各個CPU的可執行隊列中分別選擇一個進程在該CPU上運行。

很多操作系統教科書將正在CPU上執行的進程定義爲RUNNING狀態、而將可執行但是尚未被調度執行的進程定義爲READY狀態,這兩種狀態在linux下統一爲 TASK_RUNNING狀態。

Linux進程狀態:S (TASK_INTERRUPTIBLE),可中斷的睡眠狀態。

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

通過ps命令我們會看到,一般情況下,進程列表中的絕大多數進程都處於TASK_INTERRUPTIBLE狀態(除非機器的負載很高)。畢竟CPU就這麼一兩個,進程動輒幾十上百個,如果不是絕大多數進程都在睡眠,CPU又怎麼響應得過來。

Linux進程狀態:D (TASK_UNINTERRUPTIBLE),不可中斷的睡眠狀態。

與TASK_INTERRUPTIBLE狀態類似,進程處於睡眠狀態,但是此刻進程是不可中斷的。不可中斷,指的並不是CPU不響應外部硬件的中斷,而是指進程不響應異步信號。
絕大多數情況下,進程處在睡眠狀態時,總是應該能夠響應異步信號的。否則你將驚奇的發現,kill -9竟然殺不死一個正在睡眠的進程了!於是我們也很好理解,爲什麼ps命令看到的進程幾乎不會出現TASK_UNINTERRUPTIBLE狀態,而總是TASK_INTERRUPTIBLE狀態。

而TASK_UNINTERRUPTIBLE狀態存在的意義就在於,內核的某些處理流程是不能被打斷的。如果響應異步信號,程序的執行流程中就會被插入一段用於處理異步信號的流程(這個插入的流程可能只存在於內核態,也可能延伸到用戶態),於是原有的流程就被中斷了。(參見《linux內核異步中斷淺析》)
在進程對某些硬件進行操作時(比如進程調用read系統調用對某個設備文件進行讀操作,而read系統調用最終執行到對應設備驅動的代碼,並與對應的物理設備進行交互),可能需要使用TASK_UNINTERRUPTIBLE狀態對進程進行保護,以避免進程與設備交互的過程被打斷,造成設備陷入不可控的狀態。這種情況下的TASK_UNINTERRUPTIBLE狀態總是非常短暫的,通過ps命令基本上不可能捕捉到。

linux系統中也存在容易捕捉的TASK_UNINTERRUPTIBLE狀態。執行vfork系統調用後,父進程將進入TASK_UNINTERRUPTIBLE狀態,直到子進程調用exit或exec(參見《神奇的vfork》)。
通過下面的代碼就能得到處於TASK_UNINTERRUPTIBLE狀態的進程:

#include   void main() {  if (!vfork()) sleep(100);  }


編譯運行,然後ps一下:

kouu@kouu-one:~/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狀態的父進程依然屹立不倒。

上面一篇文章中我們介紹了Linux進程的R、S、D三種狀態,這裏接着上面的文章介紹另外三個狀態。

Linux進程狀態:T (TASK_STOPPED or TASK_TRACED),暫停狀態或跟蹤狀態。

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

當進程正在被跟蹤時,它處於TASK_TRACED這個特殊的狀態。“正在被跟蹤”指的是進程暫停下來,等待跟蹤它的進程對它進行操作。比如在gdb中對被跟蹤的進程下一個斷點,進程在斷點處停下來的時候就處於TASK_TRACED狀態。而在其他時候,被跟蹤的進程還是處於前面提到的那些狀態。

對於進程本身來說,TASK_STOPPED和TASK_TRACED狀態很類似,都是表示進程暫停下來。
而TASK_TRACED狀態相當於在TASK_STOPPED之上多了一層保護,處於TASK_TRACED狀態的進程不能響應SIGCONT信號而被喚醒。只能等到調試進程通過ptrace系統調用執行PTRACE_CONT、PTRACE_DETACH等操作(通過ptrace系統調用的參數指定操作),或調試進程退出,被調試的進程才能恢復TASK_RUNNING狀態。

Linux進程狀態:Z (TASK_DEAD - EXIT_ZOMBIE),退出狀態,進程成爲殭屍進程。

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

在這個退出過程中,進程佔有的所有資源將被回收,除了task_struct結構(以及少數資源)以外。於是進程就只剩下task_struct這麼個空殼,故稱爲殭屍。
之所以保留task_struct,是因爲task_struct裏面保存了進程的退出碼、以及一些統計信息。而其父進程很可能會關心這些信息。比如在shell中,$?變量就保存了最後一個退出的前臺進程的退出碼,而這個退出碼往往被作爲if語句的判斷條件。
當然,內核也可以將這些信息保存在別的地方,而將task_struct結構釋放掉,以節省一些空間。但是使用task_struct結構更爲方便,因爲在內核中已經建立了從pid到task_struct查找關係,還有進程間的父子關係。釋放掉task_struct,則需要建立一些新的數據結構,以便讓父進程找到它的子進程的退出信息。

父進程可以通過wait系列的系統調用(如wait4、waitid)來等待某個或某些子進程的退出,並獲取它的退出信息。然後wait系列的系統調用會順便將子進程的屍體(task_struct)也釋放掉。
子進程在退出的過程中,內核會給其父進程發送一個信號,通知父進程來“收屍”。這個信號默認是SIGCHLD,但是在通過clone系統調用創建子進程時,可以設置這個信號。

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

#include   void main() {  if (fork())  while(1) sleep(100);  }


編譯運行,然後ps一下:

kouu@kouu-one:~/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狀態。

Linux進程狀態:X (TASK_DEAD - EXIT_DEAD),退出狀態,進程即將被銷燬。

而進程在退出過程中也可能不會保留它的task_struct。比如這個進程是多線程程序中被detach過的進程(進程?線程?參見《linux線程淺析》)。或者父進程通過設置SIGCHLD信號的handler爲SIG_IGN,顯式的忽略了SIGCHLD信號。(這是posix的規定,儘管子進程的退出信號可以被設置爲SIGCHLD以外的其他信號。)
此時,進程將被置於EXIT_DEAD退出狀態,這意味着接下來的代碼立即就會將該進程徹底釋放。所以EXIT_DEAD狀態是非常短暫的,幾乎不可能通過ps命令捕捉到。

進程的初始狀態

進程是通過fork系列的系統調用(fork、clone、vfork)來創建的,內核(或內核模塊)也可以通過kernel_thread函數創建內核進程。這些創建子進程的函數本質上都完成了相同的功能——將調用進程複製一份,得到子進程。(可以通過選項參數來決定各種資源是共享、還是私有。)
那麼既然調用進程處於TASK_RUNNING狀態(否則,它若不是正在運行,又怎麼進行調用?),則子進程默認也處於TASK_RUNNING狀態。
另外,在系統調用調用clone和內核函數kernel_thread也接受CLONE_STOPPED選項,從而將子進程的初始狀態置爲 TASK_STOPPED。

進程狀態變遷

進程自創建以後,狀態可能發生一系列的變化,直到進程退出。而儘管進程狀態有好幾種,但是進程狀態的變遷卻只有兩個方向——從TASK_RUNNING狀態變爲非TASK_RUNNING狀態、或者從非TASK_RUNNING狀態變爲TASK_RUNNING狀態。
也就是說,如果給一個TASK_INTERRUPTIBLE狀態的進程發送SIGKILL信號,這個進程將先被喚醒(進入TASK_RUNNING狀態),然後再響應SIGKILL信號而退出(變爲TASK_DEAD狀態)。並不會從TASK_INTERRUPTIBLE狀態直接退出。

進程從非TASK_RUNNING狀態變爲TASK_RUNNING狀態,是由別的進程(也可能是中斷處理程序)執行喚醒操作來實現的。執行喚醒的進程設置被喚醒進程的狀態爲TASK_RUNNING,然後將其task_struct結構加入到某個CPU的可執行隊列中。於是被喚醒的進程將有機會被調度執行。

而進程從TASK_RUNNING狀態變爲非TASK_RUNNING狀態,則有兩種途徑:
1、響應信號而進入TASK_STOPED狀態、或TASK_DEAD狀態;
2、執行系統調用主動進入TASK_INTERRUPTIBLE狀態(如nanosleep系統調用)、或TASK_DEAD狀態(如exit系統調用);或由於執行系統調用需要的資源得不到滿足,而進入TASK_INTERRUPTIBLE狀態或TASK_UNINTERRUPTIBLE狀態(如select系統調用)。
顯然,這兩種情況都只能發生在進程正在CPU上執行的情況下。

 

.進程的三種基本狀態

    進程在運行中不斷地改變其運行狀態。通常,一個運行進程必須具有以下三種基本狀態。

 就緒(Ready)狀態

    當進程已分配到除CPU以外的所有必要的資源,只要獲得處理機便可立即執行,這時的進程狀態稱爲就緒狀態。

 執行(Running)狀態
當進程已獲得處理機,其程序正在處理機上執行,此時的進程狀態稱爲執行狀態。

 阻塞(Blocked)狀態
正在執行的進程,由於等待某個事件發生而無法執行時,便放棄處理機而處於阻塞狀態。
引起進程阻塞的事件可有多種,例如,等待I/O完成、申請緩衝區不能滿足、等待信件(信號)等。

2.進程三種狀態間的轉換

一個進程在運行期間,不斷地從一種狀態轉換到另一種狀態,它可以多次處於就緒狀態和執行狀態,也可以多次處於阻塞狀態。圖3_4描述了進程的三種基本狀態及其轉換。

 (1) 就緒→執行
處於就緒狀態的進程,當進程調度程序爲之分配了處理機後,該進程便由就緒狀態轉變成執行狀態。

 (2) 執行→就緒
處於執行狀態的進程在其執行過程中,因分配給它的一個時間片已用完而不得不讓出處理機,於是進程從執行狀態轉變成就緒狀態。

 (3) 執行→阻塞
正在執行的進程因等待某種事件發生而無法繼續執行時,便從執行狀態變成阻塞狀態。

 (4) 阻塞→就緒
處於阻塞狀態的進程,若其等待的事件已經發生,於是進程由阻塞狀態轉變爲就緒狀態。


例:

題目:某系統的狀態轉換圖如圖所示。

狀態轉換圖

(1)分別說明引起狀態轉換1、2、3、4的原因,並各舉一個事件。
(2)爲什麼在轉換圖中沒有就緒到阻塞和阻塞到運行的轉換方向?
(3)一個進程的狀態變換能夠引起另一個進程的狀態變換,說明下列因果變遷是否可能發生,原因是什麼?
(a)3→1(b)2→1(c)3→2(d)3→4(e)4→1

答: (1)
1:就緒->執行, 當前運行進程阻塞,調度程序選一個優先權最高的進程佔有處理機;
2:執行->就緒, 當前運行進程時間片用完;
3:執行->阻塞,當前運行進程等待鍵盤輸入,進入了睡眠狀態。
4:阻塞->就緒,I/O操作完成,被中斷處理程序喚醒。

(2)就緒進程沒有佔有處理機,也即沒有經過運行,其狀態就不會改變。
阻塞狀態進程喚醒後先要進入就緒隊列,纔會被調度程序選中,進入了執行狀態。

(3)
(a) 3→1: 可能,當前運行進程阻塞,調度程序選一個優先級最高的進程佔有處理機。
(b)2→1:可能,當前運行進程優先級下降,調度程序選一個優先級最高的進程佔有處理機。
(c)3→2: 不可能,佔有CPU的一個進程不能同時進入兩個狀態;在單CPU的系統中,狀態3發生後,cpu沒有執行進程,故不會發生狀態轉換2。
(d)3→4:一般不可能,不相干的兩個事件。狀態轉換3是由於運行進程等待資源而發生的,這並不會使得阻塞隊列中的進程得到資源而進入就緒隊列。但在Unix中,當系統的0#進程因runin標誌而睡眠時,有(在內存)進程睡眠,就會喚醒0#進程,使其進入就緒狀態,以便將該進程和在盤交換區就緒進程交換位置。
(e)4→1:一般無關,但當就緒隊列爲空時,一個進程被喚醒轉入就緒隊列後,調度程序使該進程佔有處理機(但是同一個進程)。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章