Linux進程狀態(tài)和優(yōu)先級的用法及說明
1. OS教材中的進程狀態(tài)
首先我們要知道,系統(tǒng)中的進程有三個最基本也是最重要的狀態(tài):運行、阻塞、掛起。
然后要介紹一個概念,調(diào)度隊列。
1.1 調(diào)度隊列和等待隊列
操作系統(tǒng)會為 CPU 提供一個調(diào)度隊列,它是操作系統(tǒng)內(nèi)核用于管理進程調(diào)度的核心隊列結(jié)構(gòu),1里面存放的是所有已經(jīng)具備運行條件、等待 CPU 分配時間片的進程。

大概邏輯就是這樣。
在我們學(xué)校當(dāng)中的教材里面,絕大部分會將就緒態(tài)和運行態(tài)嚴格區(qū)分開來,教材中認為,就緒狀態(tài)是指進程已經(jīng)獲得除 CPU 以外的所有所需資源,完成了運行前的全部準(zhǔn)備工作,只等待操作系統(tǒng)調(diào)度器分配 CPU 時間片即可立即執(zhí)行的狀態(tài)。
處于就緒狀態(tài)的進程會被 操作系統(tǒng)放入調(diào)度隊列中,由調(diào)度器按照調(diào)度策略依次選擇上 CPU 運行。它與正在 CPU 上執(zhí)行的運行狀態(tài)的唯一區(qū)別,就是當(dāng)前是否占用 CPU;一旦調(diào)度器切換 CPU 使用權(quán)給該進程,它就從就緒狀態(tài)變?yōu)檫\行狀態(tài);如果時間片用完被換下 CPU,它又回到就緒狀態(tài)繼續(xù)排隊。
1.2 阻塞和運行狀態(tài)
但是在實際的Linux 操作系統(tǒng)中,只要進程位于 CPU 的調(diào)度隊列中,內(nèi)核就統(tǒng)一將其標(biāo)記為 TASK_RUNNING 狀態(tài),也就是我們常說的運行狀態(tài)。這里的 “運行狀態(tài)” 包含兩層含義:一種是進程正在 CPU 上真實執(zhí)行指令,另一種是進程已準(zhǔn)備就緒、在調(diào)度隊列中等待被調(diào)度器選中執(zhí)行。操作系統(tǒng)并不單獨區(qū)分 “就緒隊列” 和 “運行隊列”,凡是在調(diào)度隊列中、具備執(zhí)行條件的進程,都屬于運行狀態(tài),調(diào)度器會按照調(diào)度算法從隊列中依次挑選進程上 CPU 運行。
所以,我們就記住:進程在調(diào)度隊列里,就可以認定它處于運行狀態(tài)。
就緒狀態(tài)和運行狀態(tài)大概了解了,接下來我們講一講阻塞狀態(tài)。為了能更好的讓大家理解,我們先看一看進程處于阻塞狀態(tài)的現(xiàn)象:

對于上圖中這個代碼,我們因為寫了一個scanf,所以需要我們先從鍵盤上寫入內(nèi)容,然后再讀取才能繼續(xù)運行。同時我們查看進程,可以看到這個myprocess可執(zhí)行程序是一個正在運行的進程,所以這種一個進程卡住不動的現(xiàn)象,就叫做阻塞。
這是從看到的現(xiàn)象中去理解阻塞的意義,那么接下來我們從操作系統(tǒng)的角度再去理解。
首先,我們寫下scanf函數(shù)并執(zhí)行之后,我們計算機中的操作系統(tǒng)就在等待鍵盤上能輸入數(shù)據(jù)。而這個鍵盤屬于是硬件設(shè)備,一旦鍵盤輸入數(shù)據(jù),就表示鍵盤數(shù)據(jù)就緒,即硬件就緒。
操作系統(tǒng)作為硬件的管理者,管理的方式還是我們之前講過的:先描述,再組織。所以對于硬件來說,它也有對應(yīng)的類似于 struct task_struct 一樣的屬性信息結(jié)構(gòu)體。并且因為硬件不止一個,所以有 struct device *next 給鏈接起來,和我們在上篇文章中提到的操作系統(tǒng)底層的信息管理邏輯一樣。同時還存在一個指向進程描述符類型的指針 :struct task_struct *wait_queue。

當(dāng)我們要運行這個代碼的時候:

一遇到scanf,操作系統(tǒng)就會去向鍵盤要數(shù)據(jù),但是因為用戶還沒有通過鍵盤輸入數(shù)據(jù),所以就表示硬盤未就緒,此時就相當(dāng)于該進程正在等待,那么此時,操作系統(tǒng)就會把調(diào)度隊列中的我們本來要運行的這個進程拿出來,將這個進程的PCB中的運行狀態(tài)改為阻塞狀態(tài),再放到鍵盤設(shè)備對應(yīng)的等待隊列里。此時因為進程現(xiàn)在已經(jīng)不在調(diào)度隊列里了,那就不再參與 CPU 調(diào)度,所以才會出現(xiàn)“卡住”的樣子,直到所等待的事件發(fā)生,內(nèi)核才會將進程從等待隊列中取下,重新放回調(diào)度隊列,使其恢復(fù)運行狀態(tài):

