程序就是處於執行期的程式,但程序並不僅僅侷限於一段可執行程式**,也就是**段,通常程序還包括很多其它的資源,像開啟的檔案,掛起的訊號,核心內部資料,處理器狀態,乙個或多個具有記憶體對映的記憶體位址空間及乙個或多個執行執行緒以及用來存放全域性變數的資料段等。
現代作業系統的程序提供兩種虛擬機制:虛擬處理器和虛擬記憶體。虛擬處理器是指雖然實際上可能是許多程序分享乙個處理器,但是虛擬處理器給程序乙個假象讓這些程序覺得自己在獨享處理器;虛擬記憶體機制則讓程序在分配和管理記憶體的時候覺得自己擁有整個系統的所有記憶體資源。
linux支援多程序特性,可以最大化的使用cpu資源;使用者可以在同乙個cpu上執行多個使用者程式。多程序的原理是:時鐘中斷觸發程序排程程式,排程程式分時執行多個程序。這就要求每個程序能夠保留現場資訊(cpu現場、系統資源、排程資訊等)。
程序存放在叫做任務佇列(task list)的雙向迴圈鍊錶中。鍊錶中的每一項包含乙個具體程序的所有資訊,型別為task_struct,稱為程序描述符(process descriptor),
task_struct相對較大,在32位機器上,它大約有1.7kb。但如果考慮到該結構內包含了核心管理乙個程序所需的所有資訊,那麼它的大小也算相當小了。每個程序都有乙個程序描述符,記錄以下重要資訊:程序識別符號pid、程序占用的記憶體區域、相關檔案的檔案描述符、程序環境、訊號處理、同步處理等。
linux通過slab分配器分配task_struct結構,這樣能達到物件復用和快取著色(cache coloring)的目的。在2.6以前的核心中,各個程序的task_struct結構存放在它們核心棧的尾端,這樣做是為了讓那些像x86那樣暫存器較少的硬體體系結構只要通過棧指標就可以計算出它的位置,而避免使用額外的暫存器專門記錄。現在則在棧底記錄乙個thread_info結構體,每個任務的thread_info結構在它的核心棧尾端分配,其實結構體內task域指向該任務實際的task_struct結構。
由於系統需要頻繁的獲取當前程序描述符的位址,為了提高效率,linux巧妙的實現該功能,使用current巨集可以快速得到當前程序位址。硬體體系不同,該巨集的實現也不同,它必須針對專門的硬體體系結構做處理。有的硬體體系結構可以拿出乙個專門的暫存器來存放指向當前程序task_struct的指標用於加快訪問速度。而像x86這樣的體系結構,其暫存器並不富餘,就只能在核心棧的尾端建立thread_info結構,通過計算偏移間接查詢task_struct結構。
task_struct中的state描述程序的當前狀態。程序的狀態一共有5種,而程序必然處於其中一種狀態:
1. task_running(執行)——程序是可執行的,它或者正在執行,或者在執行佇列中等待執行。這是程序在使用者空間中執行唯一可能的狀態;也可以應用到核心空間中正在執行的程序。
2. task_interruptible(可中斷)——程序正在睡眠(也就是說它被阻塞)等待某些條件的達成。一旦這些條件達成,核心就會把程序狀態設定為執行,處於此狀態的程序也會因為接收到訊號而提前被喚醒並投入執行。
3. task_uninterruptible(不可中斷)——除了不會因為接收到訊號而被喚醒從而投入執行外,這個狀態與可打斷狀態相同。這個狀態通常在程序必須在等待時不受干擾或等待事件很快就會發生時出現。由於處於此狀態的任務對訊號不作響應,所以較之可中斷狀態,使用得較少。
4. task_zombie(僵死)——該程序已經結束了,但是其父程序還沒有呼叫wait4()系統呼叫。為了父程序能夠獲知它的訊息,子程序的程序描述符仍然被保留著。一旦父程序呼叫了wait4(),程序描述符就會被釋放。
5. task_stopped(停止)——程序停止執行,程序沒有投入執行也不能投入執行。通常這種狀態發生在接收到sigstop,sigtstp,sigttin,sigttou等訊號的時候。此外,在除錯期間接收到任何訊號,都會使程序進入這種狀態。
5種狀態的轉換關係如圖1:
圖1unix系統的程序之間存在乙個明顯的繼承關係,在linux系統中也是如此。所有的程序都是pid為1的init程序的後代。核心在系統啟動的最後階段啟動init程序。該程序讀取系統的初始化指令碼並執行相關程式最終完成系統啟動的整個過程。
系統中的每個程序必有乙個父程序,相應的,每個程序也可以擁有零個或多個子程序。task_struct的real_parent指向父程序、parent指向「養父程序「。父程序是實際建立子程序的程序;而「養父程序」是負責處理子程序消亡的程序,在大多數情況下,父程序就是「養父程序「。但是也有例外,例如,當父程序比子程序提前消亡時,父程序會幫子程序重新尋找乙個「養父程序」,通常是程序1。children成員是父程序的子程序鍊錶;sibling成員是子程序的兄弟程序鍊錶。
linux系統為每個使用者程序分配了兩個棧:使用者棧和核心棧。當乙個程序在使用者空間執行時,系統使用使用者棧;當在核心空間執行時,系統使用核心棧。由於核心棧位址空間的限制,核心棧不會分配很大的空間。此外,核心程序只有核心棧,沒有使用者棧。
當程序從使用者空間陷入到核心空間時,首先,作業系統在核心棧中記錄使用者棧的當前位置,然後將棧暫存器指向核心棧;核心空間的程式執行完畢後,作業系統根據核心棧中記錄的使用者棧位置,重新將棧暫存器指向使用者棧。由於每次從核心空間中返回時,核心棧肯定已經使用完畢,所以從使用者棧切換到核心棧時,只需要簡單的將棧暫存器指向核心棧頂即可,不需要做什麼特殊處理。
unix的程序建立很特別。許多其他的作業系統都提供了產生程序的機制,首先在新的位址空間裡建立程序,讀入可執行檔案,最後開始執行。unix採用了與眾不同的實現方法,它把上述步驟分解到兩個單獨的函式中去執行:fork()和exec()。首先fork()通過拷貝當前程序建立乙個子程序。exec()函式負責讀取可執行檔案並將其載入位址空間開始執行。
傳統的fork()系統呼叫直接把所有的資源複製給新建立的程序。這種實現過於簡單並且效率低下,因為它拷貝的資料也許並不共享,更糟的情況是,如果新程序打算立即執行乙個新的映像,那麼所有的拷貝都將前功盡棄。
linux的fork()使用寫時拷貝(copy-on-write)頁實現。寫時拷貝是一種可以推遲甚至免除拷貝資料的技術。核心此時並不複製整個程序位址空間,而是讓父程序和子程序共享乙個拷貝,只有在需要的時候,資料才會被複製,從而使各個程序擁有各自的拷貝。也就是說,資源的複製只有在需要寫入的時候才進行,在此之前,只是以唯讀方式共享。這種技術使位址空間上的頁的拷貝被推遲到實際發生寫入的時候才進行,在頁根本不會被寫入的情況下他們就無需複製了。
fork()的實際開銷就是複製父程序的頁表以及給子程序建立唯一的程序描述符。在一般情況下,程序建立後都會馬上執行乙個可執行檔案,這種優化可以避免拷貝大量根本就不會被使用的資料。
fork()和vfork()都能建立程序,這兩個函式最後都會去呼叫clone(),然後由clone()去呼叫do_fork()。do_fork()首先呼叫copy_process()建立新程序,然後呼叫wake_up_new_task()將程序放入執行佇列中並啟動新程序。
copy_process()主要完成以下工作:
1、引數標誌檢測,處理存在衝突或者依賴關係的引數標誌組合;
2、不能為init程序建立兄弟程序,防止這些兄弟程序在退出時變為殭屍程序(程序0不會處理殭屍程序)。如果當前程序為init,則不能指定clone_parent引數標誌;
3、呼叫dup_task_struct()為新程序建立乙個task_struct結構、乙個thread_info結構和核心棧(核心棧的末端就是thread_info結構,所以這兩者共享了頁結構),此時父子程序的描述符、核心棧內容完全一致(除了thread_info相關的內容);在核心棧與thread_info之間寫入「核心棧防越界魔數」stack_end_magic(0x57ac6e9d);
4、判斷當前使用者所擁有的程序數是否超出最大值;
5、呼叫copy_flags()設定新程序的flags成員,表明程序是否擁有超級使用者許可權的pf_superpriv標誌被清0,表明程序還沒有呼叫exec()函式的pf_forknoexec標誌被設定;
6、初始化新程序的孩子鍊錶和兄弟鍊錶指標指向新程序自己(即無孩子和兄弟程序);
7、設定新程序的時間為0;設定程序建立時間為當前系統時間;
8、進行排程程式相關的設定,設定執行狀態為task_running,繼承父程序的優先順序值;
9、根據引數標誌拷貝或共享檔案系統、訊號量、訊號處理函式、開啟的檔案、位址空間、命名空間和cpu暫存器組等資源;預設情況下,子程序拷貝父程序的資源;
10、呼叫alloc_pid()為新程序分配乙個有效的pid;
11、根據引數標誌,設定子程序與父程序的關係以及與程序組的關係;
12、返回新程序的程序描述符。
總而言之,fork()的作用就是「複製」乙份父程序及其資源,搭建好了乙個程序執行時所需的必要環境;當呼叫exec()函式族時,才會將程序位址空間的內容替換為子程序的可執行檔案。
除了不拷貝父程序的頁表項之外,vfork()系統呼叫和fork()的功能相同。子程序作為父程序的乙個單獨的執行緒在它的位址空間裡執行,父程序被阻塞,直到子程序退出或執行exec()。子程序不能向位址空間寫入。在過去的3bsd時期,這個優化是很有意義的,那時沒有寫時拷貝,現在由於fork()引入了寫時拷貝頁並且明確了子程序先執行,vfork()的好處就僅限於不拷貝父程序的頁表項了。如果以後fork()實現了寫時拷貝頁表項,vfork()就完全沒用了。
程序在執行結束,或接受到它既不能處理也不能忽略的訊號,或異常時,都會被終結。此時,依靠do_exit()(在kernel/exit.c檔案中)把與程序相關聯的所有資源都被釋放掉(假設程序是這些資源的唯一使用者)。至此,與程序相關的所有資源都被釋放掉了。程序不可執行(實際上也沒有位址空間讓它執行)並處於task_zombie狀態。它占用的所有資源就是核心棧、thread_info和task_struct。此時程序存在的唯一目的就是想它的父程序提供資訊。在父程序獲得已終結的子程序的資訊後,或者通知核心它並不關注那些資訊後,子程序持有的task_struct等剩餘記憶體才被釋放。
如果父程序在子程序之前退出,必須有機制保證子程序能找到乙個新的父類,否則的話這些成為孤兒的程序就會在退出時永遠處於僵死狀態,白白的耗費記憶體。解決方法是給子程序在當前執行緒組內找乙個執行緒作為父親,如果不行,就讓init做它們的父程序。
Linux程序管理概述
程序是正在執行中的程式。當程式被執行時,執行人的許可權和屬性,以及程式的 都會被加載入記憶體,作業系統給這個程序分配乙個 id,稱為 pid 程序 id 也就是說,在作業系統中,所有可以執行的程式與命令都會產生程序。只是有些程式和命令非常簡單,如 ls 命令 touch 命令等,它們在執行完後就會結...
3 1 程序管理概述
cpu 的工作原理就是取指執行,但如果遇到 io 操作,那麼 cpu 將等待 io 的完成,io 操作的速度很慢,v cpu v io 10 6 1,也就是說,執行乙個 io 操作,cpu 能執行 10 6 條指令,所以為了充分利用 cpu,就要在 io 的時候執行其它程式。這裡引出了乙個概念,併發...
Linux記憶體管理概述
該圖 作者aryang 下面對各部分進行概述。linux程序的線性位址空間 程序虛擬位址空間分布 0 3g是user位址空間,3 4g是kernel位址空間。適用於arm x86等,mips按0 2g,2 3g劃分 1.緊接著核心資料區向上是mem map全域性page陣列。3.核心動態載入驅動模組...