天天看點

《linux核心設計與實作-筆記2》

第 3 章 程序管理

這一章主要介紹程序在linux系統中的表示,從如何建立到如何被回收的整個過程。同時特别提到了線程在linux系統中的實作。

程序的定義:

程序就是處于執行期的程式,但程序不僅局限于可執行代碼,還包括其他資源,比如打開的檔案,挂起的信号,核心資料,處理器狀态,多個線程,全局變量等。

線程的定義:

線程是程序中活動的對象,線程有單獨的程式計數器,程序棧,一組程序寄存器。值得注意的是,unix系統中,一個程序包括一個線程,但linux系統中,線程和程序其實不區分的(核心角度)。

虛拟機制:虛拟處理器和虛拟記憶體。線程之間共享虛拟記憶體,但每個有各自的虛拟處理器。

記住,程序是執行期的程式 + 相關資源

程序建立通常是通過fork()系統調用來實作,通過複制現有程序來建立全新程序,調用一次,傳回兩次,一次回到父程序,傳回子程序PID,一次回到子程序,傳回0。一般建立新的程序都是為了執行新的程式,接着調用exec()可以建立新的位址空間。linux中,fork()實際上是clone()系統調用實作的。

最終程式通過exit()退出執行,釋放資源。父程序通過wait()系統調用獲得子程序狀态,這時,子程序才從僵死狀态中,完全被釋放掉。

程序描述符及任務結構

程序清單在核心中是一個任務隊列的雙向循環連結清單。結點的資料類型是task_struct,稱為程序描述符。包含程序所有資訊,在32位機器上,大約有1.7kb。

《linux核心設計與實作-筆記2》
linux 通過slab配置設定器配置設定task_struct結構。這樣的目的是對象複用和緩存着色。避免動态配置設定和釋放帶來的資源消耗

程序建立時會産生兩個棧,一個使用者空間的程序棧,另一個是屬于核心空間的核心棧。程序調用系統調用陷入核心态就會發生棧的切換。實際上隻是棧的位址的變動。這裡需要注意的是,從使用者空間陷入核心态時,核心棧已經是清空的,是以,這時僅僅是将核心棧的棧頂指針給現在的棧位址寄存器就行。

程序核心棧是一個thread_union的結構如下:

union thread_union {
	struct thread_info thread_info;
	unsigned long stack[THEREAD_SIZE/sizeof(long)];
};

struct thread_info {
	struct task_struct   *task; // 注意
	struct exec_domain   *exec_domain;
	__u32                flg;
	__u32                status;
	__u32                cpu;
	int                  preempt_count;
	mm_segment_t         addr_limit;
	struct restart_block restart_block;
	void                 *sysenter_return;
	int                  uaccess_err;

};

struct task_struct {
	void *stack; // 注意

};
           

從代碼中可以看出task_struct 和 thread_info 互相保持對方的指針。再看一個程序核心棧的布局圖,我們就知道,可以通過計算直接得到thread_info的指針。

《linux核心設計與實作-筆記2》

是以,核心棧和程序描述符之間就有了一個座橋梁,可以互相引用。

PID辨別每個程序,類型為pid_t,實際上是一個int類型,預設值最大32768.可以修改。這個值越小,轉一圈就越快。

程序狀态

系統中的程序必處于五種狀态之一,如圖:

《linux核心設計與實作-筆記2》

可中斷,程序處于睡眠(阻塞),等待某些條件達成,但可以被信号提前喚醒

不可中斷,程序處于睡眠(組設),等待某些條件達成,不會被信号提前喚醒

程序的狀态可以設定,但是我們知道這裡會需要設定記憶體屏障,強制其他處理器重新排序。避免不一緻狀态:)

程序家族樹

linux系統中的程序有明顯的繼承關系。原因是将程序的建立分為fork()/exec()兩個階段。所有的程序都是PID為1的init程序的後代。核心在系統啟動的最後階段啟動init程序,該程序讀取初始化腳本并執行其他的相關程式,最終完成系統的啟動過程。

程序描述符中,有parent指針,指向父程序,有children list,表示子程序連結清單。還有任務隊列中的next、prev指針,指向任務隊列的前一個和下一個程序。

程序建立

剛才說到,程序的建立和特别(unix,linux)。許多其他的系統是通過spawn程序機制,建立新的位址空間,讀入可執行檔案,最後開始執行。而linux采用fork()和exec()。fork拷貝目前程序,建立一個子程序,差別僅僅在于PID、PPID和一些統計量(挂起的信号等)。exec負責讀取可執行檔案,開始運作。

傳統的fork,直接複制所有資源給新程序。簡單但效率低下,因為大多數新程序都打算立即執行exec,那之前的拷貝就毫無意義。linux的fork使用的是寫時拷貝(copy on write)頁實作。延遲拷貝的過程,核心不複制整個程序位址空間,而父子程序共享一個程序位址空間。隻有需要寫入時,資料才會被指派。實際大部分新程序都是立即執行exec,不需要複制原有的程序資料。能夠快速建立程序。

fork的實際開銷是,複制父程序的頁表以及給子程序建立唯一的程序描述符。

線程在linux中的實作

我們一般來說,線程是輕量級的程序,程序中包含若幹線程。同一程式的線程共享記憶體位址空間,包括打開的檔案和其他資源。linux實作線程非常特别,從核心的角度,沒有線程的概念。linux把所有的線程當成程序來實作。線程僅僅被視為和其他程序共享某些資源(位址空間)的程序。每個線程有自己的task_struct,是以在核心看來就是一個普通的程序。想想,實際這種方法很高明。

線程的建立時調用clone(),但是傳入了CLONE_VM參數,表示共享位址空間。

程序終止

程序終止需要釋放它占用的資源,包括task_struct。程序的析構是自身引起的,通過調用exit()系統調用,釋放資源,包括打開的檔案描述符,定時器等。最後調用exit_notify給父程序發送信号,給子程序重新找養父,養父為同一程序組的其他程序或者init程序。程序的狀态為EXIT_ZOMBIE。程序處于這個僵死狀态,它現在占用的記憶體就是核心棧、thread_info結構和task_struct結構。唯一的目的就是向它的父程序提供資訊。父程序檢索到資訊後,通知核心那是無關資訊,程序剩餘記憶體被釋放掉。

繼續閱讀