因此我們可以做一個總結(jié):阻塞和運行的本質(zhì)區(qū)別,實際上就是看你這個進程的task_struct在誰提供的隊列當(dāng)中。
1.3掛起狀態(tài)
接著我們來談一談什么叫做掛起。

在操作系統(tǒng)同時運行大量進程的場景下,若系統(tǒng)檢測到物理內(nèi)存資源不足,為了優(yōu)化內(nèi)存使用、保證關(guān)鍵進程正常運行,操作系統(tǒng)會選擇一部分優(yōu)先級較低、暫時不急需執(zhí)行的進程,將其數(shù)據(jù)與代碼遷移到其他存儲空間,再釋放該進程占用的內(nèi)存。在硬件層面,磁盤上專門用于此類臨時存儲的區(qū)域稱為 swap 交換分區(qū),它的作用就是存放這些暫時不緊急的進程數(shù)據(jù)。
那么如何判斷哪些進程屬于 “不緊急、可遷移” 的對象呢?操作系統(tǒng)通常優(yōu)先選擇處于阻塞狀態(tài)的進程。因為阻塞進程正在等待某一事件(如 I/O 完成、鍵盤輸入、資源釋放等),短期內(nèi)無法投入運行。系統(tǒng)會將這類進程的代碼、數(shù)據(jù)等從內(nèi)存換出,保存到 swap 分區(qū),然后釋放內(nèi)存空間,供其他更活躍、更急需的進程使用。
當(dāng)阻塞進程等待的事件就緒、需要繼續(xù)執(zhí)行時,操作系統(tǒng)會將其數(shù)據(jù)從 swap 分區(qū)重新加載回內(nèi)存,使進程恢復(fù)可調(diào)度狀態(tài),再將其放入調(diào)度隊列,等待 CPU 調(diào)度執(zhí)行。
因此,掛起狀態(tài)可以這樣定義:當(dāng) “進程的代碼、數(shù)據(jù)等內(nèi)容被暫時換出內(nèi)存、保存到磁盤的 swap 分區(qū),僅在內(nèi)存中保留進程控制塊 task_struct 以標(biāo)識進程存在” 時,該進程所處的狀態(tài)即為掛起狀態(tài)。處于掛起狀態(tài)的進程既不占用實際內(nèi)存空間,也不參與 CPU 調(diào)度,必須先被重新調(diào)入內(nèi)存,才能繼續(xù)等待事件或參與調(diào)度。
其中,如果進程原本處于阻塞狀態(tài),在內(nèi)存不足時被 操作系統(tǒng)換出到 swap 分區(qū),進入掛起,這叫做阻塞掛起;如果進程原本已經(jīng)具備運行條件,在調(diào)度隊列當(dāng)中,處于運行狀態(tài),只是因為內(nèi)存緊張而被換出到 swap 分區(qū)(這種是因為已經(jīng)將阻塞狀態(tài)的進程的數(shù)據(jù)放到swap分區(qū)后,內(nèi)存空間依然不足),那就叫做運行掛起。
關(guān)于 swap 交換分區(qū)我們還需要再詳細介紹一下:
它是操作系統(tǒng)在磁盤上劃分的一塊專用存儲空間,主要用作虛擬內(nèi)存,用來緩解物理內(nèi)存不足的問題,其空間大小一般為 內(nèi)存空間大小的 1~2 倍。當(dāng)系統(tǒng)同時運行大量進程,導(dǎo)致物理內(nèi)存資源緊張時,內(nèi)核會將部分暫時不活躍的進程數(shù)據(jù)從內(nèi)存轉(zhuǎn)移到 Swap 分區(qū),從而騰出寶貴的物理內(nèi)存供當(dāng)前急需運行的程序使用。因此,Swap 本質(zhì)上是用磁盤空間擴展內(nèi)存容量的一種機制。
將數(shù)據(jù)從內(nèi)存寫入 Swap 分區(qū)的過程,稱為 swap out。當(dāng)掛起進程等待的事件就緒、需要繼續(xù)執(zhí)行時,操作系統(tǒng)會將之前存放在 Swap 分區(qū)中的代碼和數(shù)據(jù)重新讀回物理內(nèi)存,并恢復(fù)其運行環(huán)境。這個從 Swap 分區(qū)讀回內(nèi)存的過程,稱為 swap in。
簡單來說,Swap 分區(qū)相當(dāng)于內(nèi)存的 “后備倉庫”,swap out 負責(zé)騰出內(nèi)存,swap in 負責(zé)恢復(fù)進程運行。借助這一機制,操作系統(tǒng)既可以在內(nèi)存緊張時保證系統(tǒng)穩(wěn)定運行,又能實現(xiàn)進程掛起與喚醒,提升整體資源利用率。不過,swap交換分區(qū)也有一個缺點,因為 Swap 在磁盤上,速度遠慢于內(nèi)存,頻繁 swap 會導(dǎo)致系統(tǒng)卡頓。
2. 具體OS中的狀態(tài)
我們上面提到的阻塞、運行、掛起,都只是在簡單介紹它們的概念,僅僅停留在基礎(chǔ)知識,屬于是通用操作系統(tǒng)理論。但是在實際的Linux操作系統(tǒng)當(dāng)中,這三種狀態(tài)都是怎么被標(biāo)識的,到底長什么樣子,我們還不了解。
/*
*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 */
};這是Linux操作系統(tǒng)中關(guān)于進程狀態(tài)的數(shù)組,里面存儲了進程的不同狀態(tài)。
- R 運行狀態(tài)(running): 并不意味著進程一定在運行中,它表明進程要么是在運行中要么在運行隊列里。
- S 睡眠狀態(tài)(sleeping): 意味著進程在等待事件完成(這里的睡眠有時候也叫做可中斷睡眠(interruptible sleep))。
- D 磁盤休眠狀態(tài)(Disk sleep)有時候也叫不可中斷睡眠狀態(tài)(uninterruptible sleep),在這個狀態(tài)的進程通常會等待 IO 的結(jié)束。
- T 停止?fàn)顟B(tài)(stopped): 可以通過發(fā)送 SIGSTOP 信號給進程來停止(T)進程。這個被暫停的進程可以通過發(fā)送 SIGCONT 信號讓進程繼續(xù)運行。
- X 死亡狀態(tài)(dead): 這個狀態(tài)只是一個返回狀態(tài),你不會在任務(wù)列表里看到這個狀態(tài)。
- t跟蹤停止?fàn)顟B(tài)(tracing stop)對應(yīng)內(nèi)核的
TASK_TRACED狀態(tài)。它是 T 停止?fàn)顟B(tài)的特殊變體,專門用于調(diào)試場景。 - Z僵尸狀態(tài)(zombie)對應(yīng)內(nèi)核的
EXIT_ZOMBIE狀態(tài)。當(dāng)子進程已經(jīng)退出,但父進程尚未通過wait()/waitpid()回收其退出狀態(tài)時,子進程就會進入僵尸狀態(tài)。
并且每一個狀態(tài)都有自己對應(yīng)的標(biāo)識符,標(biāo)識符以數(shù)字的形式表示。比如R狀態(tài)就用 0 代表,S 狀態(tài)就用 1 代表。
3. 查看狀態(tài)
3.1 S 可中斷睡眠狀態(tài)(sleeping)
我們首先創(chuàng)建一個文件myprocess,然后寫入代碼:

