第三章 進程管理
3.1 進程
1、進程就是處於執行期的程序;進程就是正在執行的程序代碼的實時結果;進程是處於執行期的程序以及相關的資源的總稱;進程包括代碼段和其他資源。
線程:是在進程中活動的對象。
2、執行線程,簡稱線程,是在進程中活動的對象。每個線程都擁有一個獨立的程序計數器、進程棧和一組進程寄存器。
3、內核調度的對象是線程,而不是進程。Linux對線程並不特別區分,視其為特殊的進程
4、在現代操作系統中,進程提供兩種虛擬機制:虛擬處理器和虛擬內存。在線程之間可以共享虛擬內存,但每個都擁有各自的虛擬處理器。
5、程序本身並不是進程。實際上,完全可能存在兩個或多個不同的進程執行的是同一個程序。
6、進程在創建它的時刻開始存活。在Linux系統中,這通常是調用fork()系統的結果。
7、fork()系統調用從內核返回兩次:一次回到父進程,另一次回到新產生的子進程。
8、fork()實際上是由clone()系統調用實現的。
9、程序通過exit()系統調用退出程序。
10、進程的另一個名字是任務。
11、exec():創建新的地址空間並把新的程序載入其中。
wait4():父進程查詢子進程是否終結
wait()、waitpid():程序退出執行后變為僵死狀態,調用這兩個消滅掉。
3.2 進程描述符及任務結構
1、內核把進程的列表存放在叫做任務隊列的雙向循環鏈表中。鏈表中的每一項都是類型為task_struct、稱為進程描述符的結構,該結構定義在<linux/sched.h>文件中,包含一個具體進程的所有信息。
2、進程描述符中包含的數據能完整的描述一個正在執行的程序:它打開的文件,進程的地址空間,掛起的信號,進程的狀態,還有其他更多信息。
一、分配進程描述符
1、Linux通過slab分配器分配task_struct結構,這樣能達到對象復用和緩存着色的目的。
2、slab分配器——動態生成,只需在棧底或者棧頂創建一個新的結構struct thread_info。
每個任務的thread_info結構在它的內核棧的尾端分配。結構中task域中存放的是指向該任務實際task_struct的指針。
二、進程描述符的存放
1、內核通過一個唯一的進程標識值PID來標識每個進程。pid類型為pid_t,實際上就是一個int類型,最大值默認設置為32768,上限私改/proc/sys/kernel/pid_max。
2、pid存放在各自進程描述符中。
3、通過current宏查找到當前正在運行進程的進程描述符。
4、x86中,在內核棧的尾端創建thread_info結構,通過計算偏移間接地查找task_struct結構。
5、current通過current_thread_info()把棧指針的后13個有效位屏蔽掉,再從thread_info的task域中提取並返回task_struct的地址。
三、進程狀態
1、進程描述符中的state域是用來描述進程當前狀態的。共有五種狀態,標志如下:
(1)TASK_RUNNING(運行):進程是可執行的,或者正在執行,或者在運行隊列中等待執行.
(2)TASK_INTERRUPTIBLE(可中斷):進程正在睡眠/被阻塞
(3)TASK_UNINTERRUPTIBLE(不可中斷):睡眠/被阻塞進程不被信號喚醒
(4)TASK_TRACED:被其他進程跟蹤的進程
(5)TASK_STOPPED(停止):進程停止執行;進程沒有投入運行也不能投入運行。
2、接收到SIGSTOP、SIGTSTP、SIGTTIN、SIGTTOU等信號時,或者調試時收到任何信號,都可以進入這種狀態。
四、設置當前進程狀態
1、用set_task_state(task,state)函數。將任務task的狀態設置為state。
2、set_current_state(state)和set_task_state(current,state) 等價。
五、進程上下文
1、程序執行系統調用或者觸發異常后,會陷入內核空間,這時候內核“代表進程執行”並處於進程上下文中。在此上下文中current宏有效。
2、進程對內核的訪問必須通過接口:系統調用和異常處理程序。
六、進程家族樹
1、所有的進程都是pid為1的init進程的后代。
2、內核在系統啟動的最后階段啟動init進程。
3、系統中的每一個進程必有一個父進程,可以擁有0個或多個子進程,擁有同一個父進程的進程叫做兄弟。這種關系存放在進程描述符中,parent指針指向父進程task_struct,children是子進程鏈表。
4、獲得父進程的進程描述符:struct task_struct *my_parent = current->parent;
5、訪問子進程:
struct task_struct *task;
struct list_head *list;
list_for_each(list, ¤t->children){
task = list_entry(list, struct task_struct, sibling);
/* task現在指向當前的某個子進程 */
}
init進程的進程描述符是作為init_task靜態分配的。
6、獲取鏈表中的下一個進程:list_entry(task->tasks.next, struct task_struct, tasks);
7、獲取鏈表中的上一個進程:list_entry(task->tasks.prev, struct task_struct, tasks);
以上依賴於next_task(task)和prev_task(task)這兩個宏實現。
8、for_each_process(task)宏,依次訪問整個任務隊列,每次訪問任務指針都指向鏈表中的下一個元素。
struct task_struct *task;
for_each_process(task){
/* 它打印出每一個任務的名稱和PID */
printk("%s[%d]\n",task->comm, task->pid);
}
3.3 進程創建
一、寫時拷貝
1、fork():通過拷貝當前進程創建一個子進程。子進程與父進程的區別僅在於PID,PPID和某些資源和統計量。
2、exec():讀取可執行文件並將其載入地址空間開始運行。
3、寫時拷貝是一種可以推遲甚至免除拷貝數據的技術,內核不復制整個進程地址空間,而是讓父進程和子進程共享一個拷貝。
4、資源的復制只有在需要寫入時才會進行,在此之前以只讀方式讀取。
5、fork的實際開銷就是復制父進程的頁表以及給子進程創建唯一的進程描述符。
二、fork()
1、Linux通過clone()系統調用實現fork()。
2、do_fork完成創建中大量工作,定義在kernel/fork.c文件中。該函數調用copy_process()函數讓進程開始運行。
3、創建進程的大概步驟如下:
(1)fork()、vfork()、__clone()都根據各自需要的參數標志調用clone()。
(2)由clone()去調用do_fork()。
(3)do_fork()調用copy_process()函數,然后讓進程開始運行。
(4)返回do_fork()函數,如果copy_process()函數成功返回,新創建的子進程被喚醒並讓其投入運行。內核有意選擇子進程首先執行。
三、vfork()
1、除了不拷貝父進程的頁表項之外,vfork()系統調用和fork()的功能相同。
2、系統最好不要調用vfork()。
3、vfork()系統調用的實現是通過向clone()傳遞一個特殊標志來進行的。
(1)調用copy_process()是,task_struct的vfor_done成員被設置為NULL。
(2)執行do_fork()時,如果給定特定標志,則vfor_done會指向一個特定地址。
(3)子進程先開始執行后,父進程不是馬上恢復執行,而是一直等待,知道子進程通過vfor_done指針向它發送信號。
(4)在調用mm_release()時,該函數用於進程退出內存地址空間,並且檢查vfor_done是否為空,如果不為空,則會向父進程發送信號。
(5)回到do_fork(),父進程醒來並返回。
3.4 線程在Linux中的實現
1、線程機制是現代編程技術中常用的一種抽象概念,該機制提供了在同一程序內共享內存地址空間運行的一組線程,可以共享打開的文件和其他資源,支持並發程序設計,在多處理器系統上可以保證真正的並行處理。
2、從內核的角度來看並沒有線程這個概念,它把所有線程都當做進程來實現,線程僅僅被視為一個與其他進程共享某些資源的進程。
一、創建線程
1、線程的創建和普通進程的創建類似,只不過在調用clone()時需要傳遞一些參數標志來指明需要共享的資源:clone(CLONE_VM | CLONE_FS | CLONE_FILES | CLONE_SIGHAND, 0);
父子倆共享地址空間、文件系統資源、文件描述符和信號處理程序。
2、普通的fork()的實現:clone(SIGCHLD, 0);
vfork()的實現:clone(CLONE_VFORK | CLONE_VM | SIGCHLD, 0);
3、傳遞給clone()的參數標志決定了新創建進程的行為方式和父子進程之間共享的資源種類。
二、內核線程
1、內核線程:獨立運行在內核空間的標准進程。
2、內核線程沒有獨立的地址空間,只在內核空間運行,從來不切換到用戶空間,可以被調度和被搶占。
3、內核線程只能由其他內核線程創建。內核通過從kthreadd內核進程中衍生出所有新內核線程來自動處理。在<linux/kthread.h>中聲明接口。
4、新的任務是由kthread內核進系統調用程通過clone()而創建的。
5、內核線程啟動后就一直運行直到調用do_exit()退出,或者內核的其他部分調用kthread_stop()退出,傳遞給kthread_stop()的參數為kthread_create()函數返回的task_struct結構的地址。
3.5 進程終結
1、當一個進程終結時,內核必須釋放它所占有的資源並告知父進程。
2、進程終結的原因:一般是來自自身,發生在調用exit()系統調用時。既可能顯式地調用這個系統調用,也可能隱式的從某個程序的主函數返回。
3、不管進程是怎么終結的,該任務大部分都要靠do_exit()來完成。
一、刪除進程描述符
1、在父進程獲得已終結的子進程信息並且通知內核不關注后,子進程的task_struct結構才被釋放。
2、wait()這一族函數都是通過唯一的一個系統調用wait4()來實現的。它的標准動作是掛起調用它的進程,直到其中的一個子進程退出,此時函數返回該子進程的PID。
3、釋放進程描述符時,需要調用release_task()。
二、孤兒進程造成的進退維谷
1、如果父進程在子進程之前退出,必須有機制來保證子進程能找到一個新的父親,否則這些成為孤兒的進程就會在退出時永遠處於僵死狀態,白白地耗費內存。
2、解決方法:
(1)給子進程在當前線程組內找一個線程作為父親,
(2)如果不行,就讓init做它們的父進程。
在do_exit()中會調用exit_notify(),該函數會調用forget_original_parent(),而后者會調用find_new_reaper()來執行尋父過程。
接着遍歷所有子進程並為它們設置新的父進程。
然后調用ptrace_exit_finish()同樣進行新的尋父過程,是給ptraced的子進程尋找父親。
遍歷了兩個鏈表:子進程鏈表和ptrace子進程鏈表,給每個子進程設置新的父進程。
最后init進程會例行調用wait()來檢查其子進程,清除所有與其相關的僵死進程。
3.6 總結
在本章中,學習了操作系統中的核心概念——進程。討論了Linux如何存放和表示進程(用task_struct和thread_info),如何創建進程(通過fork(),實際上最終是clone()),如何把新的執行映像裝入到地址空間(通過exec()系統調用族),如何表示進程的層次關系,父進程又是如何收集其后代的信息(通過wait()系統調用族),以及進程最終如何消亡(強制或自願的調用exit())。進程是一個非常基礎、非常關鍵的抽象概念,位於每一種現代操作系統的核心位置,也是我們擁有操作系統(用來運行程序)的最終原因。
參考資料
《Linux內核設計與實現》第3版