最近,我發現了一個超級強大的人工智能學習網站。它以通俗易懂的方式呈現復雜的概念,而且內容風趣幽默。我覺得它對大家可能會有所幫助,所以我在此分享。點擊這里跳轉到網站。
目錄
- 一、進程狀態
- 1.1運行狀態
- 1.2阻塞狀態
- 1.3掛起狀態
- 二、具體Linux中的進程狀態
- 2.1看看Linux內核源代碼怎么說
- 2.2進程狀態查看
- D磁盤休眠狀態(Disk sleep)
- T停止狀態(stopped)
- 三、僵尸進程
- 3.1僵尸進程危害
- 四、孤兒進程
- 🍀小結🍀
🎉博客主頁:小智_x0___0x_
🎉歡迎關注:👍點贊🙌收藏??留言
🎉系列專欄:Linux入門到精通
🎉代碼倉庫:小智的代碼倉庫
一、進程狀態
進程狀態在操作系統中可以分為五大狀態,分別是:
- 新建狀態:一個進程剛被創建,把PCB和對應的代碼和數據剛申請出來,尚未進入就緒隊列
- 就緒狀態:進程已經分配到除CPU以外的所有必要資源,只要再獲得CPU,便可立即執行,進程這時的狀態稱為就緒狀態
- 阻塞狀態:也稱為等待或睡眠狀態,一個進程正在等待某一事件發生(例如請求I/O而等待I/O完成等)而暫時停止運行,這時即使把處理機分配給進程也無法運行,故稱該進程處于阻塞狀態1。處于阻塞狀態的進程也可能有多個,通常將它們排成一個隊列,稱為阻塞隊列
- 運行狀態:進程占有處理器正在運行的狀態。進程已獲得CPU,其程序正在執行1
- 終止狀態:指進程完成任務到達正常結束點,或出現無法克服的錯誤而異常終止,或被操作系統及有終止權的進程所終止時所處的狀態
1.1運行狀態
當多個進程需要運行時,它們會競爭CPU資源。在極端情況下,如果只有一個CPU,那么眾多進程就必須通過調度器來合理使用CPU資源,確保資源的均衡使用。為了管理這些進程,每個CPU都會維護一個自己的運行隊列struct runqueue。這個運行隊列是一個重要的數據結構,用于存儲當前CPU上待運行的進程。
每個進程都有一個對應的task_struct結構體,這個結構體包含了進程的各種屬性。而運行隊列中最重要的屬性是head和tail指針,它們分別指向隊列的頭部和尾部。這意味著排隊的不是進程的代碼,而是進程的PCB在排隊。
當CPU需要運行某個進程時,它會從運行隊列中挑選一個進程來執行。挑選的過程是由調度器完成的,它會根據一定的算法從隊列中選擇一個合適的進程。一旦選定了進程,調度器就會將該進程從隊列中移除,并將其放到CPU上運行。
當多個進程處于運行隊列中時,它們所處的狀態被稱為運行狀態(r狀態)。這意味著這些進程已經準備好,可以隨時被調度執行。當一個進程開始運行時,它將自己置于CPU上并執行。然而,一個進程并不需要一直執行到完成才讓出CPU。例如,如果有一個進程陷入了一個無限循環中,它可能會一直占用CPU,導致其他進程無法得到調度和執行。
為了防止這種情況的發生,操作系統引入了時間片的概念。每個進程都被分配了一個時間片,它定義了該進程在CPU上的最大執行時間。一旦一個進程的執行時間超過了它的時間片,操作系統會強制將其從CPU上移除,并將其放回運行隊列的尾部等待再次調度。
時間片的引入確保了每個進程都有機會在一段時間內得到執行,從而實現了多個進程的并發執行。并發執行是指在同一時間段內,多個進程的代碼都會被執行。通過合理分配時間片,操作系統可以確保所有進程都得到公平調度和執行,從而提高了系統的整體效率和響應速度。
1.2阻塞狀態
我們每個外設都有一個等待隊列,每個使用設備的進程,如果想要使用某個設備,就需要等待該設備變為就緒狀態。如果設備當前不可用,即處于不可讀狀態,那么進程會自動將其PCB(進程控制塊)加入到該設備的等待隊列中。一旦設備變為可用狀態,即處于可讀狀態,進程就可以從等待隊列中取出PCB,進入就緒狀態,等待CPU調度。如果設備仍然沒有準備好,進程就會在等待隊列中等待,這時我們稱進程處于阻塞狀態。因此,當我們說讓進程去某個資源中等待時,實際上是將進程加入到該資源的等待隊列中,也就是讓進程進入阻塞狀態。
系統中可能有很多阻塞的進程,每個都在等待資源或設備。進程間也可能互相等待。如果只有一個CPU,那同一時間只能運行一個進程,其他的都得等著。
1.3掛起狀態
如果有很多進程都在等一個設備,比如磁盤,但磁盤一直沒準備好,這些進程就只能在內存里等著,也不占用CPU。但有時候,操作系統的內存可能會不夠用,因為每個進程都需要占用一些內存。如果一個進程只是等著,沒有被CPU調度,那它的代碼和數據其實在內存里是空閑的。為了解決這個問題,操作系統可以把等待進程的代碼和數據移到磁盤里去,只留下一個PCB在內存里排隊。這樣,當設備準備好了,再把代碼和數據從磁盤移回內存。這個過程叫做換出和換入。當代碼和數據不在內存里時,我們稱這個進程為掛起狀態。這樣操作系統就可以空出一些內存給其他進程用。這個掛起狀態適用于所有等待的進程。
掛起狀態對用戶是不可見的,這是操作系統的一種行為。
上面介紹的這些屬于操作系統學科的理論知識,不同的操作系統可能會有不同的實現方案,下面我們來深入研究研究具體的 Linux 操作系統中有哪些進程狀態。
二、具體Linux中的進程狀態
2.1看看Linux內核源代碼怎么說
- 為了弄明白正在運行的進程是什么意思,我們需要知道進程的不同狀態。一個進程可以有幾個狀態(在Linux內核里,進程有時候也叫做任務)。
下面的狀態在kernel源代碼里定義:
/*
* 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):這個狀態只是一個返回狀態,你不會在任務列表里看到這個狀態。
2.2進程狀態查看
我們先來寫一段代碼觀察效果:
int main()
{while(1){printf("hello linux\n");}return 0;
}
可以看到這里并不是R狀態,而是S狀態,這是因為當程序瘋狂地進行打印操作時,你的設備可能并不處于可以直接寫入的狀態。因此,你的進程有很大可能性一直處于等待狀態。你可能認為你的進程正在直接向設備寫入數據,但事實上,操作系統內部,你的進程可能大部分時間都在等待I/O設備(如顯示器)就緒。因此,你的進程狀態一直顯示為S狀態,這個S狀態實際上對應著我們操作系統原理中的阻塞狀態。
我們再來把代碼修改一下去掉printf
:
int main()
{while(1);return 0;}
當去掉printf語
句后,程序就不再涉及I/O
操作,而是純粹地運行計算任務。在這種情況下,進程的狀態會變成R狀態,也就是運行狀態。這是因為沒有I/O
等待,進程可以一直占用CPU進行計算,直到任務完成。所以,當沒有I/O
操作時,進程就處于運行狀態,也就是R狀態。
查詢結果中顯示的+
表示該進程在前臺運行,這意味我們此時在 bash 命令行輸指令是不會有任何反應的,可以在輸入指令的后面加上&
,此時表示讓該進程在后臺運行,要終止掉該進程只能通過指令kill -9 PID
給進程發送終止信號。
D磁盤休眠狀態(Disk sleep)
如果一個進程處于D狀態,我們也稱之為disk sleep,在Linux系統中它也是一種阻塞狀態,只不過這種狀態在操作系統層面被稱為深度睡眠。相對應的,我們之前提到的S狀態可以被稱為淺度睡眠。就像人們睡覺時有深淺之分,淺度睡眠可以被直接喚醒,比如被程序員或其他進程喚醒。
我們再來通過一個有趣的故事來了解D狀態:
有一個進程要完成I/O操作,此時進程呢他那朝著遠端的磁盤大喊了一聲磁盤啊,我這有1GB的數據我把數據存到你存到磁盤的某個位置上,你幫我做一下吧然后呢,當磁盤聽到這句話,磁盤慢慢悠悠的探出來個腦袋,就給進程說好的,那你把數據給我吧,我去幫你寫,于是磁盤就抱著進程的數據去做寫入了,磁盤進行寫入時,這個進程它翹著二郎腿,悠哉悠哉的在這里等待,此時操作系統它從旁邊路過,操作系統現在壓力很大,不知道什么原因,反正整個計算機里,進程變得非常多了,而且每一個地方都特別吃資源,內存資源嚴重不足,當前操作系統火急火燎的在想辦法,當他從這個進程旁邊路過的時候,他瞥了一眼這個進程,覺得他很不爽,他把他能做的全做了,包括進程所對應的代碼和數據能置換全置換了,所以操作系統一氣之下啊,就對著這個進程說,你這個進程這么沒眼色,你沒看到當前內存已經被撐得快爆了嗎?我已經馬上快掛掉了,你還在這里翹著二郎腿,你還在等著呢,不要等了,所以操作系統直接把這個進程干掉了,(如果系統壓力已經在內存輾轉騰挪已經解決不了時操作系統就要開始動手殺進程了,所以操作系統是會殺掉處于某一些他自己判定不太重要的進程) 刪掉了之后,此時這個剛剛在寫入數據的磁盤也遇到了問題,當前他在寫入時發現寫到一半時突然發現磁盤空間不夠了,這個磁盤就轉過頭,探出腦袋就對這個進程說進程啊,不好意思,我寫入失敗了,唉,進程呢,進程怎么不見了?那我數據沒有寫成功該怎么辦呢我到底是再嘗試一次呢還是我再等一等,我該怎么做決策呢,(有的硬件直接丟掉的有的給你再試著寫一下,大部分都是掉直接丟掉),此時數據就被丟失了,丟失的數據非常重要。于是呢,法官就把操作系統,進程和磁盤一并帶上了法庭,法官先審問操作系統說你怎么能殺掉進程呢?操作系統卻說:“請問用戶有沒有賦予我管理他軟件資源的權利?請問我殺掉進程是不是在特別極端的情況下殺掉了?請問我有沒有履行我操作系統的職責?我的職責是保證系統不掛數據丟失和我有什么關系,我就是在做我操作系統該做的事情,如果你判我有罪了,那么請問下次如果再碰到這樣的極端情況,那到底我還做不做,我如果不殺最后導致操作系統掛掉,第一,它的數據該丟還是會丟,第二,可能還會影響其他進程,那么這個責任誰來承擔。”法官說:“唉,這話說的還挺有道理啊,是的,他只是履行了他的義務,而且他確實是在極端情況下做的這個事情”于是法官又把目光轉移到了磁盤身上,對磁盤說:“你怎么能把人家數據丟掉呢?”此時磁盤就說:“法官大人不要怪我,在這件事情上我就是個跑腿的人家讓我干啥就干啥我在寫入的時候就已經告訴了對方,我可能會失敗,我讓他去等的我要給他結果的,我的工作模式從來向來都是這樣,從我出生的時候就是這樣,其他磁盤也是這樣,如果你認為我有罪的話,那是不是我的兄弟磁盤,我的朋友磁盤也有問題,那是不是我們把儲存的邏輯全部都改下,我就是個跑腿的,我怎么能決定這個數據寫入失敗該怎么辦,數據被丟失是因為我還有其他事情要做,因為有其他進程也要讓我寫入”,法官聽了之后覺得也有道理,那么就把視角轉向了受害人進程的身上,反正操作系統沒錯,磁盤也沒錯,那就是你的問題了,在法官剛看向進程的時候,進程撲通一下就跪下來了對法官說:“法官我可是受害人啊,我是被殺掉的,我就是靜靜的坐在那里,人在家中坐,鍋從天上來,我是被殺掉的,我怎么能有錯呢啊”。法官此時一想,唉,他們三個好像說的都挺有道理,難道是我錯了嗎? (凡是有爭議的地方,那么一定是因為制度設計不合理) 法官最后說算了算了,你們三個都回去吧,我完了去改改操作系統,法官就把進程的狀態加了一個狀態,那么這個狀態就是只要這個進程當前正在有寫入任務交給了磁盤,如果磁盤沒有辦法立馬響應的話,需要進程等待這個進程絕對不能以淺度睡眠的方式運行即S狀態,必須把自己設為D狀態,從我們的源代碼方式規定,D狀態進程任何人都不能殺掉,包括操作系統 所以故事又來了,這個進程呢,又喊出來磁盤啊,磁盤說怎么又是你,進程說沒關系,這次寫吧,這次我不怪你了,所以呢,磁盤抱著數據就去進行寫入了,那么當前這個進程翹著二郎腿在這里啊,嗑著瓜子,在這里等,一個操作系統路過了操作系統說你這個進程話還沒說完,進程就亮出了一個免死金牌,一邊看去,不要影響我,我還忙著呢,你現在是沒資格殺我啊,所以操作系統一看行啊,有這回事就行反正我盡力了,人家不讓殺他就不讓殺我去那么去殺別的進程,反正對我來我做了我的工作,你沒有被殺死那后果自己承擔,所以當前進程的此時就處了一個狀態叫D狀態,當它進行等待時進程不可被殺死,這樣的話就不會存在剛剛我們的這種問題了所以當磁盤把數據寫完之后,返回來時告訴進程進程啊,你的數據寫完了這個時候得到了結果之后,進程再把自己的狀態由D狀態恢復成R狀態。
我們最后再來總結一下這個小故事:
在這個故事中,一個進程請求磁盤保存1GB數據。磁盤接受了任務,進程則等待。由于系統內存緊張,操作系統決定殺掉該進程以釋放資源。磁盤在寫入過程中發現空間不足,但進程已被殺死,數據丟失。法官審問后,決定給進程增加D狀態,保證正在進行I/O操作的進程不會被殺死。這樣,即使操作系統再次路過,也無法殺掉處于D狀態的進程,避免了數據丟失。
D狀態也是阻塞狀態的一種
T停止狀態(stopped)
T狀態稱之為暫停狀態也叫做stop狀態,我們來舉一個例子:
int main()
{while(1){printf("hello linux\n");sleep(1);}return 0;}
此時我們的進程還是S狀態.
接下來我們給進程發送信號來暫停進程kill -19 759
:
可以看到我們的進程被stop了,我們再來查一下進程狀態
此時進程就變成了T狀態,我們想讓進程再次跑起來,可以給進程發送18號信號kill -18 759
:
可以看到程序又正常跑了起來,再來查看進程狀態:
這里又變成了S狀態,但是卻少了+
這是因為我們將進程暫停啟動之后進程就會變成后臺進程。此時我們想要殺掉這個進程就只能使用kill -9
來殺死進程。
當一個進程處于T狀態一般是不會接受信號,除了特殊信號,我們使用gdb調試工具調試的時候,設置斷點,程序遇到斷點停止時候就會處于t狀態。
T
和t
的區別:
在進程狀態中,“T”代表停止狀態或常規暫停,而“t”代表追蹤停止。二者的主要區別在于,“T”狀態通常可以通過發送SIGSTOP信號給進程來使其停止,而這個被暫停的進程可以通過發送SIGCONT信號來繼續運行。“t”狀態主要發生在進程被調試過程中遇到斷點時,此時進程會進入追蹤停止狀態。
三、僵尸進程
如果今天我們有父和子兩個進程,父進程一直在運行,并且父進程不關心對應的子進程,而子進程直接退出之時,他并不是退出之后立馬要將自己的所有資源全部釋放,因為父進程沒有還沒有來關心他,那么此時操作系統就必須把子進程的狀態一直給我維持著,直到我的父進程開始關心他,那么其中我們把這種已經死掉的,但是當前需要由父進程來關心,此時這個進程所維持的這種狀態,我們稱之為Z狀態。
下面我們再來寫一段代碼驗證一下:
子進程執行完5次打印后就處于 Z 狀態,等待父進程來回收它的資源。處于 Z 狀態的進程的相關資源尤其是 task_struct 結構體不能被釋放。只有當父進程把子進程的相關資源回收后,子進程才能變成 X死亡狀態。我們將這種處于 Z 狀態的進程就叫做僵尸進程,如果父進程一直不來回收,那這種進程會長時間占用內存資源,造成內存泄漏。后面我們會介紹解決這種問題的方法(waitpid())感興趣的老鐵可以期待后續更新!
3.1僵尸進程危害
- 進程的退出狀態必須被維持下去,因為他要告訴關心它的進程(父進程),你交給我的任務,我辦的怎么樣了。可父進程如果一直不讀取,那子進程就一直處于Z狀態。
- 維護退出狀態本身就是要用數據維護,也屬于進程基本信息,所以保存在task_struct(PCB)中,換句話說,Z狀態一直不退出,PCB一直都要維護。
- 那一個父進程創建了很多子進程,就是不回收,就會造成內存資源的浪費!因為數據結構對象本身就要占用內存,想想C中定義一個結構體變量(對象),是要在內存的某個位置進行開辟空間!
- 內存泄漏
四、孤兒進程
上面的例子中我們是讓子進程先退出,父進程一直運行,接下來我們讓父進程先退出,子進程一直運行,我們再來看看會有什么結果。
#include <iostream>
#include <unistd.h>
#include <cstdlib>
using namespace std;
int main()
{pid_t id=fork();if(id==0){int cnt =500;while(cnt){printf("i an child,pid:%d,ppid:%d,cnt:%d\n",getpid(),getppid(),cnt);cnt--;sleep(1);}printf("child quit!\n");exit(0);}else{int cnt=5;while(cnt){printf("i am parent,pid:%d,ppid:%d,cnt:%d\n",getpid(),getppid(),cnt);sleep(1);cnt--;}printf("parent quit!\n");}return 0;}
我們可以看到父進程在執行結束后就只剩下子進程,為什么父進程不會處在 Z僵尸狀態呢?答案是父進程也是 bash 的子進程,父進程在執行結束后,它的父進程 bash 會將其回收掉,并且過程非常快,所以我們我們沒有看到父進程處在 Z僵尸狀態。其次我們發現,當父進程結束后,它的子進程的父進程會變成1號進程,即操作系統。我們將父進程是1號進程的進程叫做孤兒進程,該進程被系統領養。因為孤兒進程未來也會退出,也要被釋放。
🍀小結🍀
今天我們學習了"【Linux】探索Linux進程狀態 | 僵尸進程 | 孤兒進程"
相信大家看完有一定的收獲。種一棵樹的最好時間是十年前,其次是現在!
把握好當下,合理利用時間努力奮斗,相信大家一定會實現自己的目標!加油!創作不易,辛苦各位小伙伴們動動小手,三連一波💕💕~~~
,本文中也有不足之處,歡迎各位隨時私信點評指正!