編譯運行之后,我們來查看它的狀態(tài):

會發(fā)現(xiàn)這個進程的狀態(tài)是 S+ ,+ 到底是什么意思我們先不管,我們先看 S ,這代表這個進程目前處于睡眠狀態(tài)。但是我們的程序一直在運行???一直在執(zhí)行while循環(huán)里面的printf語句,持續(xù)的在屏幕中打印我們所定的內(nèi)容,可為什么這個進程的狀態(tài)還是休眠呢,它不應(yīng)該是運行嗎?
我們再將 myprocess.c 文件中的代碼修改一下:


我們發(fā)現(xiàn)當(dāng)程序持續(xù)執(zhí)行while空語句的時候,這個進程的狀態(tài)從原來的睡眠狀態(tài)變成了運行狀態(tài)。
這是因為,在 Linux 操作系統(tǒng)中,進程狀態(tài)會根據(jù)自身執(zhí)行行為發(fā)生明確變化。保留 printf 與注釋 printf 會讓進程呈現(xiàn)完全不同的狀態(tài),其核心原因在于進程是否需要等待事件、是否主動放棄 CPU。
當(dāng)程序中保留 printf 與 sleep 語句時,進程會在循環(huán)中執(zhí)行輸出操作并主動等待。printf 屬于典型的輸出 I/O 操作,即進程通過系統(tǒng)調(diào)用向終端設(shè)備寫入數(shù)據(jù),由設(shè)備驅(qū)動最終刷新顯示到屏幕;而 sleep 則讓進程主動等待指定時長。
由于 I/O 設(shè)備的處理速度遠慢于 CPU,進程在執(zhí)行 printf 時無法立即完成操作,必須等待設(shè)備響應(yīng)。此時內(nèi)核會將進程從運行隊列移出,掛入對應(yīng)的等待隊列,使其主動讓出 CPU,進入 Linux 下的可中斷睡眠狀態(tài)(S),對應(yīng)操作系統(tǒng)理論中的阻塞狀態(tài)。直到 I/O 完成或定時時間到達,進程才會被喚醒并重新放回運行隊列繼續(xù)執(zhí)行。因此在未注釋 printf 時,進程絕大多數(shù)時間都處于等待狀態(tài),狀態(tài)標(biāo)識為 S。
當(dāng)程序注釋掉 printf 與 sleep 語句后,進程僅執(zhí)行一個無限空循環(huán)。該循環(huán)不涉及任何 I/O 操作,不等待任何外部事件,也不會主動放棄 CPU。此時進程始終具備執(zhí)行條件,要么正在 CPU 上運行,要么時間片用完后回到運行隊列等待再次調(diào)度。這兩種情況在 Linux 中統(tǒng)一表示為運行 / 就緒狀態(tài)(R)。因此注釋 printf 后,進程會持續(xù)處于 R 狀態(tài),不會進入阻塞。
簡單來說,是否出現(xiàn)等待行為,是決定進程狀態(tài)為 R 還是 S 的根本原因:有等待則進入阻塞(S),無等待則保持運行 / 就緒(R)。
那既然進程處于 S 可中斷睡眠狀態(tài),我們之前講過可以使用 Ctrl + C 的鍵盤操作去中斷這個進程,現(xiàn)在,我們還可以使用 kill 命令:

