天天看點

linux核心——從fork()看程序管理一、任務結構與程序描述符二、程序的狀态三、回到fork( )四、線程在linux中的實作五、程序的終止與孤兒程序

衆所周知,linux系統中,程序的建立一般由fork( )或者exec( )來實作。然而,了解fork( )背後所做的工作是很有必要的,有助于我們更好地了解程序之間的通信。在此之前,我們先來介紹幾種關于程序的結構。

一、任務結構與程序描述符

首先,核心中有一個包括所有程序的清單,叫做任務隊列。它是一個雙向循環連結清單,連結清單中的每一項類型都是task_struct,稱為程序描述符的結構。(定義在核心<linux/sched.h>中)

程序描述符所包含的資訊有:

① 辨別符:唯一,用來區分程序。

② 狀态:運作态等;

③ 優先級;

④ 程式計數器:下一條指令的位址;

⑤ 記憶體指針;

⑥ 上下文資料:程式運作時寄存器中的資料;

⑦ I/O狀态資訊:包括顯式的I/O請求、配置設定給程序的裝置等等;

⑧ 記賬資訊;

二、程序的狀态

程序在運作的過程中會有多次狀态切換,總的來說有5種狀态:

運作态:程序正在執行;

就緒态:程序準備執行;

阻塞/等待态:程序在某些事件發生之前不能執行;

建立态:剛剛建立,還沒執行;(沒加載到記憶體中)

退出态:準備退出,釋放記憶體;

linux核心——從fork()看程式管理一、任務結構與程式描述符二、程式的狀态三、回到fork( )四、線程在linux中的實作五、程式的終止與孤兒程式

三、回到fork( )

首先,fork( )通過拷貝目前程序來建立一個子程序(寫時拷貝技術,後面會介紹)。父子程序的差別僅僅是PID、PPID、以及一些統計量不同而已。exec()函數負責讀取可執行檔案并将其載入到位址空間開始運作。是以,fork()會傳回兩次,傳回到父程序時傳回值等于0,傳回到子程序時傳回值小于0,程式設計的時候正是通過傳回值來區分父子程序。

接下來介紹寫時拷貝。傳統的fork()直接把所有的資源複制給子程序,這樣過于簡單且效率低下,并且拷貝的資料也許不共享。更糟的是,如果子程序打算立即執行一個新的映像(exec),所有的拷貝将前功盡棄。所有,現在的fork( )用寫時拷貝來實作。宗旨就是推遲甚至免除拷貝資料。最開始,核心并不複制整個程序位址空間,而是讓父子程序共享一個拷貝,在寫入之前,以自讀的方式共享。當需要寫入的時候,資料才會被複制,進而各個程序擁有各自的拷貝。這樣使得linux有了程序快速執行的能力,也是最大的優點。

fork()接下來的工作就好了解了。

1、為新程序配置設定相關資源,如核心棧,程序描述符等。此時的子程序和父程序的描述符是完全相同的。

2、檢查程序總數是否超過系統上限。(對于普通使用者一般不會)

3、将子程序與父程序區分開來。程序描述符内很多資料将被清0或者改變。

4、将子程序設定為不可中斷型,保證它不會投入運作。

5、為新程序配置設定有效的PID。

6、拷貝或者共享打開的檔案,檔案系統資訊、信号處理函數等等。

值得注意的是,核心會有意讓子程序先執行,因為一般子程序會馬上調用exec()函數,這樣可以避免寫時拷貝的額外開銷。否則,如果父程序先執行并且寫入資料,可能會做一次無用的寫時拷貝。

四、線程在linux中的實作

為什麼有了程序,還要建立線程機制呢?因為有時候一個程序還是太大了,linux是搶占式的,将一個程序分成多個線程來執行能提高效率。線程機制支援并發程式設計,可以真正的并行處理。

對linux核心來說,并沒有線程這個概念。它會把線程當做程序來實作,線程僅僅被視作一個與其他程序共享資源的程序。不過,建立的線程會與父程序共享位址空間、檔案系統資源、檔案描述符、信号處理程式。

不得不提到一種特殊的線程——核心線程。核心程序需要後再執行一些操作,就由核心線程來完成。它和普通程序的差別在于核心線程沒有獨立的位址空間。它隻在核心空間運作,從不切換到使用者空間。不過,他也可以被排程,被搶占。

五、程序的終止與孤兒程序

程序總是要終止的,所有的程序在終止時都會調用do_exit( )來完成終止的一部分操作。(注意隻是一部分)執行完do_exit()之後,程序會進入僵死狀态,等待父程序來收尾(waitpid)。此時,系統會保留它的程序描述符。這樣使得系統可以在子程序終止後仍然能獲得它的資訊。是以,清理工作和程序描述符的删除是分開進行的。在父程序獲得子程序的資訊之後,或者通知核心它并不關注那些資訊後,子程序的程序描述符才會被釋放。

接下來問題就來了,如果父程序在子程序之前就終止了呢?linux給出的解決辦法是,給子程序在目前線程組找一個線程當做父親,如果不行,就把init()當做他的父親。這樣就避免了孤兒程序的僵死狀況。

繼續閱讀