歡迎來到Linux教程網
Linux教程網
Linux教程網
Linux教程網
您现在的位置: Linux教程網 >> UnixLinux >  >> Linux基礎 >> Linux技術

Linux進程狀態(ps stat)之R、S、D、T、Z、X

我們先來了解一下各種進程狀態:

TASK_RUNNING
:進程當前正在運行,或者正在運行隊列中等待調度。
TASK_INTERRUPTIBLE
:進程處於睡眠狀態,正在等待某些事件發生。進程可以被信號中斷。接收到信號或被顯式的喚醒呼叫喚醒之後,進程將轉變為
TASK_RUNNING
狀態。

TASK_UNINTERRUPTIBLE
:此進程狀態類似於
TASK_INTERRUPTIBLE
,只是它不會處理信號。中斷處於這種狀態的進程是不合適的,因為它可能正在完成某些重要的任務。 當它所等待的事件發生時,進程將被顯式的喚醒呼叫喚醒。(task_
UNINTERRUPTIBLE
)狀態不會響應任何信號,所有Kill -9 pid,會發現進程依然存在
TASK_STOPPED
:進程已中止執行,它沒有運行,並且不能運行。接收到
SIGSTOP
SIGTSTP
等信號時,進程將進入這種狀態。接收到
SIGCONT
信號之後,進程將再次變得可運行。

TASK_TRACED
:正被調試程序等其他進程監控時,進程將進入這種狀態。
EXIT_ZOMBIE
:進程已終止,它正等待其父進程收集關於它的一些統計信息。

EXIT_DEAD
:最終狀態(正如其名)。將進程從系統中刪除時,它將進入此狀態,因為其父進程已經通過
wait4()
waitpid()
調用收集了所有統計信息。

內核映射

Linux 內核提供了兩種方法將進程置為睡眠狀態。

將進程置為睡眠狀態的普通方法是將進程狀態設置為

TASK_INTERRUPTIBLE
TASK_UNINTERRUPTIBLE
並調用調度程序的
schedule()
函數。這樣會將進程從 CPU 運行隊列中移除。如果進程處於可中斷模式的睡眠狀態(通過將其狀態設置為
TASK_INTERRUPTIBLE
),那麼可以通過顯式的喚醒呼叫(
wakeup_process()
)或需要處理的信號來喚醒它。但是,如果進程處於非可中斷模式的睡眠狀態(通過將其狀態設置為
TASK_UNINTERRUPTIBLE
),那麼只能通過顯式的喚醒呼叫將其喚醒。除非萬不得已,否則我們建議您將進程置為可中斷睡眠模式,而不是不可中斷睡眠模式(比如說在設備 I/O 期間,處理信號非常困難時)。

當處於可中斷睡眠模式的任務接收到信號時,它需要處理該信號(除非它已被屏弊),離開之前正在處理的任務(此處需要清除代碼),並將

-EINTR
返回給用戶空間。再一次,檢查這些返回代碼和采取適當操作的工作將由程序員完成。因此,懶惰的程序員可能比較喜歡將進程置為不可中斷模式的睡眠狀態,因為信號不會喚醒這類任務。但需要注意的一種情況是,對不可中斷睡眠模式的進程的喚醒呼叫可能會由於某些原因不會發生,這會使進程無法被終止,從而最終引發問題,因為惟一的解決方法就是重啟系統。一方面,您需要考慮一些細節,因為不這樣做會在內核端和用戶端引入bug。另一方面,您可能會生成永遠不會停止的進程(被阻塞且無法終止的進程)。

現在,我們在內核中實現了一種新的睡眠方法!Linux Kernel 2.6.25 引入了一種新的進程睡眠狀態,

TASK_KILLABLE
:當進程處於這種可以終止的新睡眠狀態中,它的運行原理類似於
TASK_UNINTERRUPTIBLE
,只不過可以響應致命信號,kill -9一個進程在調用exit命令結束自己的生命的時候,其實它並沒有真正的被銷毀,而是留下一個稱為僵屍進程(Zombie)的數據結構(系統調用 exit,它的作用是使進程退出,但也僅僅限於將一個正常的進程變成一個僵屍進程,並不能將其完全銷毀)。

在Linux進程的狀態中,僵屍進程是非常特殊的一種,它已經放棄了幾乎所有內存空間,沒有任何可執行代碼,也不能被調度,僅僅在進程列表中保留一個位 置,記載該進程的退出狀態等信息供其他進程收集,除此之外,僵屍進程不再占有任何內存空間。它需要它的父進程來為它收屍,如果他的父進程沒安裝 SIGCHLD信號處理函數調用wait或waitpid()等待子進程結束,又沒有顯式忽略該信號,那麼它就一直保持僵屍狀態,如果這時父進程結束了, 那麼init進程自動會接手這個子進程,為它收屍,它還是能被清除的。但是如果如果父進程是一個循環,不會結束,那麼子進程就會一直保持僵屍狀態,這就是

為什麼系統中有時會有很多的僵屍進程。

在UNIX 系統中,一個進程結束了,但是他的父進程沒有等待(調用wait / waitpid)他, 那麼他將變成一個僵屍進程. 在fork()/execve()過程中,假設子進程結束時父進程仍存在,而父進程fork()之前既沒安裝SIGCHLD信號處理函數調用 waitpid()等待子進程結束,又沒有顯式忽略該信號,則子進程成為僵屍進程。

因此,一個僵屍進程產生的過程是:父進程調用fork創建子進程後,子進程運行直至其終止,它立即從內存中移除,但進程描述符仍然保留在內存中(進程描述符占有極少的內存空間)。子進程的狀態變成EXIT_ZOMBIE,並且向父進程發送SIGCHLD 信號,父進程此時應該調用 wait() 系 統調用來獲取子進程的退出狀態以及其它的信息。在 wait 調用之後,僵屍進程就完全從內存中移除。因此一個僵屍存在於其終止到父進程調用wait 等函數這個時間的間隙,一般很快就消失,但如果編程不合理,父進程從不調用 wait 等系統調用來收集僵屍進程,那麼這些進程會一直存在內存中。

怎樣來清除僵屍進程:

1.改寫父進程,在子進程死後要為它收屍。具體做法是接管SIGCHLD信號。子進程死後,會發送SIGCHLD信號給父進程,父進程收到此信號後,執 行waitpid()函數為子進程收屍。這是基於這樣的原理:就算父進程沒有調用wait,內核也會向它發送SIGCHLD消息,盡管對的默認處理是忽 略,如果想響應這個消息,可以設置一個處理函數。

2.把父進程殺掉。父進程死後,僵屍進程成為"孤兒進程",過繼給1號進程init,init始終會負責清理僵屍進程.它產生的所有僵屍進程也跟著消失。

The system load data is collected by the calc_load(

) function, which is invoked by update_times( ).

This activity is therefore performed in the TIMER_BH bottom half. calc_load(

) counts the number of processes in the TASK_RUNNING or TASK_UNINTERRUPTIBLE state

and uses this number to update the CPU usage statistics.

上面清楚的說明,Linux在計算CPU負載的時候,TASK_RUNNING和TASK_UNINTERRUPTIBLE兩種狀態的進程都會被統計

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進程狀態: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上執行的情況下。

內核模塊代碼:

—————-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

Copyright © Linux教程網 All Rights Reserved