天天看點

Linux核心設計與實作 學習筆記(二)程序管理

      程序和線程的概念我就不講了。總之,你記着:核心排程的對象是線程,而不是程序。linux系統中的線程很特别,它對線程和程序并不做特别區分。程序的另外一個名字叫任務(task).我和作者一樣,習慣了把使用者空間運作的程式叫做程序,把核心中運作的程式叫做任務。

      核心把程序存放在叫做任務隊列(task list)的雙向循環連結清單中,連結清單中的每一項都是類型為task_struct,名稱叫做程序描述符(process descriptor)的結構,該結構定義在include/linux/sched.h檔案中,它包含了一個具體程序的所有資訊。

      linux通過slab配置設定器配置設定task_struct結構,這樣能達到對象複用和緩存着色的目的。在2.6以前的核心中,各個程序的task_struct存放在它們核心棧的尾端。由于現在用slab配置設定器動态生成task_struct,是以隻需在棧底或棧頂建立一個新的結構(struct thread_info),他在asm/thread_info.h中定義,需要的請具體參考。每個任務中的thread_info結構在它的核心棧中的尾端配置設定,結構中task域存放的是指向該任務實際task_struct指針。

      在核心中,通路任務通常需要獲得指向其task_struct指針。實際上,核心中大部分處理程序的代碼都是通過task_struct進行的。通過current宏查找到目前正在執行的程序的程序描述符就顯得尤為重要。在x86系統上,current把棧指針的後13個有效位屏蔽掉,用來計算thread_info的偏移,該操作通過current_thread_info函數完成,彙編代碼如下:

movl $-8192, %eax

andl  %esp, %eax

      最後,current再從thread_info的task域中提取并傳回task_struct的值:current_thread_info()->task;

      程序描述符中的state域描述了程序的目前狀态。系統中的每個程序都必然處于五種程序狀态中的一種,什麼運作态啦,阻塞态啦,它們之間轉化的條件啦等等,這一點我也不細說了,為啥?随便一本作業系統的書裡,講得都比我好,要講就要講别人講不好的,是不?現在我關心的問題是:當核心需要調整某個程序的狀态時,該怎麼做?這時最好使用set_task_state(task, state)函數,該函數将指定的程序設定為指定的狀态,必要的時候,它會設定記憶體屏蔽來強制其他處理器作重新排序。(一般隻有在SMP系統中有此必要)否則,它等價于:task->state = state; 另外set_current_state(state)和set_task_state(current, state)含義是等價的。

     一般程式在使用者空間執行。當一個程式執行了系統調用或者觸發了某個異常,它就陷入核心空間。系統調用和異常處理程式是對核心明确定義的接口,程序隻有通過這些接口才能陷入核心執行----對核心的所有通路都必須通過這些接口。

     linux程序之間存在一個明顯的繼承關系。所有的程序都是PID為1的init程序的後代,核心在系統啟動的最後階段啟動init程序。該程序讀取系統的初始化腳本并執行其他的相關程式,最終完成系統啟動的整個過程。

     系統中的每個程序必有一個父程序,每個程序也可以擁有一個或多個子程序。程序既然有父子之稱,當然就有兄弟之意了。每個task_struct都包含一個指向其父程序task_struct且叫做parent的指針,同時包含一個稱為children的子程序連結清單。是以通路父程序:struct task_struct *task = current->parent;按照如下方式通路子程序:

struct task_struct *task;
struct list_head *list;
list_for_each(list, &current->children){
           task = list_entry(list, struct task_struct, sibling);
}
           

       其中init程序描述符是作為init_task靜态配置設定的。通過上面的init程序,父子程序關系,兄弟程序關系以及程序描述符的結構,我們可以得到一個驚人的事實:可以通過這種關系從系統的任何一個程序出發查找到任意指定的其他程序。而且方式還挺多的,這個就看書了,内容挺多我就不說了,隻是最後需要指出的是,在一個擁有大量程序的系統中通過重複來周遊所有的程序是非常耗費時間的,是以,如果沒有充足的理由千萬别這樣做。愛要一萬個理由,這麼做呢,沒看出來.

      許多的作業系統都提供了産生程序的機制,linux這優秀的系統也不例外。Unix很簡單:首先fork()通過拷貝目前程序建立一個子程序。子父程序的差別僅僅在于PID,PPID和某些資源和統計量。然後exec()函數負責讀取可執行檔案并将其載入位址空間并執行。從上面分析可以看出,傳統的fork()系統調用直接把所有的資源複制給心建立的程序。這種方式過于簡單但效率底下。在Linux下使用了一種叫做寫時拷貝(copy-on-write)頁實作。這種技術原理是:記憶體并不複制整個程序位址空間,而是讓父程序和子程序共享同一拷貝,隻有在需要寫入的時候,資料才會被複制。不懂?簡單點,就是資源的複制隻是發生在需要寫入的時候才進行,在此之前,都是以隻讀的方式共享。

      linux通過clone()系統調用實作fork(),通過參數标志來說父子程序共享的資源。無論是fork(),還是vfork(),__clone()最後都根據各自需要的參數标志去調用clone().然後有clone()去調用do_fork().這樣一說,我想大家明白我的意思了,問題的關鍵糾結于do_fork(),它定義在kernel/fork.c中,完成了大部分工作,該函數調用copy_process()函數,然後讓進城開始運作,copy_precess()函數完成的工作很有意思:

1.調用dup_task_struct()為新程序建立一個核心棧,它的定義在kernel/fork.c檔案中。該函數調用copy_process()函數。然後讓程序開始運作。從函數的名字dup就可知,此時,子程序和父程序的描述符是完全相同的。
2.檢查這個新建立的的子程序後,目前使用者所擁有的程序數目沒有超過給他配置設定的資源的限制。
3.現在,子程序開始使自己與父程序差別開來。程序描述符内的許多成員都要被清0或設為初始值。
4.接下來,子程序的狀态被設定為TASK_UNINTERRUPTIBLE以保證它不會投入運作。
5.調用copy_flags()以更新task_struct的flags成員,表明程序是否擁有超級使用者權限的PF_SUPERPRIV标志被清0。表明程序還沒有調用exec函數的PF_FORKNOEXEC标志。
6.調用get_pid()為新程序擷取一個有效的PID.
7.根據傳遞給clone()的參數标志,拷貝或共享打開的檔案,檔案系統資訊,信号處理函數。程序位址空間和命名空間等。一般情況下,這些資源會被給定程序的所有線程共享;否則,這些資源對每個程序是不同的,是以被拷貝到這裡.
8.讓父程序和子程序平分剩餘的時間片
9.最後,作掃尾工作并傳回一個指向子程序的指針。
           

      經過上面的操作,再回到do_fork()函數,如果copy_process()函數成功傳回。新建立的子程序被喚醒并讓其投入運作。核心有意選擇子程序先運作。因為一般子程序都會馬上調用exec()函數,這樣可以避免寫時拷貝的額外開銷。如果父程序首先執行的話,有可能會開始向位址空間寫入。

      說完了fork,接下來說說他的兄弟---vfork(),兄弟就是兄弟,這像!兩者功能相同,不同點在于vfork()不拷貝父程序的頁表項。子程序作為父程序的一個單獨的線程在它的位址空間裡運作,父程序被阻塞,直到子程序退出或執行exec(),子程序不能向位址空間寫入。按照剛才的方法,分析一下vfork(),它是通過向clone()系統調用傳遞一個特殊标志來進行的,過程如下:

1.在調用copy_process時,task_struct的vfor_done成員被設定為NULL
2.在執行do_fork()時,如果給定特别标志,則vfork_done會指向一個特殊位址。
3.子程序開始執行後,父程序不是馬上恢複執行,而是一直等待,直到子程序通過vfork_done指針向它發送信号。
4.在調用mm_release()時,該函數用于程序退出記憶體位址空間,如果vfork_done不為空,會向父程序發送信号。
5.回到do_fork(),父程序醒來并傳回。
           

      上面步驟的順利完成就意味着父子程序将會在各自的位址空間裡運作。說句真的,通過研究發現這樣的開銷是降低了,但技術上不算咋優良。

      如果說程序是80年代早上初升的太陽, 那不得不說的線程就是目前正午的烈日。線程機制提供了在同一程式内共享記憶體位址空間運作的一組線程。線程機制支援并發程式設計技術,可以共享打開的檔案和其他資源。如果你的系統是多核心的,那多線程技術可保證系統的真正并行。然而,有一件令人奇怪的事情,在linux中,并沒有線程這個概念,linux中所有的線程都當作程序來處理,換句話說就是在核心中并沒有什麼特殊的結構和算法來表示線程。那麼,說了這多,到底在linux中啥是線程,我們說在linux中,線程僅僅是一個使用共享資源的程序。每個線程都擁有一個隸屬于自己的task_struct.是以說線程本質上還是程序,隻不過該程序可以和其他一些程序共享某些資源資訊。

      這樣一說,後面就明白了也好解決了,兩者既然屬于同一類,那建立的方式也是一樣的,但總要有不同啊,這個不同咋展現呢,這個好辦,我們在調用clone()的時候傳遞一些參數标志來指明需要共享的資源就可以了:clone(CLONE_VM | CLONE_FS | CLONE_FILES | CLONE_SIGHAND, 0);這段代碼産生的結果和調用fork()差不多,隻是父子倆共享位址空間,檔案系統資源,檔案描述符和信号處理程式。換個說法就是這裡的父程序和子程序都叫做線程。也就是說clone()的參數決定了clone的行為,具體有哪些參數,我是個懶人,也不想說了。

      前邊說的主要是使用者級線程,現在我們接着來說說核心級線程。核心線程和使用者級線程的差別在于核心線程沒有獨立的位址空間(實際上它的mm指針被設定為NULL).它也可以被排程也可以被搶占。核心線程也隻能由其他核心線程建立。方法如下:int kernel_thread(int (*fn)(void *), void *arg, unsigned long flags).新的任務也是通過像普通的clone()系統調用傳遞特定的flags參數而建立的。上面函數傳回時,父程序退出,并傳回一個子線程task_struct的指針。子程序開始運作fn指向的函數,arg是運作時需要用到的參數。一個特殊的clone标志CLONE_KERNEL定義了核心線程常用到參數标志:CLONE_FS, CLONE_FILES, CLONE_SIGHAND.大部分的核心線程把這個标志傳遞給它們的flags參數。

      我雖有才,還是不如書上說的好啊,講了那麼多的建立,出生,突然來點終結的的話, 多少有點感傷啊。但感傷歸感傷,程序終歸是要終結的。一個程序終結時必須釋放它所占用的資源并把這一消息告訴其父程序。程序終止的方式有很多種,程序的析構發生在它調用exit()之後,即可能顯示地調用這個系統調用,也可能隐式地從某個程式的主函數傳回。當程序接受到它即不能處理也不能忽略的信号或異常時,它還可能被動地終結。但話說回來,不管程序怎麼終結,該任務大部分都要靠do_exit()來完成,它定義在kernel/exit.c中,具體的工作如下所示:

1.将tast_struct中的标志成員設定為PF_EXITING.
2.如果BSD的程序記賬功能是開啟的,要調用acct_process來輸出記賬資訊。
3.調用__exit_mm()函數放棄程序占用的mm_struct,如果沒有别的程序使用它們即沒被共享,就徹底釋放它們。
4.調用sem_exit()函數。如果程序排隊等候IPC信号,它則離開隊列。
5.調用__exit_files(), __exit_fs(), __exit_namespace()和exit_sighand()以分别遞減檔案描述符,檔案系統資料,程序名字空間和信号處理函數的引用計數。當引用計數的值為0時,就代表沒有程序在使用這些資源,此時就釋放。
6.把存放在task_struct的exit_code成員中的任務退出代碼置為exit()提供的代碼中,或者去完成任何其他由核心機制制定的退出動作。
7.調用exit_notify()向父程序發送信号,将子程序的父程序重新設定為線程組中的其他線程或init程序,并把程序狀态設為TASK_ZOMBIE.
8.最後,調用schedule()切換到其他程序。
           

      經過上面的步驟,與程序相關的資源都被釋放掉了,它以不能夠再運作且處于TASK_ZOMBLE狀态。現在它占用的所有資源就是儲存threadk_info的核心棧和儲存tast_struct結構的那一小片slab。此時程序存在的唯一目的就是向它的父程序提供資訊。

      僵死的程序是不能再運作的。但系統仍然保留它的程序描述符,這樣就有辦法在子程序終結時仍可以獲得它的資訊。在父程序獲得已終結的子程序的資訊後,子程序的task_struct結構才被釋放。

      熟悉linux系統中子程序相關知識的我們都知道在linux中有一系列wait()函數,這些函數都是基于系統調用wait4()實作的。它的動作就是挂起調用它的程序直到其中的一個子程序退出,此時函數會傳回該退出子程序的PID.調用該函數時提供的指針會包含子函數退出時的退出代碼。最終釋放程序描述符時,會調用release_task(),完成的工作如下:

1.調用free_uid()來減少該程序擁有者的程序使用計數。
2.調用unhash_process()從pidhash上删除該程序,同時也要從task_list中删除該程序。
3.如果這個程序正在被ptrace追蹤,将追蹤程序的父程序重設為其最初的父程序并将它從ptrace_list上删除。
4.最後,調用put_task_struct釋放程序核心棧和thread_info結構所占的頁,并釋放task_struct所占的slab高速緩存.
           

       至此,程序描述符和所有程序獨享的資源就全部釋放掉了。

       最後,我們讨論程序相關的最後一個問題:前邊的一切看似很完美,很美好,美好讓人還怕,不是麼?哪裡出問題了,父程序建立子程序,然後子程序退出處釋放占用的資源并告訴父程序自己的PID以及退出狀态。問題就出在這裡,子程序一定能保證在父程序前邊退出麼,這是沒辦法保證的,是以必須要有機制來保證子程序在這種情況下能找到一個新的父程序。否則的話,這些成為孤兒的程序就會在退出時永遠處于僵死狀态,白白的耗費記憶體。解決這個問題的辦法,就是給子程序在目前線程組内找一個線程作為父親,如果這樣也不行(運氣太背了,不是)。在do_exit()會調用notify_present(),該函數會通過forget_original_parent來執行尋父過程,具體我就不講了,講到這個詳細的地步,還不自己看看,我沒辦法了.

       一旦系統給程序成功地找到和設定了新的父程序,就不會再有出現駐留僵死程序的危險了,init程序會例行調用wait()來等待子程序,清除所有與其相關的僵死程序。

繼續閱讀