這篇文章是《讀薄「Linux 內核設計與實現」》系列文章的第 II 篇,本文主要講了以下問題:進程管理的任務、進程管理與其他模塊的依賴關系、進程描述符和任務隊列、進程的創建、線程的實現、進程的終止、進程調度。
0x00 進程管理的任務
進程能創建新的進程(通過復制現有進程)
確定哪個進程能擁有 CPU
接受中斷並將中斷導向相應的內核子系統
管理時鐘硬件
當一個進程結束時釋放其資源
動態裝載執行模塊
0x01 進程管理與其他模塊的依賴關系
I 進程模塊的內外界面
對用戶進程提供了一組簡單的系統調用接口
對內核的其他模塊提供了豐富的接口功能
II 進程模塊與其他模塊的依賴關系
內存管理模塊:當一個進程被調度時,為它建立內存映射
IPC 模塊:bottom-half 處理使用了其中的信號量隊列
文件系統模塊:在裝載 module 時為進程調度提供實際設備的訪問途徑
所有其他模塊都依賴於進程調度模塊,因為當要進行硬件訪問時,它們需要 CPU 掛起用戶進程,切換到系統態進行處理0x02 進程描述符和任務隊列
I 進程描述符
task_struct
(進程描述符)定義於
II 分配進程描述符
Linux 通過 slab 分配
task_struct
可以達到對象復用和緩存著色的目的,不需要頻繁調用內存管理相應功能,相當於一種高級緩存
III 進程描述符的存放
通過 PID 標識進程,最大值默認為32768
如何獲得當前運行的進程?
通過 current 宏(體系結構相關),
current_thread_info()->task;
0x03 進程的創建
I 進程創建過程描述
Linux 中,進程的創建是通過拷貝已存在的進程來實現的
內核啟動的時候,
start_kernel()
初始化各系統數據結構,同時生成了與系統共存亡的後台進程:init
這些子進程通過
fork()
系統調用生成他們的子進程
進程的終止是通過系統調用
_exit()
實現的
II 進程創建函數
fork(): 進程復制自身產生子進程
exec(): 加載可執行代碼模塊覆蓋自身代碼
III 寫時拷貝(copy-on-write)
使地址空間上的頁的拷貝被推遲到實際發生寫入的時候才進行
0x04 線程的實現
Linux 沒有真正的線程,線程當做進程來實現(僅僅是進程間資源直接共享的一種機制)
通過 clone 時傳遞一些參數標志來實現:
[code]clone(CLONE_VM | CLONE_FS | CLONE_FILES | CLONE_SIGHAND, 0);
這些標志定義於 <linux/sched.h>
內核線程:獨立運行在內核的標准進程
0x05 進程的終止
刪除進程描述符
父進程得知進程終止的消息後才能刪除子進程的描述符
父進程 WAIT() (通過系統調用
wait4()
實現),掛起父進程,直到其中一個子進程退出
之後,真正釋放描述符(
release_task()
):
調用
_unhash_process()
從 pidhash 上刪除該進程
_exit_signal()
釋放目前僵死進程所使用剩余資源
調用
put_task_struct
釋放描述符(內核棧、thread_info 所占的頁、slab 高速緩存)
若父進程異常終止,將發生什麼情況?
如果父進程在子進程之前推出,這些成為孤兒的進程會處於僵死狀態,解決方案是給子線程在當前進程中找一個父親,如果不行,則以 init 進程為父親,過程:
do_exit() -> exit_notify() -> forget_original_parent() -> find_new_reaper()
開始尋父。
0x06 進程調度
I 多任務系統分類
搶占式:由調度程序決定一個進程的運行與掛起,掛起的動作就叫做搶占(Linux 為搶占式內核),進程在被搶占之前的運行時間是預先設置好的,叫做時間片
非搶占式:除非進程自己停止,否則它會一直運行,它主動掛起自己的動作叫讓步(yielding)
II 調度策略
I/O 消耗型和 CPU 消耗型進程
I/O 消耗型:進程大部分時間用來提交 I/O 請求或是等待 I/O 請求
CPU 消耗型:進程把時間大多用在執行代碼上
UNIX 系列傾向於 I/O 消耗型進程優先級(Linux 采用2種不同的優先級范圍)
nice 值:從 -20~+19,默認為0,nice 越大,優先級越低
實時優先級:從 0~99,越高的實時優先級越高,它的值可以配置
時間片
時間片分配多大?:時間片過長使得系統交互性不好;時間片過短會導致大部分 CPU 時間浪費在進程的切換上。
Linux 采用可變長的時間片
III Linux 調度算法
O(1) 調度
優先級數組
O(1)調度算法中的一個核心數據結構即為
prio_array
結構體,該結構體中有一個用來表示進程動態優先級的 queue,它其中的每一項都是一種優先級形成的進程的鏈表,即每一條鏈表中的進程都有相同優先級
[code]struct prio_array{
unsigned int nr_active;
unsigned long bitmap[BITMAP_SIZE];
struct list_head queue[MAX_PRIO];
}
另外,該結構體中還包含一個優先級位圖 bitmap,該位圖使用1位來表示1種優先級,開始時,所有位置都置0,一旦有進程處於可運行狀態時,該進程所在優先級對應位圖中的相應位被置1.
因此,在 O(1)調度中查找最高的優先級就轉化為查找優先級位圖中第一個被置1的位確定了最高優先級之後,選取下一個進程就是在 queue 對應的鏈表中取一個進程即可
活動進程和過期進程
在 O(1)調度中,對可運行態的進程分了兩類:一類為
活動進程,即時間片還未用完的進程;另一類為
過期進程,即時間片已經用完的進程,但進程還未結束,它們沒有機會得到執行。
在可運行隊列結構中(runqueue)的 arrays 的兩個元素分別來表示上述兩個集合,
active
和
expire
兩個指針分別指向這2個集合
時間片的計算
active
中的進程一旦時間片使用完畢就會放入expire
中,並設置好新的時間片;當active
為空時,說明所有進程時間片已耗盡,此時,將active
和expire
對調,重新開始下一輪時間片的遞減過程。O(1)算法中的兩個核心
選擇下一個進程
時間片的重新分配
CFS 調度(公平調度)
O(1)算法是根據進程的優先級進行調度的;CFS 是根據進程的虛擬進程運行時間來進行調度的
如何選擇進程?
CFS 提出了虛擬運行時間的概念(vruntime),vruntime 記錄了一個
可執行進程到當前時刻為止執行的總時間,vruntime 越大,它被調度的可能性越小,因此每次選擇 vruntime 越小的進程進行調度(在 Linux 中使用紅黑樹來找出 vruntime 最小的進程)
調度進程的運行時間
現在已經知道了進程是如何調度的,那麼當進程運行時,它的運行時間是多少呢?
CFS 的運行時間是由當前所有可調度進程優先級比重來確定的例:3個進程優先級為5,15,20,它們時間片大小分配為:5/40, 15/40, 20/40
IV 搶占和上下文切換
上下文切換表示從一個可執行進程切換到另一個可執行進程(定義在 sched.h 中的 context_switch())
調用
switch_mm()
:把虛擬進程從一個進程切換到新進程
調用
switch_to()
:保存和恢復棧信息、寄存器信息
need_resched
標志:每個進程都包含該標志,為了讓內核知道在什麼時候調用
schedule()
.當某進程應該被搶占時,設置該標志;當一個優先級高的進程進入可執行態時,也會設置該標志。
用戶搶占的發生
從系統調用返回用戶空間時
從中斷處理程序返回用戶空間時
當內核返回用戶空間時,如果 need_resched
被設置,會導致 schedule()
被調用,此時發生用戶搶占內核搶占的發生
從中斷程序返回內核空間時
內核代碼再一次具有可搶占性時
如果內核中的任務顯示調用
schedule()
如果內核中的任務阻塞(同樣也會調用
schedule()
)
本文的版權歸作者 羅遠航 所有,采用 Attribution-NonCommercial 3.0 License。任何人可以進行轉載、分享,但不可在未經允許的情況下用於商業用途;轉載請注明出處。感謝配合!