Linux 進程狀態(R, S, D, T(t), X, Z)

目錄

進程狀態

R-- 運行狀態

S-- 睡眠狀態(可中斷睡眠狀態)

D-- 磁盤休眠狀態(不可中斷睡眠)

T-- 停止狀態& t-- (跟蹤狀態)

X-- 死亡狀態(退出狀態)

Z-- 僵死狀態&殭屍進程

殭屍進程

殭屍進程危害

如何避免殭屍進程

孤兒進程


進程狀態

進程的先描述,再組織 戳鏈接( ̄︶ ̄)↗https://blog.csdn.net/qq_41071068/article/details/103213364

在進程的組織中我們以學校組織管理學生爲例子. 那麼 在學校中,  學生有着不同的狀態,  比如說, 休學, 請假, 畢業, 退學,  學業警

告等各種表示學生學習生活狀態的信息 .進程就像學校裏的學生一樣, 也有着不同的狀態. 我們來看 內核原碼中對linux下進程狀態

的定義, 如下 :

/*
* The task state array is a strange "bitmap" of
* reasons to sleep. Thus "running" is zero, and
* you can test for combinations of others with
* simple bit tests.
*/
static const char * const task_state_array[] = {
    "R (running)", /* 0 */         /*[重點]*/
    "S (sleeping)", /* 1 */        /*[重點]*/
    "D (disk sleep)", /* 2 */
    "T (stopped)", /* 4 */         /*[重點]*/
    "t (tracing stop)", /* 8 */
    "X (dead)", /* 16 */
    "Z (zombie)", /* 32 */         /*[重點]*/
};
  • R-- 運行狀態(running): 並不意味着進程一定在運行中,它表明進程要麼是在運行中要麼在運行隊列裏。
  • S-- 睡眠狀態(sleeping): 意味着進程在等待事件完成(這裏的睡眠有時候也叫做可中斷睡眠(interruptible sleep)
  • D-- 磁盤休眠狀態(Disk sleep)有時候也叫不可中斷睡眠狀態(uninterruptible sleep),在這個狀態的進程通常會等待IO的結束.
  • T-- 停止狀態(stopped): 可以通過發送 SIGSTOP 信號給進程來停止(T)進程。這個被暫停的進程可以通過發送 SIGCONT 信號讓進程繼續運行。
  • X-- 死亡狀態(dead):這個狀態只是一個返回狀態,你不會在任務列表裏看到這個狀態。
  • Z-- 殭屍狀態 (zombie) : 已經死了, 依然佔着某些資源

R-- 運行狀態

只有該狀態下的進程纔可能在CPU中執行, 爲什麼說是可能呢? 因爲在Linux下, 正在運行和就緒(在運行隊列中, 拿到時間片就能

運行)的進程都視作運行狀態. Linux 下 R 狀態的進程的task_struct結構 (PCB) 被放入對應的CPU的可執行隊列中(一個進程最多

只能出現在一個CPU的可執行隊列中). 進程調度器的任務就是從各個CPU的可執行隊列中分別選擇一個進程在該CPU上行 .

如下面代碼運行會進入死循環, 會一直佔用CPU, 處於R狀態

#include<unistd.h>
int main(){
    while(1);
    return 0;
}

S-- 睡眠狀態(可中斷睡眠狀態)

處於這個狀態的進程因爲等待某個事件的發生, 比如被wait()阻塞,而被掛起. 這些進程的task_struct被放入對應事件的等待

隊列中. 當等待的事件發生時(由外部中斷觸發、或由其他進程觸發),對應的等待隊列中的這個睡眠狀態的進程被喚醒.

通過ps命令我們會看到,一般情況下,進程列表中的絕大多數進程都處於S 狀態(除非機器正處於高負荷狀態下. 畢竟CPU就那

麼幾個,而進程動輒就是幾十上百個,如果不是絕大多數進程都在睡眠,那對於CPU來說, CPU簡直太難了 .

如下代碼, 子進程由於sleep進入S狀態, 父進程由於wait阻塞等待子進程退出, 也處於S狀態

#include<stdio.h>
#include<unistd.h>
#include<sys/wait.h>
int main(){
    pid_t pid = fork();
    if(pid == 0){
        sleep(30);
        _exit(0);
    }
    wait(NULL);
    return 0;
}

 

D-- 磁盤休眠狀態(不可中斷睡眠)

與S狀態類似, D狀態進程也處於 "睡眠狀態", 但是此刻的睡眠時不可中斷的.  不可中斷,指的並不是CPU不響應外部硬件的中

斷,而是指進程不響應異步信號 . 絕大多數情況下,進程處在睡眠狀態時,總是應該能夠響應異步信號的。否則你將驚奇的發

現,kill -9竟然殺不死一個正在睡眠的進程了!於是我們也很好理解,爲什麼ps命令看到的進程幾乎不會出現D 狀態,而總是S狀

態.  而 D 狀態存在的意義就在於,內核的某些處理流程是不能被打斷的. 如果響應異步信號,程序的執行流程中就會被插入一段

用於處理異步信號的流程(這個插入流程可能只存在於內核態,也可能延伸到用戶態),於是原有的流程被中斷了. 在進程對某

些硬件進行操作時(比如進程調用read系統調用對某個設備文件進行讀操作,而read系統調用最終執行到對應設備驅動的代碼,

並與對應的物理設備進行交互), 可能需要使用 D 狀態對進程進行保護,以避免進程與設備交互的過程被打斷,造成設備陷入不

可控的狀態 .  這種情況下的 D 狀態總是非常短暫的,通過ps命令基本上不可能捕捉到 .

很難捕捉, 但還是可以捕捉的, 讓grep 在根目錄下搜索字符, grep會對磁盤進行掃描, 即處於不可中斷的D狀態, 直到IO結束.

T-- 停止狀態& t-- (跟蹤狀態)

向進程發送一個SIGSTOP(停止信號)信號,它就會因響應信號而進入T狀態(除非該進程本身處於 D 狀態而不響應信號 ).

(SIGSTOP與SIGKILL(無條件終止)信號一樣,是非強制的. 不允許用戶進程通過signal系統的系統調用重新設置對應的信號處理

函數)向進程發送一個SIGCONT(繼續信號)信號,可以讓其從 T 狀態恢復到 R 狀態。

當進程正在被跟蹤時,它處於 t 這個特殊的狀態。“正在被跟蹤”指的是進程暫停下來,等待跟蹤它的進程對它進行操作。比如在

gdb中對被跟蹤的進程下一個斷點,進程在斷點處停下來的時候就處於 t 狀態。而在其他時候,被跟蹤的進程還是處於前面提到

的那些狀態。對於進程本身來說,T 和 t 狀態很類似,都是表示進程暫停下來。而 t 狀態相當於在 T 之上多了一層保護,處於 t

狀態的進程不能響應SIGCONT信號而被喚醒. 只能等到調試進程通過ptrace系統調用執行PTRACE_CONT、PTRACE_DETACH

等操作 (通過ptrace系統調用的參數指定操作), 或調試進程退出,被調試的進程才能恢復 R 狀態 .

如下代碼:

#include<unistd.h>
int main(){
    while(1);
    return 0;
}

進入死循環後是 R 狀態 , 用 kill -STOP PID 來暫停, 如下

如下代碼 

#include<stdio.h>
int main(){
    int flag = 10;
    if(flag == 10){
        flag = 0;
    }
    return 0;
}

當執行到斷點暫停, 進程此時處於 t 狀態

X-- 死亡狀態(退出狀態)

進程即將被銷燬時的狀態, 所以X狀態是非常短暫的,幾乎不可能通過ps命令捕捉到 .

Z-- 僵死狀態&殭屍進程

在進程退出過程中, 進程佔有的所有資源將被回收, 除了task_struct結構 (以及少數資源) 以外不立即釋放.

但當一個進程退出, 但其所佔用的資源沒有釋放回收時, 這個進程就變成了殭屍進程, 處於僵死狀態.

例如當子進程先於父進程退出時, 爲了保存自身的退出狀態(退出碼或異常信號), task_struct 以及少數資源不會立即釋放, 一直等

待父進程接收其退出狀態, 這時操作系統會通知父進程獲取子進程的退出狀態, 當父進程獲取到子進程的退出狀態後, 釋放子進程

未釋放完的資源. 但若父進程沒有關注到子進程退出, 一直沒有獲取子進程退出狀態. 這時, 子進程就成爲了一個只有着一個空

task_struct(只保存着退出狀態, 以及一些統計信息). 而沒有真正的靈魂, "有的進程活着, 但它已經死了", 此時這個子進程就處於僵

死狀態.  

當然,內核也可以將這些信息保存在別的地方,而將task_struct釋放掉,以節省一些空間。但是使用task_struct結構更爲方

便,因爲內核中已經建立了從pid到task_struct查找關係,還有進程間的父子關係。釋放掉task_struct,則需要建立一些新的數據

結構,以便讓父進程找到它的子進程的退出信息。父進程可以通過wait系列的系統調用(如wait4,waitid)來等待某個或某些子進

程的退出,並獲取它的退出信息. 然後wait系列的系統調用會順便將子進程的屍體(task_struct)也釋放掉. 前面說到, 子進程在退出

時操作系統會通知父進程獲取退出碼, 即內核會給其父進程發送一個信號,通知父進程來收屍. 這個信號默認是SIGCHLD,但是

在通過clone系統調用(fork()和vfork()就是封裝了clone)創建子進程時,可以設置這個信號 .

殭屍進程

處於僵死狀態的進程稱之爲殭屍進程.

下面代碼

#include <stdio.h>
#include <stdlib.h> 
#include<unistd.h>
int main() {
    pid_t pid = fork();
    if(pid < 0){ 
        perror("fork"); 
        return 1; 
    }
    else if(pid == 0){
        printf("child[%d] is begin Z...\n", getpid());
        sleep(5); 
        exit(EXIT_SUCCESS);  
    }
    else{
        printf("parent[%d] is sleeping...\n", getpid());
        sleep(10); 
    }
    return 0; 
}

 

可以看到,  在父進程處於S狀態時, 沒有獲取子進程退出狀態, 子進程就變成了殭屍進程.

殭屍進程危害

  • 子進程先於父進程終止,而父進程又沒有調用wait或waitpid, 此種情況子進程進入僵死狀態,並且會一直保持下去直到系統重啓. 內核只保存進程的一些必要信息在task_struct中以備父進程所需.
  • 那一個父進程創建了很多子進程,要是都不回收,就會造成內存資源的浪費, 因爲task_struct(PCB)數據結構對象本身就要佔用內存,要在內存的某個位置進行開闢空間, 這樣就會造成資源泄漏.
  • Linux操作系統最大進程程數是有限制的, 如果殭屍進程過多, 會導致進程數過多, 創建不了新的進程.

如何避免殭屍進程

  1. 讓要退出的進程的父進程用wait()阻塞等待子進程退出並回收,  或用waitpid() 隔段時間來查詢子進程是否結束並回收 .
     
  2. 採用信號SIGCHLD通知處理,並在信號處理程序中調用wait()函數
    具體方法 戳鏈接( ̄︶ ̄)↗https://blog.csdn.net/qq_41071068/article/details/103659853
  3. 讓殭屍進程變成孤兒進程,由init進程回收,就是讓父進程先退出

wait()和waitpid()詳解在另一篇中,戳鏈接( ̄︶ ̄)↗https://blog.csdn.net/qq_41071068/article/details/103302883

還是上面的代碼. 稍作修改, 如下 :

#include <stdio.h>
#include <stdlib.h> 
#include <unistd.h>
#include<sys/wait.h>
int main() {
    pid_t pid = fork();
    if(pid < 0){ 
        perror("fork"); 
        return 1; 
    }
    else if(pid == 0){
        sleep(5); 
        exit(EXIT_SUCCESS);  
    }
    printf("父進程被wait()阻塞, 等待子進程退出並回收\n");
    wait(NULL);
    return 0; 
}

孤兒進程

那麼如果父進程先退出了呢, 誰又來給子進程“收屍”?當一個父進程進程退出的時候,會將它的所有子進程都託管給別的進程(使

之成爲別的進程的子進程. 託管給誰呢?可能是退出進程所在進程組的下一個進程 (如果存在的話 ), 或者是1號進程。所以每個進

程每時每刻都有父進程存在. 除非它是1號進程. 

1號進程: pid爲1的進程, 又稱init進程. linux系統啓動後, 第一個被創建的用戶態進程就是init進程.

它有兩項使命:

1、執行系統初始化腳本,創建一系列的進程(它們都是init進程的子孫)

2、在一個死循環中等待其子進程的退出事件,並調用waitid系統調用來完成“收屍”工作 ;
     init進程不會被暫停, 也不會被殺死(這是由內核來保證的). 它在等待子進程退出的過程中處於S狀態,  “收屍”過程
     中則處於R狀態.

如下代碼

#include<stdio.h>
#include<unistd.h>
#include<stdlib.h>
int main(){
    pid_t pid = fork();
    if(pid < 0){
        perror("fork");
        exit(-1);
    }
    else if(pid == 0){
        printf("子進程睡一會\n");
        sleep(10);
        printf("子進程退出\n");
    }
    else{
        printf("父進程先走一步\n");
        exit(0);
    }
    return 0;
}

 

在子進程睡眠的這10秒內, 可以看到子進程被1號進程收養, 子進程退出後, 由1號進程回收.

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