Linux是一個多用戶,多任務的系統,能夠同時運行多個用戶的多個程序,就必然會產生不少的進程,而每一個進程會有不一樣的狀態。linux
Linux進程狀態:R (TASK_RUNNING),可執行狀態。shell
只有在該狀態的進程纔可能在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),不可中斷的睡眠狀態。socket
與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狀態的進程:函數
1 2 3 4 |
#include void main() { if (!vfork()) sleep(100); } |
編譯運行,而後ps一下:spa
1 2 3 4 |
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系列的系統調用(如wait四、waitid)來等待某個或某些子進程的退出,並獲取它的退出信息。而後wait系列的系統調用會順便將子進程的屍體(task_struct)也釋放掉。
子進程在退出的過程當中,內核會給其父進程發送一個信號,通知父進程來「收屍」。這個信號默認是SIGCHLD,可是在經過clone系統調用建立子進程時,能夠設置這個信號。
經過下面的代碼可以製造一個EXIT_ZOMBIE狀態的進程:
1 2 3 4 5 |
#include void main() { if (fork()) while(1) sleep(100); } |
編譯運行,而後ps一下:
1 2 3 4 |
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進程。它有兩項使命:
執行系統初始化腳本,建立一系列的進程(它們都是init進程的子孫);
在一個死循環中等待其子進程的退出事件,並調用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狀態,則有兩種途徑:
響應信號而進入TASK_STOPED狀態、或TASK_DEAD狀態;
執行系統調用主動進入TASK_INTERRUPTIBLE狀態(如nanosleep系統調用)、或TASK_DEAD狀態(如exit系統調用);或因爲執行系統調用須要的資源得不到知足,而進入TASK_INTERRUPTIBLE狀態或TASK_UNINTERRUPTIBLE狀態(如select系統調用)。
顯然,這兩種狀況都只能發生在進程正在CPU上執行的狀況下。
內核模塊代碼:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 |
—————-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); |
1 2 |
—–Makefile———— obj-m := killd.o |
編譯模塊
1 |
make -C yourkerneltree M=`pwd` modules |
插入模塊的時候提供D狀態的進程號,就能夠將其轉換爲stopped狀態,使用普通kill就能夠殺死。
1 |
./insmod ./killd.ko pid=1234 |