使用 kill -9 PID 就可以強制殺掉這個正在運行或者卡住的進程,其中 -9 是 Linux 系統(tǒng)中的信號編號,代表系統(tǒng)預(yù)定義的 SIGKILL(強制終止信號)。
Linux 操作系統(tǒng)通過信號來控制進程的行為,不同的數(shù)字對應(yīng)不同的指令:

-9 是最強制、最高優(yōu)先級的終止信號,內(nèi)核會立即殺死指定進程,不允許進程忽略、阻塞或處理,進程無法做任何清理操作,直接被系統(tǒng)強制結(jié)束。
普通的 kill PID(不帶 -9)默認使用 -15(SIGTERM),屬于溫和的退出信號,允許進程保存數(shù)據(jù)、釋放資源后正常關(guān)閉。
接著我們來講一下S+的 + 的含義:

首先我們寫出這樣一段代碼,執(zhí)行一個死循環(huán),一直睡眠 1 秒,然后去查看進程:

大家會發(fā)現(xiàn),此時我的進程處于 S+ 狀態(tài),當(dāng)我再次輸入各種指令的時候,我的Shell不做任何反應(yīng),輸入什么內(nèi)容都沒有用。其實這是因為+ 作為狀態(tài)修飾符,表明該進程是前臺進程組的成員,Linux 系統(tǒng)默認就是讓進程在前臺運行,這是系統(tǒng)的標(biāo)準(zhǔn)行為。也就是說,./myprocess 這一進程是一個前臺進程,前臺進程會占用當(dāng)前終端,可直接與用戶交互,且能接收 Ctrl+C、Ctrl+Z 等鍵盤信號,但是終端會被其占用,無法在同一窗口輸入新命令。
如果我不想讓我的進程處于前臺進程,那么可以手動的加上這一符號:

此時的狀態(tài)變成了 S :
![]()
所以,在命令末尾添加符號 &,表示將進程放到后臺運行。此時進程狀態(tài)從 S+ 變?yōu)?S,+ 符號消失,意味著進程不再占用當(dāng)前終端,用戶可以繼續(xù)在終端輸入其他指令,且無法通過 Ctrl + C 直接終止該進程。只能通過kill指令去刪除進程。
3.2 D 不可中斷睡眠狀態(tài)(disk sleep)
D狀態(tài)也被稱為磁盤睡眠(Disk Sleep),是 Linux 中一種特殊的阻塞狀態(tài)。
當(dāng)進程需要與硬件設(shè)備進行關(guān)鍵、不可打斷的數(shù)據(jù)交互時,就會進入 D 狀態(tài)。最典型的場景是進程訪問磁盤、讀寫文件,此時進程會向磁盤設(shè)備發(fā)起 I/O 請求。由于磁盤 I/O 速度遠慢于 CPU,進程必須等待硬件完成操作,不能繼續(xù)執(zhí)行。
為了保證數(shù)據(jù)安全與設(shè)備狀態(tài)穩(wěn)定,內(nèi)核不允許這種等待被信號打斷。如果進程在磁盤讀寫中途被強制終止,可能導(dǎo)致數(shù)據(jù)不完整、文件系統(tǒng)損壞或設(shè)備狀態(tài)異常。因此,內(nèi)核會將進程標(biāo)記為不可中斷睡眠,讓它安靜等待 I/O 結(jié)束,期間不響應(yīng)任何普通信號。
處于D狀態(tài)的進程其實會有一定的風(fēng)險。與常見的 S 狀態(tài)(可中斷睡眠)不同,處于 D 狀態(tài)的進程無法被 Ctrl+C 終止,也無法被 kill 命令殺死,即使是 kill -9 也無效。它只能等待 I/O 完成、硬件返回結(jié)果后,由內(nèi)核自動喚醒,回到 R 就緒狀態(tài)繼續(xù)執(zhí)行。
所以,大量長期處于 D 狀態(tài)的進程會占用系統(tǒng)資源與進程描述符,導(dǎo)致系統(tǒng)可用進程數(shù)量減少,嚴重時會影響新程序啟動,甚至引發(fā)系統(tǒng)服務(wù)異常。更關(guān)鍵的是,D 狀態(tài)通常意味著 I/O 瓶頸。持續(xù)出現(xiàn) D 狀態(tài)說明磁盤或硬件設(shè)備響應(yīng)緩慢,也就是磁盤或者硬件設(shè)備老化了。會導(dǎo)致整個系統(tǒng) I/O 性能下降,文件讀寫卡頓、系統(tǒng)負載升高,用戶會明顯感覺到操作延遲、程序響應(yīng)變慢。
在極端情況下,如果磁盤故障或設(shè)備斷開導(dǎo)致 I/O 永遠無法完成,相關(guān)進程會永久卡在 D 狀態(tài),形成不可殺死的 “僵尸化” 進程,只有重啟系統(tǒng)才能徹底清除,嚴重影響系統(tǒng)穩(wěn)定性與可用性。
不過一般正常情況下,磁盤 I/O 很快,D 狀態(tài)只會短暫出現(xiàn),用戶幾乎感知不到。但如果磁盤繁忙、I/O 阻塞或設(shè)備異常,進程會長時間停留在 D 狀態(tài),表現(xiàn)為系統(tǒng)卡頓、進程無法殺死,這也是判斷 I/O 瓶頸的重要標(biāo)志。
由于D狀態(tài)一般不會出現(xiàn)或者說很難捕捉到D狀態(tài),在這就不做實際演示。感興趣的可以去大模型搜索一下如何在命令行輸入指令模擬D狀態(tài)進程的出現(xiàn),友情提示:這個實驗可能會導(dǎo)致你的操作系統(tǒng)崩潰,需要重裝。
3.3T 停止?fàn)顟B(tài)(stopped)
T 狀態(tài)即停止?fàn)顟B(tài),表示進程被暫停執(zhí)行,不再參與 CPU 調(diào)度,但進程的內(nèi)存、打開的文件等資源依然保留,隨時可以恢復(fù)運行。
進程進入 T 狀態(tài)通常是收到了暫停類信號,最常見的是用戶在終端按下 Ctrl+Z,或是系統(tǒng)調(diào)用 kill -19 PID 發(fā)送 SIGSTOP 信號。與 S 狀態(tài)、D 狀態(tài)等待 I/O 或事件不同,T 狀態(tài)下的進程并非在等待外部條件,而是被強制凍結(jié),內(nèi)核不會再為其分配時間片。
比如我們還是使用這段代碼:

