目錄
進程狀態
進程的先描述,再組織 戳鏈接( ̄︶ ̄)↗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操作系統最大進程程數是有限制的, 如果殭屍進程過多, 會導致進程數過多, 創建不了新的進程.
如何避免殭屍進程
- 讓要退出的進程的父進程用wait()阻塞等待子進程退出並回收, 或用waitpid() 隔段時間來查詢子進程是否結束並回收 .
- 採用信號SIGCHLD通知處理,並在信號處理程序中調用wait()函數
具體方法 戳鏈接( ̄︶ ̄)↗https://blog.csdn.net/qq_41071068/article/details/103659853 -
讓殭屍進程變成孤兒進程,由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號進程回收.