文章目錄
- 引言
- 一、進程基礎:概念與核心數據結構
- 1.1 進程的本質:程序的動態化身
- 1.2 進程控制塊(PCB):內核管理的靈魂
- 1.2.1 鏈表節點嵌入
- 1.2.2 鏈表操作宏
- 1.2.3 全局鏈表管理
- 1.3 進程查看與系統調用
- 1.3.1 通過系統調用獲取進程標識符
- 1.3.2 通過系統調用創建子進程
- 二、進程生命周期:狀態與特殊場景
- 2.1 進程的幾個狀態
- 2.2 進程狀態查看
- 2.3 僵尸進程與孤兒進程:資源管理的兩面性
- 2.3.1 僵尸進程
- a. 什么是僵尸進程?
- b. 僵尸進程的成因
- c. 僵尸進程的危害
- d. 如何識別僵尸進程?
- 2.3.2 孤兒進程
- a. 什么是孤兒進程?
- b. 孤兒進程與僵尸進程的對比
- c. 如何識別孤兒進程?
- 三、進程優先級與調度策略
- 3.1 基本概念
- 3.2 優先級體系:PRI 與 NI 的協同
- 3.3 調度的核心矛盾:競爭、獨立、并行與并發
- 四、進程切換:內核級上下文管理
- 4.1 進程切換的核心步驟
- 4.2 切換背后的 “原子操作”
- 4.3 進程切換的開銷
- 五、Linux 2.6 內核調度隊列:O (1) 算法的經典實現
- 5.1 調度隊列架構:雙向鏈表與位圖的精妙組合
- 5.2 active指針和expired指針
引言
在操作系統的龐大體系中,進程是當之無愧的核心樞紐 —— 它是程序從代碼到動態執行的 “生命載體”,是內核調度資源的最小單元,更是理解操作系統如何協調硬件與軟件的關鍵切入點。從進程控制塊(PCB)對進程狀態的精細描述,到調度隊列對 CPU 資源的高效分配,每一個機制都蘊含著計算機科學的精妙設計。
本文將深入進程的 “內核視角”,解析task_struct
如何通過雙向鏈表編織成復雜的進程網絡,探討僵尸進程與孤兒進程的資源管理哲學,揭示優先級調度與進程切換的底層邏輯。無論你是想夯實操作系統基礎的學習者,還是渴望深入內核機制的開發者,都能從中窺見計算機系統如何通過進程管理實現 “有序的并發奇跡”。
一、進程基礎:概念與核心數據結構
1.1 進程的本質:程序的動態化身
- 《操作系統概念》中指出:進程是程序的執行實例。
- 內核視角:資源分配的最小單元。
1.2 進程控制塊(PCB):內核管理的靈魂
- 進程信息被放在一個叫做進程控制塊的數據結構中,可以理解為進程屬性的集合。
- Linux下的PCB是
task_struct
struct task_struct {// 1. 進程標識與狀態pid_t pid; // 進程唯一 IDpid_t ppid; // 父進程 IDvolatile long state; // 狀態(如 TASK_RUNNING, TASK_INTERRUPTIBLE)struct task_struct *parent; // 父進程指針// 2. 內存與資源管理struct mm_struct *mm; // 虛擬內存描述(頁表、地址空間)struct files_struct *files; // 文件描述符表(打開的文件)struct fs_struct *fs; // 文件系統上下文(根目錄、當前目錄)// 3. CPU 上下文(執行狀態)struct thread_struct thread; // 寄存器、棧等線程信息(Linux 中進程與線程統一)unsigned long thread_saved_pc; // 程序計數器(PC)// 4. 調度與優先級int prio; // 動態優先級(影響 CPU 調度)struct sched_entity se; // CFS 調度器實體(公平調度)// 5. 信號與 IPCsigset_t blocked; // 阻塞的信號集合struct sigpending pending; // 待處理信號struct sem_undo *semundo; // 信號量 undo 操作(IPC 回滾)// 6. 其他元數據char comm[TASK_COMM_LEN]; // 進程名(如 "bash")struct list_head tasks; // 進程鏈表(內核維護的進程列表)
};
在運行時,它會被加載到內存中。
組織進程
Linux是通過雙向循環鏈表的形式來組織 task_struct 的。它是通過在 task_struct 中嵌入了鏈表節點來實現雙向循環鏈表的。
1.2.1 鏈表節點嵌入
list_head
結構體
內核定義通用雙向鏈表節點:
struct list_head {struct list_head *next, *prev; // 雙向指針
};
task_struct
中的鏈表成員
每個進程的task_struct
包含list_head
類型的成員(如tasks
),作為鏈表節點:
struct task_struct {struct list_head tasks; // 連接到全局進程鏈表// 其他成員(如PID、狀態、內存管理等)
};
1.2.2 鏈表操作宏
通過一個宏來計算 task_struct
的起始地址。
接下來就有意思了,你都知道 task_struct
的起始地址了,然后通過對它的設計,我們是能夠保證它在一定的偏移量上是用于保存什么的結構。因而可以利用偏移量和強轉指針類型實現類型無關的鏈表操作。
1.2.3 全局鏈表管理
- 進程鏈表頭
內核以init_task
(0 號進程的task_struct
)為頭節點,維護全局進程鏈表。所有進程通過tasks
成員鏈接到該鏈表,形成進程樹。 - 多鏈表共存
task_struct
可通過不同的list_head
成員屬于多個鏈表(如:run_list
:就緒隊列(CPU 調度時使用)。wait_list
:等待隊列(I/O 阻塞時使用)。
),實現進程在不同狀態(運行、就緒、阻塞)下的動態管理。
1.3 進程查看與系統調用
-
進程的信息可以通過
/proc
系統文件夾查看
如:要獲取PID為1的進程信息,你需要查看/proc/1
這個文件夾。 -
大多數進程信息也可以使用 top 、ps 這些用戶級工具來獲取
ps aux | grep test | grep -v grep
我們這里讓程序一直休眠,查看一下進程
使用另一個終端進行查看:
1.3.1 通過系統調用獲取進程標識符
1.3.2 通過系統調用創建子進程
這里我們需要知道:
fork
雖然是一次調用,但是它有兩個返回值- 父子進程代碼共享,數據各自開辟空間,私有一份(采用寫實拷貝)
可以看到,父進程ret
接收到的返回值和子進程的 pid 是相同的!但是子進程的ret
為 0。
為什么會有兩個返回值呢?
- 父進程返回值
- 父進程調用
fork
進入內核態之后,內核復制出子進程并分配新pid
。 - 父進程從內核態返回時,直接返回子進程的
pid
,因此父進程fork
的返回值是子進程的PID
- 父進程調用
- 子進程返回值
- 我們前面提到過,子進程在創建的時候,代碼和父進程共享,但是它會復制父進程的數據,但內核會做一件事:把子進程用來存儲返回值的寄存器,強制設置為0,所以子進程從內核態返回到用戶態時,
fork
的返回值為 0,以便于識別子進程。
- 我們前面提到過,子進程在創建的時候,代碼和父進程共享,但是它會復制父進程的數據,但內核會做一件事:把子進程用來存儲返回值的寄存器,強制設置為0,所以子進程從內核態返回到用戶態時,
還有一個問題:
我們這里的 ret
不是一個變量嗎?為什么它能同時讓 else if(ret == 0)
和 else
成立呢?
這個有了上面父子進程的返回值不同解釋起來也簡單。
本質是:fork
讓父子進程成為兩個獨立執行流,而前面說了,子進程的數據是拷貝自父進程的,但是內核對子進程 fork
的返回值進行了修改,所以看似 “一個變量滿足多個分支”,實際是 “兩個進程、兩個變量副本”,所以能讓父進程走 else
、子進程走 else if
,產生 “同時成立” 的錯覺 。
二、進程生命周期:狀態與特殊場景
2.1 進程的幾個狀態
進程標準狀態
- R(Running)
運行態:進程正在 CPU 上執行。 - S(Sleeping)
睡眠態 / 阻塞態:進程因等待資源(如 I/O 完成)而暫停執行。
細分可能包括:- D(Disk Sleep):不可中斷睡眠,通常因等待磁盤 I/O 而阻塞,無法被信號喚醒。
- S(Interruptible Sleep):可中斷睡眠,能被信號喚醒。
- T(Stopped)
暫停態 / 停止態:進程被暫停(如通過SIGSTOP信號),保留當前狀態,可恢復。 - Z(Zombie)
僵尸態:進程已終止,但父進程尚未回收其退出狀態(PCB 殘留)。 - X(Dead)
死亡態:進程已完全終止,資源被徹底釋放(此狀態通常不可見,因進程已消失)。
補充狀態(非標準但常見)
- I(Idle)
空閑態:某些系統中用于表示內核線程或空閑進程。 - W(Waiting)
等待態:與阻塞態類似,等待特定事件。 - K(Killed)
被殺死:進程接收到終止信號(如SIGKILL),正在被清理。 - t (Tracing Stop)
調試追蹤暫停態(因調試器 ptrace 追蹤觸發,如 gdb 調試時遇到斷點)。
2.2 進程狀態查看
ps aux # 側重資源監控(CPU / 內存占用)
ps axj # 側重進程關系(父子 / 組 / 會話結構)
- a:顯示一個終端所有的進程,包括其他用戶的進程。
- x:顯示沒有控制終端的進程,例如后臺運行的守護進程。
- j:顯示進程歸屬的進程組ID、會話ID、父進程ID,以及與作業控制相關的信息
- u:以用戶為中心的格式顯示進程信息,提供進程的詳細信息,如用戶、CPU和內存使用情況等
2.3 僵尸進程與孤兒進程:資源管理的兩面性
2.3.1 僵尸進程
a. 什么是僵尸進程?
- 定義:
子進程已經正常終止運行,但父進程尚未調用wait()
或waitpid()
系統調用來回收其進程控制塊(PCB,Process Control Block)和退出狀態時,子進程會暫時處于僵尸狀態(Zombie State),簡稱 僵尸進程。 - 狀態標識:
在 Linux 中,通過ps
命令查看進程時,僵尸進程的STAT
字段會顯示為Z
(Zombie 的首字母),例如:
PID PPID STAT COMMAND
1234 5678 Z+ [defunct]
- 僵尸進程會以終止狀態保持在進程表中,并且會一直在等待父進程讀取退出狀態碼。
- 所以,只要子進程退出,父進程還在運行,但父進程沒有讀取子進程狀態,子進程進入 Z 狀態。
b. 僵尸進程的成因
- 父子進程的生命周期差異:
- 父進程創建子進程后,子進程先于父進程結束運行。
- 子進程終止時,內核會保留其退出狀態和少量資源(如 PID、運行時間等),直到父進程調用
wait()
獲取這些信息。 - 若父進程未及時調用
wait()
,子進程就會變成僵尸進程。
- 常見場景:
- 父進程邏輯缺陷:父進程未編寫回收子進程的代碼(如未使用
wait()
)。 - 父進程阻塞或死循環:父進程因等待其他資源而無法執行
wait()
。
- 父進程邏輯缺陷:父進程未編寫回收子進程的代碼(如未使用
c. 僵尸進程的危害
- 占用進程號(PID)資源:
- 系統中 PID 的數量有限(通常默認最大值為 32768 或更高),大量僵尸進程會耗盡 PID 資源,導致系統無法創建新進程。
- 影響進程監控:
僵尸進程雖不占用 CPU、內存等運行資源,但會干擾管理員對進程狀態的判斷(如通過 ps 命令看到無效進程)。 - 潛在的程序 bug 信號:
- 僵尸進程通常是程序中資源管理不當的表現,可能預示代碼存在邏輯漏洞(如未處理子進程退出)。
d. 如何識別僵尸進程?
- 使用
ps
命令過濾:- 輸出中
STAT
為Z
的進程即為僵尸進程,PPID
是其父進程的 PID。
- 輸出中
ps aux | grep Z
# 或
ps -e -o stat,ppid,pid,cmd | grep '^Z'
- 查看進程狀態文件:
- 若顯示
State: Z (zombie)
,則為僵尸進程。
- 若顯示
cat /proc/[PID]/status | grep State
2.3.2 孤兒進程
孤兒進程(Orphan Process) 是操作系統中與 僵尸進程 相對的概念,同樣涉及父子進程的生命周期管理。
a. 什么是孤兒進程?
- 定義:
當 父進程先于子進程終止,且子進程尚未結束時,子進程會被 init 進程(PID=1) 接管,成為 孤兒進程。- init 進程是系統啟動后的第一個進程,負責管理所有孤兒進程的生命周期。
- 核心特點:
孤兒進程的 父進程 PID(PPID)會被重置為 1(即 init 進程的 PID),由 init 進程自動回收其資源,因此 不會產生資源泄漏問題。
b. 孤兒進程與僵尸進程的對比
特性 | 孤兒進程 | 僵尸進程 |
---|---|---|
成因 | 父進程先于子進程終止 | 子進程先終止,父進程未回收 |
父進程 PID | 被重置為 1(init 進程) | 保持原父進程 PID |
資源管理 | init 進程自動回收資源 | 父進程需手動回收,否則殘留 |
危害 | 無(資源會被正常釋放) | 可能耗盡 PID 資源 |
狀態標識 | 正常運行狀態(如 S、R) | 狀態為 Z(僵尸狀態) |
c. 如何識別孤兒進程?
- 使用
ps
命令查看進程的父進程 PID(PPID):- 若某進程的
PPID
為 1,且狀態正常(非 Z),則為孤兒進程。
- 若某進程的
ps -ef | grep [子進程關鍵詞]
# 或
ps aux --forest | grep PID
UID PID PPID C STIME TTY TIME CMD
user 12345 1 0 10:00 ? 00:00:00 [orphan_process]
- 通過
/proc
文件系統查看:
cat /proc/[PID]/status | grep PPid
# 輸出示例:PPid: 1
三、進程優先級與調度策略
3.1 基本概念
- 優先級本質
進程優先級是一個 數值參數,用于標識進程獲取 CPU 時間的 相對重要性。優先級越高的進程,越容易被 CPU 調度執行。 - 核心目標
- 系統穩定性:確保關鍵系統進程(如內存管理、設備驅動)優先運行。
- 響應性:提升交互式進程(如終端、GUI 應用)的響應速度,改善用戶體驗。
- 資源公平性:避免低優先級進程長時間饑餓(Starvation)。
查看系統進程
ps -l
其中:
- UID:代表執行者的身份
- PID:代表這個進程的代號
- PPID:代表這個進程是由哪個進程發展衍生而來的,亦即父進程的代號
- PRI:代表這個進程可被執行的優先級,其值越小越早被執行
- NI:代表這個進程的nice值
3.2 優先級體系:PRI 與 NI 的協同
- RPI 很好理解,就和比賽排名一樣,數值越低,優先級越高
- NI 也就是 nice 值,表示進程可被執行的優先級的修正數值,范圍為 [-20, 19],默認為 0
- PRI(new)= PRI(old)+ nice
查看進程優先級
top
輸入 top 進入:
在這個界面按 r
-> 輸入進程PID -> 輸入nice值就能修改已存在進程的 nice 值了
3.3 調度的核心矛盾:競爭、獨立、并行與并發
在操作系統中,資源是有限的,但是又存在很多的進程,這就導致了存在以下幾種關系:
- 競爭:
指多個進程因爭奪 共享資源(如 CPU、內存、文件、I/O 設備等)而產生的相互制約關系。 - 獨立:
指進程在邏輯上 互不干擾,各自擁有獨立的運行環境和資源,執行結果僅取決于自身邏輯,與其他進程無關。 - 并行:
指 多個進程在同一時刻 同時執行,依賴 多核 CPU 或多處理器 硬件,每個任務分配到獨立的計算單元(如 CPU 核心)。 - 并發:
指 多個進程或線程在同一時間段內交替執行,通過 CPU 時間片輪轉(如分時系統)或事件驅動機制,在宏觀上呈現 “同時運行” 的效果,但微觀上同一時刻僅執行一個任務。
四、進程切換:內核級上下文管理
在操作系統中,進程切換(Process Switch) 是實現多任務并發執行的核心機制。當操作系統需要從一個進程切換到另一個進程時,會執行一系列復雜的操作,以確保上下文環境的正確保存和恢復。
4.1 進程切換的核心步驟
- 保存當前進程的上下文
- 硬件上下文:
- CPU 寄存器(如通用寄存器、程序計數器 PC、棧指針 SP)。
- 處理器狀態(如標志位、特權級別)。
- 軟件上下文:
- 進程控制塊(PCB)中的信息,包括進程狀態、內存管理信息(頁表)、打開文件列表等。
- 硬件上下文:
- 更新進程狀態
將當前進程的狀態從 運行態 改為 就緒態 或 阻塞態,并將其 PCB 插入相應隊列(就緒隊列或阻塞隊列)。 - 選擇新進程
調度器根據調度算法(如優先級調度、輪轉調度)從就緒隊列中選擇一個新進程。 - 恢復新進程的上下文
- 從新進程的 PCB 中加載寄存器值、內存映射等信息。
- 更新內存管理單元(MMU)的頁表,切換虛擬地址空間。
- 執行上下文切換
通過 特權指令(如 x86 的iret
)將控制權轉移到新進程的程序計數器(PC)指向的指令處,開始執行新進程。
4.2 切換背后的 “原子操作”
進程切換通常由以下事件觸發:
- 時間片耗盡:
進程在 CPU 上執行的時間超過分配的時間片(如 Linux 默認 10-20ms),調度器強制切換。 - 進程阻塞:
進程因等待資源(如 I/O、鎖、信號量)主動進入阻塞狀態,釋放 CPU。 - 高優先級進程就緒:
新的高優先級進程進入就緒隊列,搶占當前低優先級進程。 - 進程終止:
進程執行完畢或被強制終止,釋放 CPU 資源。 - 系統調用 / 中斷:
進程執行系統調用(如read()
)或發生硬件中斷(如時鐘中斷),CPU 從用戶態切換到內核態,由內核決定是否切換進程。
4.3 進程切換的開銷
進程切換會帶來一定的性能開銷,主要包括:
- 上下文保存 / 恢復開銷:
寄存器讀寫、PCB 操作等指令執行需要時間。 - 內存訪問開銷:
切換頁表會導致 TLB(Translation Lookaside Buffer)失效,后續內存訪問需重新查詢頁表,增加延遲。 - CPU 緩存失效:
新進程的指令和數據可能不在 CPU 緩存中,導致緩存缺失(Cache Miss),降低執行效率。
五、Linux 2.6 內核調度隊列:O (1) 算法的經典實現
5.1 調度隊列架構:雙向鏈表與位圖的精妙組合
這張圖是 Linux 2.6 內核進程調度隊列(runqueue)的結構示意圖
下面來解釋一下這張圖:
一個單核 CPU 維護著一個 runqueue
-
運行隊列(runqueue)組成
- 元數據:包含鎖(
lock
)、運行進程數(nr_running
)、CPU 負載因子(cpu_load
)等,用于調度控制和狀態統計。 - 雙隊列設計:
- 活躍隊列(
array[0]
,藍色框):存儲時間片未耗盡的進程,通過nr_active
(活躍進程數)、bitmap[5]
(位掩碼,快速標記非空優先級隊列)、queue[140]
(140 個優先級鏈表,同優先級進程按 FIFO 排列)管理。 - 過期隊列(
array[1]
,紅色框):存儲時間片耗盡的進程,結構與活躍隊列相同,通過active
和expired
指針輪換(活躍隊列為空時交換,重新分配時間片,避免進程饑餓)。
- 活躍隊列(
- 元數據:包含鎖(
-
優先級與隊列管理
- 優先級范圍:
0~139
(0~99
實時優先級,100~139
普通優先級,對應nice值-20~19
)。 queue[140]
:每個優先級對應一個雙向鏈表,進程按優先級分組,同優先級 FIFO 調度。bitmap[5]
:5 個 32 位掩碼(共 160 位),快速定位最高優先級非空隊列(位運算實現 O (1) 查找,提升調度效率)。
- 優先級范圍:
-
任務結構(task_struct)
- 進程通過
run_list
節點嵌入queue
的雙向鏈表中,支持快速插入 / 刪除(如進程狀態切換、時間片耗盡時的隊列遷移)。
- 進程通過
-
O (1) 調度算法核心
- 優先級范圍:0~139(共 140 個優先級,對應
queue[140]
數組,每個下標代表一個優先級)。 - 位圖結構:
bitmap[5]
由 5 個unsigned long
(共 160 位)組成,每一位對應一個優先級隊列的空滿狀態。例如:- 優先級p對應
bitmap[p/32]
的第p%32
位(如優先級 5 →bitmap[0]
第 5 位,優先級 33 →bitmap[1]
第 1 位)。
- 優先級p對應
- 優先級范圍:0~139(共 140 個優先級,對應
簡單總結:
queue[140]
是指一共有這么多優先級隊列。然后有進程進入運行隊列,就把對應的bitmap
置為非0。在進程調度時,通過位運算找出第一個為1的比特位,再從對應優先級的優先級隊列中選擇進程進行調度,直到這個優先級隊列為空,且前面優先級均為空,才在下一個非空優先級隊列中選擇進程進行調度。
5.2 active指針和expired指針
上面對于這兩個指針只是簡單的提到了一句,這里還需要做些補充
active
指針永遠指向活動隊列expired
指針永遠指向過期隊列
活動隊列的調度邏輯
-
調度流程:
- 調度器通過
active->bitmap
快速定位最高優先級非空隊列(如idx=5
),取出隊首進程執行。 - 進程時間片耗盡后,從
active->queue[idx]
移除,加入expired->queue[idx]
,并標記expired->bitmap[idx]
為非空。
- 調度器通過
-
關鍵特性:
- 只出不進:active隊列在調度期間不接收新進程,確保原有進程按優先級有序執行。
- 動態減少:隨著調度進行,
active->nr_active
遞減,expired->nr_active
遞增。
過期隊列的輔助作用
- 進程管理:
- 新進程直接加入
expired
隊列。 - 時間片耗盡的進程從
active
隊列移入expired
隊列。 - 只進不出:
expired
隊列在active
隊列未空時不參與調度,僅積累進程。
- 新進程直接加入
- 時間片重置:
- 當
expired
隊列切換為active
隊列時,所有進程重新分配時間片(基于優先級計算),確保公平性
- 當
指針交換的觸發條件
- 主動觸發:
- 當
active->nr_active
減至 0 時,內核自動執行swap(&active, &expired)
,交換指針指向。 - 交換后,原
expired
隊列成為新的active
隊列,原active
隊列變為expired
隊列。
- 當
- 被動觸發:
- 若
active
隊列未空但存在更高優先級進程插入expired
隊列,調度器會優先處理active
隊列,直到其為空后再交換指針。
- 若