運行之后,我們對其使用 kill -19 PID 的指令:

就會發(fā)現(xiàn)原來的進程從S+的狀態(tài)變?yōu)榱薚狀態(tài)。
處于 T 狀態(tài)的進程既不運行也不退出,只是暫時 “掛起”。它可以被 kill -18 PID(對應(yīng) SIGCONT 信號)喚醒,恢復(fù)到之前的 R 或 S 狀態(tài)繼續(xù)執(zhí)行。這種狀態(tài)常用于調(diào)試程序、臨時暫停任務(wù),是操作系統(tǒng)對進程精細控制的一種體現(xiàn)。

在前面講解可中斷睡眠狀態(tài)的時候,我們提到了前臺進程和后臺進程,經(jīng)過講解我們可以總結(jié)成一句話:前臺進程和后臺進程最大的區(qū)別就是,是否能接收鍵盤輸入。前臺進程占用終端,能夠獲取用戶的鍵盤操作與信號;后臺進程脫離終端控制,無法接收鍵盤輸入,用戶的按鍵操作不會傳遞給該進程。
如果現(xiàn)在有一個進程就是后臺進程,但是這個進程還需要從鍵盤中獲取輸入,操作系統(tǒng)就會把它設(shè)置為T停止?fàn)顟B(tài),就像這種場景:

這段代碼里調(diào)用了scanf函數(shù),所以需要從鍵盤中獲取輸入。

我們手動設(shè)置進程后臺運行,此時該進程的狀態(tài)就被設(shè)置成了 T 。
3.4t跟蹤停止?fàn)顟B(tài)(tracing stop)
這個狀態(tài)對應(yīng)內(nèi)核的 TASK_TRACED 狀態(tài)。它是 T 停止?fàn)顟B(tài)的特殊變體,專門用于調(diào)試場景。當(dāng)進程被調(diào)試器(如 gdb) attach 或設(shè)置斷點時,會收到跟蹤信號并進入此狀態(tài)。此時進程處于暫停執(zhí)行,等待調(diào)試器指令(繼續(xù)、單步、斷點恢復(fù)等)。它不僅能被 SIGCONT(kill -18)恢復(fù),還能由調(diào)試器主動控制,是系統(tǒng)調(diào)試機制的核心狀態(tài)之一。

