一、進程狀態(tài)概述
進程狀態(tài)是指在操作系統(tǒng)中,一個進程所處的不同運行狀態(tài),進程狀態(tài)就決定了該進程接下來要執(zhí)行什么任務。常見的進程狀態(tài)有以下幾種:
-
新建狀態(tài):進程被創(chuàng)建但還沒有被操作系統(tǒng)接受和分配資源。
-
就緒狀態(tài):進程已經(jīng)獲得了所需的資源,并等待被調(diào)度執(zhí)行。
-
運行狀態(tài):進程正在執(zhí)行指令,占用CPU資源。
-
阻塞狀態(tài):進程因等待某個事件(如IO操作)而暫時停止執(zhí)行,并釋放CPU等資源。
-
終止狀態(tài):進程執(zhí)行完成或被終止,釋放所有資源。
1.1 運行狀態(tài)詳解
在上一篇文章【Linux取經(jīng)路】揭秘進程的父與子提到過,一般的計算機中只有一個 CPU,而進程卻可能有很多個,這就注定了 CPU 是一個少量的資源,對所有的進程來說,運行的本質(zhì),就是把它放到 CPU 上,所以每個 CPU 都會維護一個運行隊列,CPU 以隊列的形式對進程做調(diào)度。所有的進程要運行都要在運行隊列中排隊,參與排隊的是每個進程的 PCB 對象。所有在運行隊列中的進程,它們所處的狀態(tài)就叫做運行態(tài)(R狀態(tài))。
一個進程只要把自己放到 CPU 上開始運行,并不是一直要執(zhí)行完畢,才把自己放下來。如果一個進程被放到 CPU 上直到執(zhí)行完畢才把自己放下來繼續(xù)去執(zhí)行其他進程,那當我們的程序中寫了一個 while 死循環(huán)出來,在運行該程序的時候,其他的應用就會卡住。但現(xiàn)實并不是這樣,我們寫了一個 while 死循環(huán),其他程序照樣可以正常運行。為了避免這種一個進程長時間占用 CPU 資源的情況出現(xiàn),提出了時間片的概念。
時間片是操作系統(tǒng)中任務調(diào)度算法的一種思想,即將 CPU 的執(zhí)行時間劃分成固定長度的時間段,每個時間段稱為一個時間片。在每個時間片內(nèi),操作系統(tǒng)將 CPU 分配給一個任務進行執(zhí)行,當時間片耗盡時,操作系統(tǒng)會中斷當前任務,并將 CPU 分配給下一個任務。時間片一般是10毫秒左右,所以在一個時間段內(nèi)所有的進程代碼都會被執(zhí)行,我們將這種情況叫做并發(fā)執(zhí)行。這種情況下會有大量的把進程放上 CPU 和從 CPU 拿下來的動作,這就叫做進程切換。
1.2 阻塞狀態(tài)詳解
最常見的阻塞狀態(tài)就是一個進程需要通過鍵盤讀取數(shù)據(jù)。當一個進程等待從鍵盤輸入的過程,此時該進程就處在阻塞狀態(tài)。鍵盤是一種硬件,在馮諾依曼結(jié)構(gòu)體系中屬于輸入設(shè)備(外設(shè)),操作系統(tǒng)對硬件資源的管理是先描述再組織,因此每一個硬件都會對應一個結(jié)構(gòu)體對象,該結(jié)構(gòu)體對象中一定會維護一個等待隊列,當一個進程需要利用該硬件資源時,進程的 PCB 對象就會被鏈入該等待隊列,此時進程就處于阻塞狀態(tài)。
小Tips:操作系統(tǒng)中的等待隊列可能有成百上千個,不僅每一種硬件有等待隊列,進程中也有等待隊列,可能會出現(xiàn)一個進程等待另一個進程結(jié)束后才能繼續(xù)運行。不同的操作系統(tǒng),調(diào)度算法也會不同。
1.3 掛起狀態(tài)詳解
在一些操作系統(tǒng)的教材上還會出現(xiàn)掛起狀態(tài)。無論是運行狀態(tài)還是阻塞狀態(tài),一個進程在沒有被 CPU 調(diào)度的情況下,它的代碼和數(shù)據(jù)是處于空閑的,即沒有被使用。之前說過一個進程在內(nèi)存中有它自己的代碼和數(shù)據(jù),還有自己的 PCB 對象,當內(nèi)存空間告急時,操作系統(tǒng)就會把這些沒有被 CPU 調(diào)度的進程的代碼和數(shù)據(jù)先放到磁盤中存儲,只留進程的 PCB 對象在隊列中排隊,這種進程就處于掛起狀態(tài)。
上面介紹的這些屬于操作系統(tǒng)學科的理論知識,不同的操作系統(tǒng)可能會有不同的實現(xiàn)方案,下面我們來深入看看具體的 Linux 操作系統(tǒng)中有哪些進程狀態(tài)。
小Tips:掛起狀態(tài)對用戶是不可見的,這是操作系統(tǒng)的一種行為。就像我們把錢存銀行里,我們并不知道銀行把我們的錢拿去干嘛了,銀行可能把我們的錢借出去了或者給員工發(fā)工資了等等,我們作為客戶不得而知,我們只知道如果存的是活期,可以隨時到銀行把錢取出來,如果存的是死期只有到期了才能取出來。
二、具體的Linux操作系統(tǒng)中的進程狀態(tài)
為了弄明白正在運行的進程是什么意思,我們需要知道進程的不同狀態(tài)。一個進程可以有幾個狀態(tài)(在 Linux 內(nèi)核里,進程有時候也被叫做任務)。
2.1 Linux內(nèi)核源代碼
/*
* 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運行狀態(tài)(running):并不意味著進程一定在運行中,它表明進程要么是在運行中要么是在運行隊列里。
-
S睡眠狀態(tài)(sleeping):意味著進程在等待事件完成(這里的睡眠有時候也叫做可中斷睡眠(interruptible sleep)),它對應操作系統(tǒng)理論中的阻塞狀態(tài)。
-
D磁盤休眠狀態(tài)(Disk sleep):有時候也叫不可中斷睡眠狀態(tài)(uninterruptible sleep),在這個狀態(tài)的進程通常會等待 IO 的結(jié)束。
-
T停止狀態(tài)(stopped):可以通過發(fā)送 SIGSTOP 信號給進程來(T)進程。這個被暫停的進程可以通過發(fā)送 SIGCONT 信號讓進程繼續(xù)運行。
-
X死亡狀態(tài)(dead):這個狀態(tài)只是一個返回狀態(tài),你不會在任務列表里看到這個狀態(tài)。
2.2 查看進程狀態(tài)
先來看看下面這段代碼執(zhí)行起來后的進程狀態(tài)。
int main()
{
while(1)
{
printf("Hello Linux\n!");
}
return 0;
}
可以看出這段代碼執(zhí)行起來后的進程狀態(tài)是 S睡眠狀態(tài),將 while 循環(huán)中的打印去掉再去執(zhí)行代碼看看進程狀態(tài)。
int main()
{
while(1);
return 0;
}
此時代碼中只剩一個 while 死循環(huán),去執(zhí)行這段代碼,進程狀態(tài)變成了 R運行狀態(tài)。為什么會出現(xiàn)在這種情況呢?原因是 CPU 的執(zhí)行速度是非??斓模谝欢未a中的 printf 是要去頻繁的訪問顯示器設(shè)備,而我們的顯示器可能并不能被該進程直接去寫入,所以該進程大部時間都在顯示器的等待隊列里等待顯示設(shè)備就緒,因此最終查出來的進程狀態(tài)是 S睡眠狀態(tài)。當我們?nèi)サ?printf 之后,該進程就不會去訪問顯示器設(shè)備,始終都在運行隊列里,所以最終查出來的進程狀態(tài)是 R運行狀態(tài)。
小Tips:查詢結(jié)果中顯示的+表示該進程在前臺運行,這意味我們此時在 bash 命令行輸指令是不會有任何反應的,可以在輸入指令的后面加上&,此時表示讓該進程在后臺運行,要終止掉該進程只能通過指令kill -9 進程PID
。
2.3 D磁盤休眠狀態(tài)(Disk sleep)
D狀態(tài)也是一種阻塞狀態(tài),在 Linux 系統(tǒng)層面我們稱作深度睡眠,S狀態(tài)稱作淺度睡眠。淺度睡眠是可以被喚醒的,即可以響應外部的變化,我們可以通過 kill 指令(其他進程)將淺度睡眠的進程終止掉。下面通過一個情景劇來給大家介紹為什么要有 D 狀態(tài),以及 D 狀態(tài)的作用。
有這樣一個場景,一個進程需要向磁盤中寫入大量數(shù)據(jù)。在正常情況下往磁盤中寫入數(shù)據(jù),進程是需要等待的,等磁盤寫完后給進程一個信號,然后進程才能繼續(xù)去運行。有一天進程A就在向磁盤中寫入大量數(shù)據(jù),磁盤在寫入的過程中,進程A就在內(nèi)存中翹著二郎腿,嗑著瓜子在等待磁盤寫完了給它發(fā)信息,此時路過的操作系統(tǒng)發(fā)現(xiàn)了進程A,它對進程A說:“我這內(nèi)存壓力都大的不行了,你小子倒好,占著內(nèi)存不干正事,還在這嗑瓜子!”。于是乎操作系統(tǒng)就將進程A kill 掉了。此時磁盤傻眼了,數(shù)據(jù)寫到一半進程沒了,因為進程沒了,所以磁盤就把寫入的數(shù)據(jù)刪除了,最終結(jié)果就是數(shù)據(jù)沒有被寫入磁盤。究竟是誰導致了這場悲劇的發(fā)生呢?于是乎法官就出來,它先審問操作系統(tǒng),進程是你 kill 掉的,你怎么解釋?操作系統(tǒng)說,我命苦呀,我只是完成了我的本職工作呀,為了給用戶提供流暢的運行環(huán)境,將一些進程 kill 掉是我的職責呀,這不是我的問題呀。接著法官又來問磁盤,數(shù)據(jù)是你丟失的,你該如何解釋?磁盤說,我祖祖輩輩都是這樣工作的呀,進程它讓我寫入數(shù)據(jù),結(jié)果自己不見了,其它磁盤遇到這種情況也是將數(shù)據(jù)丟棄掉呀,你如果判我有罪,那豈不是我的父親、母親都有罪呀。最后法官來問進程A,進程還沒等法官開口就撲通跪下說,法官大人您明察秋毫呀,我才是被 kill 掉的那個,我屬于被害人呀,我怎么會有罪呢。法官聽了一圈,感覺大家都沒罪,最終法官宣判了,你們?nèi)齻€都沒罪,是制度問題,回去我改改操作系統(tǒng),當進程在向磁盤中寫入數(shù)據(jù)的時候任何人都不能將該進程 kill 掉。于是 D 狀態(tài)就誕生了。當一個進程處于 D 狀態(tài)的時候,它不會響應任何請求,任何人和操作系統(tǒng)都不能將該進程 kill 掉。
小Tips:結(jié)束掉 D 狀態(tài)的方法有兩種,一是等待某個條件滿足,如等待數(shù)據(jù)寫完,二是直接斷電。如果被用戶查到 D 狀態(tài)的進程,那就預示著這個操作系統(tǒng)離崩潰不遠啦。所以 D 狀態(tài)會有,但是一般出現(xiàn)的時間都非常短。
2.4 T停止狀態(tài)(stopped)
在 Linux 內(nèi)核源代碼中我們可以看到連個 T 狀態(tài),一個是 T ,一個是 t,我們可以認為這兩個 T 狀態(tài)是一樣的,對于一個進程,我們可以通過下面這條指令將它設(shè)置成停止狀態(tài)。
kill -19 進程PID
可以通過下面這條指令來結(jié)束停止狀態(tài)。
kill -18 進程PID//
小Tips:結(jié)束停止狀態(tài)的進程會到后臺運行,要終止掉這個進程只能通過 kill -9
指令。T狀態(tài)和S狀態(tài)很像,其中S狀態(tài)的進程一定是在等待某種資源,而T狀態(tài)的進程可能是在等待某種資源,也可能是在被其他進程控制。我們在打斷點調(diào)試一段代碼的時候,該進程就會處于T狀態(tài)。
三、僵尸進程
一個進程在退出時并不是立即將自己所有資源全部釋放,當一個進程退出時,操作系統(tǒng)會把當前進程的各種信息維持一段時間,這個狀態(tài)就叫做 Z 僵尸狀態(tài)。維持信息是給關(guān)心它的“人”,也就是父進程來查看的。如果父進程一直沒有來關(guān)心退出的子進程,那么這個子進程將長時間處于 Z 狀態(tài)。
int main()
{
pid_t id = fork();
if(id == 0)
{
int cnt = 5;
while(cnt)
{
printf("我是子進程,PID是:%d,PPID:%d,cnt:%d\n",getpid(),getppid(),cnt);
sleep(1);
cnt--;
}
_exit(0);
}
else
{
while(1)
{
printf("我是父進程,PID是:%d,PPID:%d\n",getpid(),getppid());
sleep(1);
}
}
return 0;
}
上面這段代碼在 process 進程中通過調(diào)用 fork 接口創(chuàng)建了一個子進程,子進程在執(zhí)行完五次打印后就會被終止掉,其中的 exit 函數(shù)就是用來終止一個進程,父進程將一直運行。
子進程執(zhí)行完5次打印后就處于 Z 狀態(tài)并且后面跟了一個單詞 defunct,該單詞有死了的,不存在的意思,只不過它還再等父進程來回收它的資源。處于 Z 狀態(tài)的進程的相關(guān)資源尤其是 task_struct 結(jié)構(gòu)體不能被釋放。只有當父進程把子進程的相關(guān)資源回收后,子進程才能變成 X死亡狀態(tài)。我們將這種處于 Z 狀態(tài)的進程就叫做僵尸進程,如果父進程一直不來回收,那這種進程會長時間占用內(nèi)存資源,造成內(nèi)存泄漏。
3.1 僵尸進程危害總結(jié)
-
進程的退出狀態(tài)必須被維持下去,因為它要告訴關(guān)心它的進程(父進程),你交給我的任務,我辦的怎么樣了。可父進程如果一直不讀取,那子進程就將一直處于 Z 狀態(tài)。
-
維護退出狀態(tài)本身就是要用數(shù)據(jù)維護,也屬于進程基本信息,所以保存在 PCB 對象中,換句話說,Z狀態(tài)一直不退出,PCB一直都要維護。
-
一個父進程如果創(chuàng)建了很多的子進程,就是不回收,會造成內(nèi)存資源的浪費,因為 PCB 對象本身就要占用內(nèi)存。
-
造成內(nèi)存泄漏。
四、孤兒進程
上面我們是讓子進程先退出,父進程一直運行,接下來我們讓父進程先退出,子進程一直運行,看看會有什么結(jié)果。
int main()
{
pid_t id = fork();
if(id == 0)
{
//子進程
int cnt = 500;
while(cnt)
{
printf("我是子進程,PID是:%d,PPID:%d,cnt:%d\n",getpid(),getppid(),cnt);
sleep(1);
cnt--;
}
_exit(0);
}
else
{
//父進程
int cnt = 5;
//這里的cnt是5,意味著父進程會先執(zhí)行結(jié)束
while(cnt--)
{
printf("我是父進程,PID是:%d,PPID:%d,cnt:%d\n",getpid(),getppid(),cnt);
sleep(1);
}
}
return 0;
}
可以看到父進程在執(zhí)行結(jié)束后就只剩下子進程,為什么父進程不會處在 Z僵尸狀態(tài)呢?答案是父進程也是 bash 的子進程,父進程在執(zhí)行結(jié)束后,它的父進程 bash 會將其回收掉,并且過程非???,所以我們我們沒有看到父進程處在 Z僵尸狀態(tài)。其次我們發(fā)現(xiàn),當父進程結(jié)束后,它的子進程的父進程會變成1號進程,即操作系統(tǒng)。我們將父進程是1號進程的進程叫做孤兒進程,該進程被系統(tǒng)領(lǐng)養(yǎng)。因為孤兒進程未來也會退出,也要被釋放,所以它需要被領(lǐng)養(yǎng)。
小Tips:所有的進程只對它的“兒子”,即子進程負責,不會對它的孫子進程負責,因為代碼中只有創(chuàng)建子進程的邏輯,并沒有創(chuàng)建孫子進程的邏輯,所以并不是不想讓爺爺進程來回收孫子進程的資源,是因為爺爺進程沒有這個本事,而操作系統(tǒng)會直接從內(nèi)核層面進行回收,所以當一個進程的父進程結(jié)束后,會把該進程交給操作系統(tǒng),讓操作系統(tǒng)來充當它的父進程。
五、結(jié)語
今天的分享到這里就結(jié)束啦!如果覺得文章還不錯的話,可以三連支持一下,春人的主頁還有很多有趣的文章,歡迎小伙伴們前去點評,您的支持就是春人前進的動力!文章來源:http://www.zghlxwxcb.cn/news/detail-653687.html
文章來源地址http://www.zghlxwxcb.cn/news/detail-653687.html
到了這里,關(guān)于【Linux取經(jīng)路】探索進程狀態(tài)之僵尸進程 | 孤兒進程的文章就介紹完了。如果您還想了解更多內(nèi)容,請在右上角搜索TOY模板網(wǎng)以前的文章或繼續(xù)瀏覽下面的相關(guān)文章,希望大家以后多多支持TOY模板網(wǎng)!