目錄
前言
一、補充知識點
1、并行與并發
2、時間片
3、 等待的本質
4、掛起
二. 進程的基本狀態
三、代碼演示
1、R與S
?2、T
3、Z
?四、孤兒進程
總結:
前言
在操作系統中,進程是程序執行的基本單位。每個進程都有自己的狀態,這些狀態反映了進程在系統中的當前活動情況。理解進程狀態對于系統編程、性能調優和問題排查至關重要。今天,我們將深入探討Linux進程的各種狀態,并結合實際例子分析它們的行為。
一、補充知識點
在上文中我們介紹了進程的屬性與進程的創建(詳細查看上文鏈接),在了解我們本篇文章的主題——進程狀態之前,我們需要給大家先補充幾個知識點概念:
1、并行與并發
單核CPU執行進程代碼不是把進程代碼執行完畢才開始執行下一個,而是給每一個進程預分配一個時間片,基于時間片進行調度輪轉(單CPU下),這通常被稱為并發。
所謂并發,就是多個進程在一個CPU下采用的進程切換的方式,在一段時間之內讓多個進程都得以推進。
那么所謂并行呢?就是多個進程在多個CPU下分別同時進行運行,這稱之為并行。
CPU切換和運行的速度非常快,站在我們對應的這個CPU看來呢,當前這一個物理CPU它切換了多個進程,但是因為它切換和運行的速度非常快,所以用戶根本就感知不到,
所以這也就解釋為什么我們平時的死循環把不會程序卡死的原因,CPU會直接在操作系統的指導下,他會按照時間片來進行我們對應的輪轉和調度。
2、時間片
Linux /Windows民用級別的操作系統為分時操作系統。
就是我們整個操作系統它在幫你去執行任務時,是會給每一個任務給他分配上對應的一個時間片的,比如說是10毫秒或者是1毫秒。把時間片分好之后,每一個進程在CPU上去運行時,他把自己的時間片耗盡了,他就必須得從CPU上去剝離下來,剝離下來之后再把另一個任務再放上去。這就叫做分時操作系統。沒有優先級的誰高誰低,特點:調度任務追求公平,這保證了保證用戶操作的及時響應。
實時操作系統通常用于VxWorks、FreeRTOS、QNX(工業/嵌入式領域),主要特點是確定性調度,任務優先級嚴格分級,高優先級任務可搶占低優先級任務,支持硬實時(Hard Real-Time)和軟實時(Soft Real-Time)
-
硬實時:必須在絕對截止時間內完成(如航天控制系統)
-
軟實時:允許偶爾超時(如視頻流處理)
3、 等待的本質
?等待的本質:當進程需要訪問外部設備(如鍵盤、磁盤、網絡)時,若設備未就緒,CPU 不會持續輪詢等待,而是將該進程移出運行隊列,掛入設備的等待隊列。
操作系統是怎么管理硬件的呢?也是:先描述,再組織
每個硬件設備(如鍵盤、磁盤)在內核中對應一個?struct device
?結構體,包含:
struct device {unsigned int id; // 設備唯一標識enum device_status status;// 設備狀態(就緒/忙碌/錯誤)struct list_head wait_queue; // 等待該設備的進程隊列// 其他驅動相關字段... };
當進程需等待設備時,其 PCB 會被鏈入設備的?wait_queue(等待隊列)
,直至設備觸發中斷通知就緒。
?阻塞和運行本質上都是在等待。,只是一個在硬件的等待隊列中等待,一個在CPU的運行隊列中等待
當從鍵盤中輸入數據后,操作系統會得到信息,隨后又將該PCB連入運行隊列。
本質上就是把一個PCB一會放在運行隊列里,一會放在設備等待隊列里,來回的去調用。
4、掛起
內存不足時,操作系統將非活躍進程的代碼和數據換出到磁盤(Swap 分區),僅保留 PCB 在內存,這就叫做掛起。
特點是:用時間換空間:換入/換出操作增加延遲,但緩解內存壓力。
在掛起后,進程變為進程變為阻塞掛起狀態,掛起通常是在Swap分區里進行的。在云服務器中通常會禁掉Swap分區,這是為了防止頻繁的進行換入與換出操作導致性能驟降。
二. 進程的基本狀態
在Struct device中有一個status屬性,這個通常記錄了當前進程的狀態。
我們經常在一些教科書上看見以下圖片:
這樣的圖片肯定是無法讓大家清楚的明白什么是進程的狀態。?
在Linux中,進程的狀態通常可以通過?ps
?或?top
?命令查看,常見的有:
-
R(Running):?并不意味著進程?定在運?中,它表明進程要么是在運?中要么在運?
隊列?。 -
S(Sleeping):可中斷睡眠(意味著進程在等待事件完成,如I/O)。
-
D(Uninterruptible Sleep):不可中斷睡眠(通常涉及硬件操作)。
-
T(Stopped):進程被暫停,可以通過發送 SIGSTOP 信號(如?
kill -19
)給進程來停止(T)進程。這個被暫停的進程可以通過發送 SIGCONT 信號讓進程繼續運?。 -
Z(Zombie):僵尸進程(已終止但未被父進程回收)。
-
X(Dead):進程已完全終止(這個狀態只是?個返回狀態,你不會在任務列表?看到這個狀態。)。
此外,還有一些特殊狀態(以下并非全部):
-
t(Tracing stop):進程被調試器暫停(如?
gdb
?斷點)。 -
<(高優先級)?和?N(低優先級):調度優先級相關。
我們通常可以在終端中輸入?ps ajx或ps aux來查看:
三、代碼演示
我們用以下代碼與指令操作給大家演示一下進程各種狀態的查看:
1、R與S
在當前路徑中我們有以下文件:
code.cpp:
#include<stdio.h>
#include<unistd.h>int main()
{int cnt=0;while(1){printf("hello world, cnt: %d,my pid : %d\n",cnt++ ,getpid());}return 0;
}
Makefile:
# 定義編譯器和編譯選項
CXX = g++
CXXFLAGS = -Wall -std=c++11# 定義目標文件和可執行文件名
TARGET = code
SRC = code.cpp# 默認目標
all: $(TARGET)# 直接生成可執行文件(不生成.o文件)
$(TARGET): $(SRC)$(CXX) $(CXXFLAGS) -o $@ $<# 清理生成的文件
clean:rm -f $(TARGET)# 運行程序
run: $(TARGET)./$(TARGET).PHONY: all clean run
首先我們調用make生成可執行文件code(exe),隨后輸入
./code
運行code可執行文件:
隨后在另外一個終端中輸入
watch -n 1 '(ps ajx | head -n 1; ps ajx | grep -w "./code" | grep -v grep)'
?查看進程狀態:
?這里有兩個code進程,第一個進程是bash創建的進程組,用于管理我們在前臺運行的code程序,我們不用管它,通過pid 3825121我們可以知道第二個code就是我們運行的程序,可以看見,code的進程狀態(就是STAT這一欄),一直在R與S中變換(+號表示?前臺進程組,受終端控制,如
Ctrl+C
能終止它),這是為什么呢?
進程狀態切換是由?CPU時間片調度?和?I/O等待?共同作用的結果:
-
R+(Running):進程正在CPU上執行,或位于運行隊列等待調度。
-
S+(Sleeping):進程因等待I/O(如
printf
到終端)被移出運行隊列。
我們的code.cpp中有著printf這個函數,這會涉及到IO的相關操作,printf不是直接輸出到屏幕,而是寫入?標準輸出緩沖區,最終通過?終端設備(如/dev/pts/0
)顯示。又因為終端I/O速度遠慢于CPU,因此每次printf
都可能觸發進程阻塞(進入S
狀態)。
R → S:當進程調用阻塞式I/O(如printf
、scanf
)。
S → R:當I/O操作完成(如終端準備好接收輸出)。
?2、T
依舊是原來幾個文件,我們繼續運行code:
我們在創建一個終端,輸入kill -19 +【對應進程PID】
?此時我們透過之前的查看進程狀態的終端可以發現,code進程已經變為了T狀態:
?若我們此時在輸入kill -18:
?又會發現:
程序又開始跑起來了,與之前不同的是,沒有了+號,這是因為被中斷后又繼續后,進程默認變為了后臺進程,此時在進程運行的終端上,輸入strl c是不會終止進程的,這個時候就只能通過kill -9來殺死進程:
3、Z
?在講僵尸狀態之前,我們先想一下,一個進程為什么會被創建出來呢?
一個進程會被創建出來是為了完成用戶的某個任務,那么操作系統怎么知道這個任務是否完成成功了呢?
我們以前寫代碼,比如做題,可以通過打印信息來了解,如果打印信息無關呢?
我們更改code.cpp代碼如下:
#include<stdio.h>
#include<unistd.h>// int main()
// {
// int cnt=0;
// while(1)
// {
// printf("hello world, cnt: %d,my pid : %d\n",cnt++ ,getpid());
// }
// return 0;
// }int main()
{int cnt=0;for(int i=0;i<10;i++){cnt++;}return 0;
}
code不再是一個無限循環代碼,重新輸入make生成可執行code文件并運行:
?可以看見,如果沒有打印信息,我們是無法知道任務完成成功了嗎?
請大家在終端上輸入:echo $?
?我們再把code.cpp中main函數的返回值設定為11呢?
return 11;
再運行:
我們發現,這次打印出的數又變成11了。細心的同學可能就有所猜測了。
沒錯,我們每個程序的main函數最后都會有一個return 返回值,當我們重新正常運行結束后,會執行return語句,這個返回的數,就會被父進程接受,告訴父進程,該進程執行任務是否成功。?
我們規定,返回0為執行任務成功,返回非0為失敗。
進程退出時:
1、代碼不會執行了,首先可以立即釋放的就是進程對應的程序信息數據
2、進程退出要有退出信息,保存在自己的task_struct內部
3、管理該進程的task_struct必須被OS維護起來,方便用戶未來進行獲取進程退出的信息
那么這個跟Z僵尸狀態有什么關聯呢?大家不要著急,我們把code.cpp更改如下:
#include<stdio.h>
#include<unistd.h>// int main()
// {
// int cnt=0;
// while(1)
// {
// printf("hello world, cnt: %d,my pid : %d\n",cnt++ ,getpid());
// }
// return 0;
// }// int main()
// {
// int cnt=0;
// for(int i=0;i<10;i++)
// {
// cnt++;
// }
// return 11;
// }int main()
{pid_t id =fork();if(id==0){//子進程int n=10;while(n--){printf("i am child, pid: %d, ppid: %d\n",getpid(),getppid());sleep(1);}}else {//父進程while(1){ printf("i am parent, pid:%d\n",getpid());sleep(1);}}return 0;
}
?運行并輸入
watch -n 1 '(ps ajx | head -n 1; ps ajx | grep -w "code" | grep -v grep)'
查看狀態:
我們可以看見,在子進程未執行完畢時, 二者都是出現S+或者R+的狀態,但是當我們子進程執行完畢后,可是父進程沒執行完畢,就會出現僵尸狀態:
僵尸狀態的進程:如果沒有人管我,我就會一直僵尸,task_struct會一直消耗內存→造成內存泄漏。
后面我們會講到:一般需要父進程讀取子進程信息,子進程才會自動退出。(調用waitpid),我們這里的代碼沒有調用waitpid,而是一直在循環,就不會去讀取子進程的退出信息,導致子進程一直處于僵尸狀態。
語言層面的內存泄漏的問題,如果在常駐的進程中出現,影響比較大:比如殺毒軟件。
?四、孤兒進程
剛剛的僵尸進程是子進程退出了,父進程還在。但如果是父進程死掉了,子進程還在呢?
更改code代碼如下:
?
#include<stdio.h>
#include<unistd.h>int main()
{pid_t id =fork();if(id==0){//子進程while(1){printf("i am child, pid: %d, ppid: %d\n",getpid(),getppid());sleep(1);}}else {//父進程while(1){ printf("i am parent, pid:%d\n",getpid());sleep(1);}}return 0;
}
我們在第三個終端(用來輸入其他指令kill時所使用的終端),輸入kill指令殺死父進程:
我們可以發現:
此時的子進程3868637的父進程已經變為1了。
那么這個1進程是什么呢?
輸入指令:
ps -fp 1
這個失去原本父進程的子進程,就被稱為孤兒進程。如果父進程先退出了,子進程還在:子進程成為孤兒進程,會被系統領養(一般是systemd,我這臺云服務器是屬于例外)?。
總結:
本文著重介紹了進程的幾個狀態,并通過各種代碼事例帶大家見識了一下狀態,并為各位介紹了什么是孤兒進程。這就是本篇博客進程狀態的主要內容,希望對各位有所幫助。有疑問可以在評論區提出!!!