我們剛開始調(diào)試這段代碼的時候,先給代碼打上一個斷點,然后執(zhí)行運行指令。在我們執(zhí)行運行指令之前這段程序是沒有被運行的,所以我們看到進程里面,只有GDP調(diào)試器調(diào)用的一個myprocess的進程。當(dāng)我們運行之后由調(diào)試器創(chuàng)建了一個子進程,讓這個程序得以運行。
這個得以運行的程序的狀態(tài)就是 t 跟蹤停止?fàn)顟B(tài)。這就相當(dāng)于父進程GDP先創(chuàng)建了一個子進程,遇到斷點的時候,向子進程發(fā)起了kill 19號信號。
3.5 X死亡狀態(tài) 和 Z 僵尸狀態(tài)
為了更好的讓大家能理解,我們來講個故事:
你叫小明,是一個朝氣蓬勃的大學(xué)生,非常喜歡跑步。有一天早上六點鐘,你正沿著路上在慢跑,突然從你的后面竄出來一個八十歲的老大爺,跑的飛快宛如風(fēng)馳電掣。當(dāng)他跑到你前面一段距離的時候,突然嘎嘣一下倒在地上站不起來了。作為一個有社會道德的有為青年,你當(dāng)機立斷打了急救電話和報警電話,老大爺一個人直的不能再直的躺在地上等著,過了一段時間后警察和急救人員都趕到了。
當(dāng)趕到的時候,警察和急救人員并不是馬上就把老大爺給抬走,而是先就地檢查老大爺?shù)臓顩r,醫(yī)護人員判斷老大爺是否已經(jīng)死亡,警察人員判斷老大爺?shù)乃劳鲈虻降资鞘裁础5葯z查和判斷全部完畢之后,醫(yī)護人員才會把老大爺拉走,警察人員才會疏散現(xiàn)場。
在上面這段虛構(gòu)的故事當(dāng)中的老大爺就相當(dāng)于是一個進程,在他躺在地上的時候,就相當(dāng)于這個進程已經(jīng)結(jié)束。但是老大爺?shù)降子袥]有死亡,還不得而知,需要醫(yī)護人員來判斷。對于進程而言,當(dāng)這個進程已經(jīng)結(jié)束,但是還沒有人來檢查進程的信息的時候,在這段期間,該進程的狀態(tài)就叫做:僵尸狀態(tài)。當(dāng)信息檢查完畢,確認該進程確實真的已經(jīng)結(jié)束了,才會宣判這個進程的狀態(tài)為:死亡狀態(tài)。
不過要注意的是,死亡狀態(tài)是一個瞬時狀態(tài),至少以我們當(dāng)前的知識儲備,是沒有直接觀察到進程處于死亡狀態(tài)的時候的。所以大家只要先了解死亡狀態(tài)的概念:死亡狀態(tài)是進程徹底退出、所有資源被內(nèi)核完全回收后的最終狀態(tài),進程會從系統(tǒng)中徹底消失,不再留下任何信息。
我們主要要講解的是:僵尸狀態(tài)。
是 Linux 操作系統(tǒng)中進程生命周期結(jié)束后的一種特殊中間狀態(tài)。當(dāng)一個子進程執(zhí)行完畢、通過exit()系統(tǒng)調(diào)用正常退出,或因信號異常終止時,其用戶態(tài)內(nèi)存、打開的文件描述符、堆棧等運行時資源會被內(nèi)核立即釋放,但為了向父進程提供該進程的退出狀態(tài)碼、終止信號等退出信息,內(nèi)核會保留該進程的進程控制塊(PCB/task_struct)與進程號(PID),使其暫時留在進程列表中。此時進程已不再執(zhí)行任何代碼、不占用 CPU 與內(nèi)存資源,卻仍以進程形式存在,這種已終止運行但退出信息未被父進程回收的狀態(tài),即為僵尸狀態(tài),處于該狀態(tài)的進程稱為僵尸進程。
用一句話簡單理解就是:當(dāng)子進程已經(jīng)執(zhí)行完畢退出或被意外終止,而父進程仍在運行且未及時回收子進程的退出信息時,子進程就會處于僵尸狀態(tài)。
比如說對于這段代碼:


我們創(chuàng)建一個子進程之后,讓父子進程單獨執(zhí)行自己的語句,子進程只執(zhí)行一次printf,但父進程要持續(xù)執(zhí)行printf。當(dāng)子進程的語句已經(jīng)執(zhí)行完畢的時候,父進程依然在執(zhí)行語句。此時子進程雖然已經(jīng)結(jié)束退出了,但退出信息沒有被父進程回收。那么子進程的狀態(tài)就會變成僵尸狀態(tài)。
到這里,對于僵尸進程我們只要有四個問題:
- 進程退出了,退出信息是什么?
- 進程退出了,退出信息保存在哪里?
- 檢測 Z 狀態(tài)進程,回收 Z 狀態(tài)進程,本質(zhì)是在做什么?
- 具體怎么回收?誰來回收?
其中第四個問題因為涉及到進程控制的問題,所以我們只是簡單提一下,后續(xù)文章中會講解。
首先第一個和第二個問題:
進程的退出信息(Exit Status)是一個整數(shù)數(shù)值,通常包含兩部分核心內(nèi)容:
1. 退出狀態(tài)碼(Exit Code):進程正常結(jié)束時返回的值(如 main 函數(shù)的返回值,通常 0 表示成功,非 0 表示錯誤類型)。
2. 終止原因(Termination Reason):進程是如何結(jié)束的。包括:
正常退出(Normal Exit):如 return 0、exit(1)。
異常退出(Abnormal Exit):如被信號殺死(Segmentation Fault 段錯誤、SIGKILL 強制終止等),此時會記錄導(dǎo)致退出的信號編號
退出信息是被存儲在 當(dāng)前子進程自己的task_struct 結(jié)構(gòu)體里,也就是說子進程雖然結(jié)束了,但是它的 task_struct 是一直保留的,這也是我們能在狀態(tài)查看是看到這個子進程的PID的原因。之所以存儲 task_struct,這是為了:父進程未來通過 wait() 或 waitpid() 系統(tǒng)調(diào)用時,內(nèi)核會從這里讀取數(shù)據(jù)并復(fù)制給父進程。

exit_state就代表退出時的狀態(tài),exit_code 就是退出編碼。
第三個問題:
本質(zhì)是在 “讀取數(shù)據(jù)” 與 “釋放占位”。
讀取數(shù)據(jù)(讀操作):父進程通過調(diào)用 wait()/waitpid(),主動去讀取子進程 task_struct 中遺留的退出信息(退出碼 / 信號)。這是父子進程間最后的通信儀式,父進程必須確認子進程是 “成功了” 還是 “出錯了”。
釋放資源(清理操作):當(dāng)讀取完成后,內(nèi)核確認不再需要該進程的任何信息,才會徹底釋放子進程占用的 PID(進程號) 和 task_struct 結(jié)構(gòu)。
第四個問題:
誰來回收?
主動回收:由父進程主動調(diào)用 wait() 或 waitpid() 函數(shù)。
被動 / 繼承回收:如果父進程先于子進程退出,子進程會被托管給 init 進程(PID 1) 或系統(tǒng)守護進程,由它們自動循環(huán)調(diào)用 wait() 來回收所有孤兒進程的僵尸狀態(tài)。
具體怎么回收?
阻塞等待:父進程調(diào)用 wait(),如果子進程還在運行,父進程會進入阻塞狀態(tài)(S),直到子進程退出。
獲取狀態(tài):子進程退出后,內(nèi)核將退出信息填入 wait() 的參數(shù)中,父進程讀取。
徹底銷毀:內(nèi)核檢測到所有進程引用都已解除,釋放僵尸進程的資源,使其變?yōu)?X 狀態(tài)并消失。
以上問題回答中的 wait () ,實際上是一個系統(tǒng)調(diào)用,叫做等待函數(shù),后續(xù)我們會提到,大家暫時只要知道有這么個東西就行。
最后還有一個問題需要大家理解:如果說父進程一直不回收子進程會怎么樣?
首先一定會出現(xiàn)的情況就是:子進程的PID也沒有被回收,如果說創(chuàng)建的進程太多,PID不夠用了,就會導(dǎo)致進程創(chuàng)建出現(xiàn)問題;另外:子進程的task_struct一直沒有被釋放,會占用內(nèi)存空間資源,這就是內(nèi)存泄漏的問題。
那么就有一個衍生的問題:如果一個對應(yīng)的進程已經(jīng)結(jié)束了,內(nèi)存泄露的問題還會存在嗎?為了讓大家能更好的理解,我說一個場景:當(dāng)你打開一個游戲正在愉快的玩耍的時候,同時你的后臺又在掛著網(wǎng)易云音樂聽歌,同時你的后臺還在下載電影,對于部分手機或者電腦來說,你就會發(fā)現(xiàn)你在玩游戲的過程當(dāng)中非常的卡頓,這就是內(nèi)存空間不足導(dǎo)致的問題。當(dāng)你把后臺下載電影的軟件關(guān)掉,音樂軟件也關(guān)掉,就會發(fā)現(xiàn)打游戲會變得無比絲滑順暢。這就相當(dāng)于釋放內(nèi)存,清理內(nèi)存空間的操作。
所以我們可以得到結(jié)論:如果一個進程已經(jīng)結(jié)束了,那么該進程導(dǎo)致的內(nèi)存泄漏問題就會消失。
所以對于我們平時使用的絕大部分軟件,除非當(dāng)我們點擊退出,否則這個軟件程序就會一直運行,其本質(zhì)上就是這個軟件在進行死循環(huán),屬于內(nèi)存泄漏問題。像這種一直運行的進程,就叫做常駐進程。
3.6 孤兒進程
我們前面提到了,如果子進程運行已經(jīng)結(jié)束或被信號等其他指令殺死,且父進程還沒有回收子進程的退出信息,那么子進程的狀態(tài)就會被判定為僵尸狀態(tài)。就相當(dāng)于,父進程還在運行,但子進程先退出了。那如果是父進程先退出了,子進程還在運行,那么會是什么情況呢?
這就涉及到一個進程概念:孤兒進程。
孤兒進程是指在程序運行過程中,父進程先于子進程退出,而子進程仍然繼續(xù)執(zhí)行所形成的特殊進程。當(dāng)父進程正常結(jié)束或異常終止后,原本由其管理的子進程失去了原有的父進程,便成為孤兒進程。
我們以這段代碼為例:

我們使用這一行指令來控制持續(xù)查看進程:
![]()

大家會發(fā)現(xiàn),當(dāng)子進程一直在進行的時候父進程已經(jīng)結(jié)束了,于是在下一次執(zhí)行程序的時候,子進程的PPID就由原來的5568變成了 1 ,而 1 就代表操作系統(tǒng)的初始進程,即操作系統(tǒng)內(nèi)核 PID=1的進程。所以此時該子進程就變成了一個孤兒進程,并且交給了 PID = 1 的進程,即操作系統(tǒng)內(nèi)核托管。
之所以會被回收托管,是 為了保證系統(tǒng)資源能夠正?;厥眨琇inux 內(nèi)核會自動將這類孤兒進程 “過繼” 給系統(tǒng)中的 init 進程(PID 為 1)或 systemd 進程托管,由其負責(zé)后續(xù)的資源回收工作,因此孤兒進程在退出時會被及時清理,不會像僵尸進程那樣長期占用系統(tǒng)資源。
同時,我們發(fā)現(xiàn)子進程的進程狀態(tài)由原來的 S+ 變成了 S ,也就是從前臺進程變成了后臺進程,所以當(dāng)我們想使用 Ctrl+C 的操作去關(guān)閉進程的時候,是關(guān)閉不了的,只有通過kill指令才能關(guān)閉。這是因為:S+ 中的 + 代表進程屬于前臺終端進程組,與控制終端關(guān)聯(lián)。當(dāng)父進程(終端 Shell)退出后,孤兒進程被內(nèi)核重新托管給 init 進程,并脫離原終端,不再屬于前臺進程組,因此 + 符號消失,狀態(tài)變?yōu)?S。進程本身仍處于可中斷睡眠狀態(tài),并未發(fā)生本質(zhì)改變。
4. 進程優(yōu)先級基本概念
進程優(yōu)先級是操作系統(tǒng)用于決定多個進程誰先獲得 CPU 資源、誰后執(zhí)行的調(diào)度依據(jù),它決定了進程在競爭處理器時的 “重要程度” 和 “排隊順序”。優(yōu)先級越高的進程,在系統(tǒng)資源緊張時越容易被調(diào)度器優(yōu)先選中運行;優(yōu)先級越低,則越容易被高優(yōu)先級進程 “搶占”,等待時間更長。
5.查看系統(tǒng)進程
我們前面學(xué)習(xí)過 ps axj 指令,可以查看到進程的PID和其他的詳細信息,現(xiàn)在我們來學(xué)習(xí)ps -l 指令:

我們先執(zhí)行了一個程序 peocessTest,然后查看它的進程信息。首先我們來看UID,這代表啟動這個程序的用戶的名稱,因為對于操作系統(tǒng)來說,它會把我們的用戶名轉(zhuǎn)化成一個編號以用來區(qū)分是誰運行了哪些程序。比如我的用戶名 chen ,在操作系統(tǒng)中的編號就是 1001 :

其次,進程信息中的 PRI 和 NI 就代表優(yōu)先級信息。
6. PRI 和 NI
PRI 是進程的實際優(yōu)先級(Priority),由操作系統(tǒng)內(nèi)核動態(tài)計算和調(diào)度,數(shù)值越小表示進程優(yōu)先級越高,越容易被 CPU 優(yōu)先執(zhí)行。
NI 即 nice 值,是用戶可以手動設(shè)置的優(yōu)先級修正值,用于影響最終的 PRI。之所以叫 nice ,是因為全稱是 nice value,其中的nice的中文意思是:(調(diào)度器)偏好 / 友好程度。
nice 越低(越嚴格):進程的優(yōu)先級越高,系統(tǒng)會優(yōu)先照顧它(比如實時任務(wù)、關(guān)鍵服務(wù))。
nice 越高(越客氣):進程的優(yōu)先級越低,系統(tǒng)會盡量少占用它的資源(比如后臺任務(wù)、非關(guān)鍵進程)。
在 Linux 中,nice 值范圍通常為 -20 ~ 19,NI 越小,優(yōu)先級越高;NI 越大,優(yōu)先級越低。普通進程的 PRI 一般會在默認基礎(chǔ)上加上 nice 值得到最終優(yōu)先級,用戶通過調(diào)整 NI 可以間接控制進程優(yōu)先級,普通用戶只能調(diào)高 NI(降低優(yōu)先級),只有 root 權(quán)限才能降低 NI(提升優(yōu)先級)。
系統(tǒng)調(diào)度器依據(jù) PRI 分配 CPU 時間片,從而保證重要進程更及時地運行。
手動修改 NI 值的方法就是調(diào)用 top 命令:

當(dāng)我們輸入top指令的時候,會彈出來當(dāng)前操作系統(tǒng)內(nèi)所有正在運行的進程。然后我們再按一下R鍵,系統(tǒng)會提醒我們要把哪個進程的 NI 值進行修改,接著輸入進程的 PID ,然后再輸入你想把該進程的優(yōu)先級改成多少:

比如我們把 NI 改成 10 ,那PRI就會變成 90 。
所以這里優(yōu)先級的改變有一個公式:PRI(new) = PRI(old) + NI ;其中這里的 PRI(old)指的是進程的初始的PRI。
總結(jié)
以上為個人經(jīng)驗,希望能給大家一個參考,也希望大家多多支持腳本之家。
相關(guān)文章
apache啟動報錯:the requested operation has failed解決辦法
apache啟動報錯:the requested operation has failed解決辦法2009-11-11
禁止網(wǎng)站顯示文件目錄列表的2個方法(htaccess)
這篇文章主要介紹了禁止網(wǎng)站顯示文件目錄列表的2個方法,需要的朋友可以參考下2016-04-04
查看linux文件系統(tǒng)塊大小的實現(xiàn)方法
下面小編就為大家?guī)硪黄榭磍inux文件系統(tǒng)塊大小的實現(xiàn)方法。小編覺得挺不錯的,現(xiàn)在就分享給大家,也給大家做個參考。一起跟隨小編過來看看吧2016-11-11

