第四章 進程管理 本章重點討論Linux內核如何在系統中創建、管理以及刪除進程。 進程在操作系統中執行特定的任務。而程序是存儲在磁盤上包含可執行機器指令和數據的靜態實體。進程或者任務是處於活動狀態的計算機程序。 進程是一個隨執行過程不斷變化的實體。
第四章 進程管理
本章重點討論Linux內核如何在系統中創建、管理以及刪除進程。
進程在操作系統中執行特定的任務。而程序是存儲在磁盤上包含可執行機器指令和數據的靜態實體。進程或者任務是處於活動狀態的計算機程序。
進程是一個隨執行過程不斷變化的實體。和程序要包含指令和數據一樣,進程也包含程序計數器和所有CPU寄存器的值,同時它的堆棧中存儲著如子程序參數、返回地址以及變量之類的臨時數據。當前的執行程序,或者說進程,包含著當前處理器中的活動狀態。Linux是一個多處理操作系統。進程具有獨立的權限與職責。如果系統中某個進程崩潰,它不會影響到其余的進程。每個進程運行在其各自的虛擬地址空間中,通過核心控制下可靠的通訊機制,它們之間才能發生聯系。
進程在生命期內將使用系統中的資源。它利用系統中的CPU來執行指令,在物理內存來放置指令和數據。使用文件系統提供的功能打開並使用文件,同時直接或者間接的使用物理設備。Linux必須跟蹤系統中每個進程以及資源,以便在進程間實現資源的公平分配。如果系統有一個進程獨占了大部分物理內存或者CPU的使用時間,這種情況對系統中的其它進程是不公平的。
系統中最寶貴的資源是CPU,通常系統中只有一個CPU。Linux是一個多處理操作系統,它最終的目的是:任何時刻系統中的每個CPU上都有任務執行,從而提高CPU的利用率。如果進程個數多於CPU的個數,則有些進程必須等待到CPU空閒時才可以運行。多處理是的思路很簡單;當進程需要某個系統資源時它將停止執行並等待到資源可用時才繼續運行。單處理系統中,如DOS,此時CPU將處於空等狀態,這個時間將被浪費掉。在多處理系統中,因為可以同時存在多個進程,所以當某個進程開始等待時,操作系統將把CPU控制權拿過來並交給其它可以運行的進程。調度器負責選擇適當的進程來運行,Linux使用一些調度策略以保證CPU分配的公平性。
Linux支持多種類型的可執行文件格式,如ELF,JAVA等。由於這些進程必須使用系統共享庫,所以對它們的管理要具有透明性。
4.1 Linux進程
為了讓Linux來管理系統中的進程,每個進程用一個task_struct數據結構來表示(任務與進程在Linux中可以混用)。數組task包含指向系統中所有task_struct結構的指針。
這意味著系統中的最大進程數目受task數組大小的限制,缺省值一般為512。創建新進程時,Linux將從系統內存中分配一個task_struct結構並將其加入task數組。當前運行進程的結構用current指針來指示。
Linux還支持實時進程。這些進程必須對外部時間作出快速反應(這就是“實時”的意思),系統將區分對待這些進程和其他進程。雖然task_struct數據結構龐大而復雜,但它可以分成一些功能組成部分:
State
進程在執行過程中會根據環境來改變state。Linux進程有以下狀態:
Running
進程處於運行(它是系統的當前進程)或者准備運行狀態(它在等待系統將CPU分配給它)。
Waiting
進程在等待一個事件或者資源。Linux將等待進程分成兩類;可中斷與不可中斷。可中斷等待進程可以被信號中斷;不可中斷等待進程直接在硬件條件等待,並且任何情況下都不可中斷。
Stopped
進程被停止,通常是通過接收一個信號。正在被調試的進程可能處於停止狀態。
Zombie
這是由於某些原因被終止的進程,但是在task數據中仍然保留task_struct結構。 它象一個已經死亡的進程。
Scheduling Information
調度器需要這些信息以便判定系統中哪個進程最迫切需要運行。
Identifiers
系統中每個進程都有進程標志。進程標志並不是task數組的索引,它僅僅是個數字。每個進程還有一個用戶與組標志,它們用來控制進程對系統中文件和設備的存取權限。
Inter-Process Communication
Linux支持經典的Unix IPC機制,如信號、管道和信號燈以及系統V中IPC機制,包括共享內存、信號燈和消息隊列。我們將在IPC一章中詳細討論Linux中IPC機制。
Links
Linux系統中所有進程都是相互聯系的。除了初始化進程外,所有進程都有一個父進程。新進程不是被創建,而是被復制,或者從以前的進程克隆而來。每個進程對應的task_struct結構中包含有指向其父進程和兄弟進程(具有相同父進程的進程)以及子進程的指針。我們可以使用pstree 命令來觀察Linux系統中運行進程間的關系:
init(1)-+-crond(98)
|-emacs(387)
|-gpm(146)
|-ine
td(110)
|-kerneld(18)
|-kflushd(2)
|-klogd(87)
|-kswapd(3)
|-login(160)---bash(192)---emacs(225)
|-lpd(121)
|-mingetty(161)
|-mingetty(162)
|-mingetty(163)
|-mingetty(164)
|-login(403)---bash(404)---pstree(594)
|-sendmail(134)
|-syslogd(78)
`-update(166)
另外,系統中所有進程都用一個雙向鏈表連接起來,而它們的根是init進程的task_struct數據結構。這 個鏈表被Linux核心用來尋找系統中所有進程,它對ps或者kill命令提供了支持。
Times and Timers
核心需要記錄進程的創建時間以及在其生命期中消耗的CPU時間。時鐘每跳動一次,核心就要更新保存在jiffies變量中,記錄進程在系統和用戶模式下消耗的時間量。Linux支持與進程相關的interval定時器,進程可以通過系統調用來設定定時器以便在定時器到時後向它發送信號。這些定時器可以是一次性的或者周期性的。
File system
進程可以自由地打開或關閉文件,進程的task_struct結構中包含一個指向每個打開文件描敘符的指針以及指向兩個VFS inode的指針。每個VFS inode唯一地標記文件中的一個目錄或者文件,同時還對底層文件系統提供統一的接口。Linux對文件系統的支持將在filesystem一章中詳細描敘。這兩個指針,一個指向進程的根目錄,另一個指向其當前或者pwd目錄。pwd從Unix命令pwd中派生出來, 用來顯示當前工作目錄。這兩個VFS inode包含一個count域,當多個進程引用它們時,它的值將增加。這就是為什麼你不能刪除進程當前目錄,或者其子目錄的原因。
Virtual memory
多數進程都有一些虛擬內存(核心線程和後台進程沒有),Linux核心必須跟蹤虛擬內存與系統物理內存的映射關系。
Processor Specific Context
進程可以認為是系統當前狀態的總和。進程運行時,它將使用處理器的寄存器以及堆棧等等。進程被掛起時,進程的上下文-所有的CPU相關的狀態必須保存在它的task_struct結構中。當調度器重新調度該進程時,所有上下文被重新設定。
4.2 Identifiers
和其他Unix一樣,Linux使用用戶和組標志符來檢查對系統中文件和可執行映象的訪問權限。Linux系統中所有的文件都有所有者和允許的權限,這些權限描敘了系統使用者對文件或者目錄的使用權。基本的權限是讀、寫和可執行,這些權限被分配給三類用戶:文件的所有者,屬於相同組的進程以及系統中所有進程。每類用戶具有不同的權限,例如一個文件允許其擁有者讀寫,但是同組的只能讀而其他進程不允許訪問。
Linux使用組將文件和目錄的訪問特權授予一組用戶,而不是單個用戶或者系統中所有進程。如可以為某個軟件項目中的所有用戶創建一個組,並將其權限設置成只有他們才允許讀寫項目中的源代碼。一個進程可以同時屬於多個組(最多為32個),這些組都被放在進程的task_struct中的group數組中。只要某組進程可以存取某個文件,則由此組派生出的進程對這個文件有相應的組訪問權限。
task_struct結構中有四對進程和組標志符:
uid, gid
表示運行進程的用戶標志符和組標志符。
effective uid and gid
有些程序可以在執行過程中將執行進程的uid和gid改成其程序自身的uid和gid(保存在描敘可執行映象的VFS inode屬性中)。這些程序被稱為setuid程序,常在嚴格控制對某些服務的訪問時使用,特別是那些為別的進程而運行的進程,例如網絡後台進程。有效uid和gid是那些setuid執行過程在執行時變化出的uid 和gid。當進程試圖訪問特權數據或代碼時,核心將檢查進程的有效gid和uid。
file system uid and gid
它們和有效uid和gid相似但用來檢驗進程的文件系統訪問權限。如運行在用戶模式下的NFS
服務器存取文件時,NFS文件系統將使用這些標志符。此例中只有文件系統uid和gid發生了改變(而非有效uid和gid)。這樣可以避免惡意用戶向NFS服務器發送KILL信號。
saved uid and gid
POSIX標准中要求實現這兩個標志符,它們被那些通過系統調用改變進程uid和gid的程序使用。當進程的原始uid和gid變化時,它們被用來保存真正的uid和gid。
4.3 調度
所有進程部分時間運行於用戶模式,部分時間運行於系統模式。如何支持這些模式,底層硬件的實現各不相同,但是存在一種
安全機制可以使它們在用戶模式和系統模式之間來回切換。用戶模式的權限比系統模式下的小得多。進程通過系統調用切換到系統模式繼續執行。此時核心為進程而執行。在Linux中,進程不能被搶占。只要能夠運行它們就不能被停止。當進程必須等待某個系統事件時,它才決定釋放出CPU。例如進程可能需要從文件中讀出字符。一般等待發生在系統調用過程中,此時進程處於系統模式;處於等待狀態的進程將被掛起而其他的進程被調度管理器選出來執行。
進程常因為執行系統調用而需要等待。由於處於等待狀態的進程還可能占用CPU時間,所以Linux采用了預加載調度策略。在此策略中,每個進程只允許運行很短的時間:200毫秒,當這個時間用完之後,系統將選擇另一個進程來運行,原來的進程必須等待一段時間以繼續運行。這段時間稱為時間片。
調度器必須選擇最迫切需要運行而且可以執行的進程來執行。
可運行進程是一個只等待CPU資源的進程。Linux使用基於優先級的簡單調度算法來選擇下一個運行進程。當選定新進程後,系統必須將當前進程的狀態,處理器中的寄存器以及上下文狀態保存到task_struct結構中。同時它將重新設置新進程的狀態並將系統控制權交給此進程。為了將CPU時間合理的分配給系統中每個可執行進程,調度管理器必須將這些時間信息也保存在task_struct中。
policy
應用到進程上的調度策略。系統中存在兩類Linux進程:普通與實時進程。實時進程的優先級要高於其它進程。如果一個實時進程處於可執行狀態,它將先得到執行。實時進程又有兩種策略:時間片輪轉和先進先出。在時間片輪轉策略中,每個可執行實時進程輪流執行一個時間片,而先進先出策略每個可執行進程按各自在運行隊列中的順序執行並且順序不能變化。
priority
調度管理器分配給進程的優先級。同時也是進程允許運行的時間(jiffies)。系統調用renice可以改變進程的優先級。
rt_priority
Linux支持實時進程,且它們的優先級要高於非實時進程。調度器使用這個域給每個實時進程一個相對優先級。同樣可以通過系統調用來改變實時進程的優先級。
counter
進程允許運行的時間(保存在jiffies中)。進程首次運行時為進程優先級的數值,它隨時間變化遞減。
核心在幾個位置調用調度管理器。如當前進程被放入等待隊列後運行或者系統調用結束時,以及從系統模式返回用戶模式時。此時系統時鐘將當前進程的counter值設為0以驅動調度管理器。每次調度管理器運行時將進行下列操作:
kernel work
調度管理器運行底層處理程序並處理調度任務隊列。kernel一章將詳細描敘這個輕量級核心線程。
Current process
當選定其他進程運行之前必須對當前進程進行一些處理。
如果當前進程的調度策略是時間片輪轉,則它被放回到運行隊列。
如果任務可中斷且從上次被調度後接收到了一個信號,則它的狀態變為Running。
如果當前進程超時,則它的狀態變為Running。
如果當前進程的狀態是Running,則狀態保持不變。 那些既不處於Running狀態又不是可中斷的進程將會從運行隊列中刪除。這意味著調度管理器選擇運行進程時不會將這些進程考慮在內。
Process selection
調度器在運行隊列中選擇一個最迫切需要運行的進程。如果運行隊列中存在實時進程(那些具有實時調度策略的進程),則它們比普通進程更多的優先級權值。普通進程的權值是它的counter值,而實時 進程則是counter加上1000。這表明如果系統中存在可運行的實時進程,它們將總是在任何普通進程之前運行。如果系統中存在和當前進程相同優先級的其它進程,這時當前運行進程已經用掉了一些時間片,所以它將處在不利形勢(其counter已經變小);而原來優先級與它相同的進程的counter值顯然比它大,這樣位於運行隊列中最前面的進程將開始執行而當前進程被放回到運行隊列中。在存在多個相同優先級進程的平衡系統中,每個進程被依次執行,這就是Round Robin策略。然而由於進程經常需要等待某些資源,所以它們的運行順序也常發變化。
Swap processes
如果系統選擇其他進程運行,則必須被掛起當前進程且開始執行新進程。進程執行時將使用寄存器、物理內存以及CPU。每次調用子程序時,它將參數放在寄存器中並把返回地址放置在堆棧中,所以調度管理器總是運行在當前進程的上下文。雖然可能在特權模式或者核心模式中,但是仍然處於當前運行進程中。當掛起進程的執行時,系統的機器狀態,包括程序計數器(PC)和全部的處理器寄存器,必須存儲在進程的task_struct數據結構中。同時加載新進程的機器狀態。這個過程與系統類型相關,不同的CPU使用不同的方法完成這個工作,通常這個操作需要硬件輔助完成。
進程的切換發生在調度管理器運行之後。以前進程保存的上下文與當前進程加載時的上下文相同,包括進程程序計數器和寄存器內容。
如果以前或者當前進程使用了虛擬內存,則系統必須更新其頁表入口,這與具體體系結構有關。如果處理器使用了轉換旁視緩沖或者緩沖了頁表入口(如Alpha A
XP),那麼必須沖刷以前運行進程的頁表入口。
4.3.1 多處理器系統中的調度
在Linux世界中,多CPU系統非常少見。但是Linux上已經做了很多工作來保證它能運行在SMP(對稱多處理)機器上。Linux能夠在系統中的CPU間進行合理的負載平衡調度。這裡的負載平衡工作比調度管理器所做的更加明顯。
在多處理器系統中,人們希望每個處理器總處與工作狀態。當處理器上的當前進程用完它的時間片或者等待系統資源時,各個處理器將獨立運行調度管理器。SMP系統中一個值得注意的問題是系統中不止一個idle進程。在單處理器系統中,idle進程是task數組中的第一個任務,在SMP系統中每個CPU有一個idle進程,同時每個CPU都有一個當前進程,SMP系統必須跟蹤每個處理器中的idle進程和當前進程。
在SMP系統中,每個進程的task_struct結構中包含著當前運行它的處理器的編號以及上次運行時處理器的編號。 把進程每次都調度到不同CPU上執行顯然毫無意義,Linux可以使用processor_mask來使得某個進程只在一個或者幾個處理器上運行:如果N位置位,則進程可在處理器N上運行。當調度管理器選擇新進程運行時,它 不會考慮一個在其processor_mask中在當前處理器位沒有置位的進程。同時調度管理器將給予上次在此處理器中運行的進程一些優先權,因為將進程遷移到另外處理器上運行將帶來
性能的損失。
4.4 文件
圖4.1 進程所使用的文件
圖4.1給出了兩個描敘系統中每個進程所使用的文件系統相關信息。第一個fs_struct包含了指向進程的VFS inode和其屏蔽碼。這個屏蔽碼值是創建新文件時所使用的缺省值,可以通過系統調用來改變。
第二個數據結構files_struct包含了進程當前所使用的所有文件的信息。程序從標准輸入中讀取並寫入到標准輸出中去。任何錯誤信息將輸出到標准錯誤輸出。這些文件有些可能是真正的文件,有的則是輸出/輸入終端或者物理設備,但程序都將它們視為文件。每個文件有一個描敘符,files_struct最多可以包含256個文件數據結構,它們分別描敘一個被當前進程使用的文件。f_mode域表示文件將以何種模式創建:只讀 、讀寫還是只寫。f_pos中包含下一次文件讀寫操作開始位置。f_inode指向描敘此文件的VFS inode, f_ops指向一組可以對此文件進行操作的函數入口地址指針數組。這些抽象接口十分強大,它們使得Linux 能夠支持多種文件類型。在Linux中,管道是用我們下面要討論的機制實現的。
每當打開一個文件時,位於files_struct中的一個空閒文件指針將被用來指向這個新的文件結構。Linux進 程希望在進程啟動時至少有三個文件描敘符被打開,它們是標准輸入,標准輸出和標准錯誤輸出,一般進程 會從父進程中繼承它們。這些描敘符用來索引進程的fd數組,所以標准輸入,標准輸出和標准錯誤輸出分別 對應文件描敘符0,1和2。每次對文件的存取都要通過文件數據結構中的文件操作子程序和VFS inode一起來完成,
4.5 虛擬內存
進程的虛擬內存包括可執行代碼和多個資源數據。首先加載的是程序映象,例如ls。ls和所有可執行映象一樣,是由可執行代碼和數據組成的。此映象文件包含所有加載可執行代碼所需的信息,同時還將程序數據連接進入進程的虛擬內存空間。然後在執行過程中,進程定位可以使用的虛擬內存,以包含正在讀取的文件內容。新分配的虛擬內存必須連接到進程已存在的虛擬內存中才能夠使用。 最後Linux進程調用通用庫過程,比如文件處理子程序。如果每個進程都有庫過程的拷貝,那麼共享就變得沒有意義。而Linux可以使多個進程同時使用共享庫。來自共享庫的代碼和數據必須連接進入進程的虛擬地址空間以及共享此庫的其它進程的虛擬地址空間。
任何時候進程都不同時使用包含在其虛擬內存中的所有代碼和數據。雖然它可以加載在特定情況下使用的那些代碼,如初始化或者處理特殊事件時,另外它也使用了共享庫的部分子程序。但如果將這些沒有或很少使用的代碼和數據全部加載到物理內存中引起極大的浪費。如果系統中多個進程都浪費這麼多資源,則會大大降低的系統效率。Linux使用請求調頁技術來把那些進程需要訪問的虛擬內存帶入物理內存中。核心將進程頁表中這些虛擬地址標記成存在但不在內存中的狀態,而無需將所有代碼和數據直接調入物理內存。當進程試圖訪問這些代碼和數據時,系統硬件將產生頁面錯誤並將控制轉移到Linux核心來處理之。這樣對於處理器地址空間中的每個虛擬內存區域,Linux都必須知道這些虛擬內存從何處而來以及如何將其載入內存以處理頁面錯誤。
圖4.2 進程的虛擬內存
Linux核心需要管理所有的虛擬內存地址,每個進程虛擬內存中的內容在其task_struct結構中指向的 vm_area_struct結構中描敘。進程的mm_struct數據結構也包含了已加載可執行映象的信息和指向進程頁表 的指針。它還包含了一個指向vm_area_struct鏈表的指針,每個指針代表進程內的一個虛擬內存區域。
此鏈表按虛擬內存位置來排列,圖4.2給出了一個簡單進程的虛擬內存以及管理它的核心數據結構分布圖。 由於那些虛擬內存區域來源各不相同,Linux使用vm_area_struct中指向一組虛擬內存處理過程的指針來抽 象此接口。通過使用這個策略,所有的進程虛擬地址可以用相同的方式處理而無需了解底層對於內存管理的區別。如當進程試圖訪問不存在內存區域時,系統只需要調用頁面錯誤處理過程即可。
為進程創建新虛擬內存區域或處理頁面不在物理內存錯誤時,Linux核心重復使用進程的vm_area_struct數據結構集合。這樣消耗在查找vm_area_struct上的時間直接影響了系統性能。Linux把vm_area_struct數據結構以AVL(Adelson-Velskii and Landis)樹結構連接以加快速度。在這種連接中,每個vm_area_struct結構有一個左指針和右指針指向vm_area_struct結構。左邊的指針指向一個更低的虛擬內存起始地址節點而右邊的指針指向一個更高虛擬內存起始地址節點。為了找到某個的節點,Linux從樹的根節點開始查找,直到找到正確的vm_area_struct結構。插入或者釋放一個vm_area_struct結構不會消耗額外的處理時間。
當進程請求分配虛擬內存時,Linux並不直接分配物理內存。它只是創建一個vm_area_struct 結構來描敘此虛擬內存,此結構被連接到進程的虛擬內存鏈表中。當進程試圖對新分配的虛擬內存進行寫操作時,系統將產生頁面錯。處理器會嘗試解析此虛擬地址,但是如果找不到對應此虛擬地址的頁表入口時,處理器將放棄解析並產生頁面錯誤異常,由Linux核心來處理。Linux則查看此虛擬地址是否在當前進程的虛擬地址空間中。如果是Linux會創建正確的PTE並為此進程分配物理頁面。包含在此頁面中的代碼或數據可能需要從文件系統或者交換磁盤上讀出。然後進程將從頁面錯誤處開始繼續執行,由於物理內存已經存在,所以不會再產生頁面異常。
4.6 進程創建
系統啟動時總是處於核心模式,此時只有一個進程:初始化進程。象所有進程一樣,初始化進程也有一個由堆棧、寄存器等表示的機器狀態。當系統中有其它進程被創建並運行時,這些信息將被存儲在初始化進程的task_struct結構中。在系統初始化的最後,初始化進程啟動一個核心線程(init)然後保留在idle狀態。 如果沒有任何事要做,調度管理器將運行idle進程。idle進程是唯一不是動態分配task_struct的進程,它的 task_struct在核心構造時靜態定義並且名字很怪,叫init_task。
由於是系統的第一個真正的進程,所以init核心線程(或進程)的標志符為1。它負責完成系統的一些初始化設置任務(如打開系統控制台與安裝根文件系統),以及執行系統初始化程序,如/etc/init, /bin/init 或者 /sbin/init ,這些初始化程序依賴於具體的系統。init程序使用/etc/inittab作為腳本文件來創建系統中的新進程。這些新進程又創建各自的新進程。例如getty進程將在用戶試圖登錄時創建一個login進程。系 統中所有進程都是從init核心線程中派生出來。
新進程通過克隆老進程或當前進程來創建。系統調用fork或clone可以創建新任務,復制發生在核心狀態下的核心中。在系統調用的結束處有一個新進程等待調度管理器選擇它去運行。系統從物理內存中分配出來一個新的task_struct數據結構,同時還有一個或多個包含被復制進程堆棧(用戶與核心)的物理頁面。然後創建唯一地標記此新任務的進程標志符。但復制進程保留其父進程的標志符也是合理的。新創建的task_struct將被放入task數組中,另外將被復制進程的task_struct中的內容頁表拷入新的task_struct中。
復制完成後,Linux允許兩個進程共享資源而不是復制各自的拷貝。這些資源包括文件、信號處理過程和虛擬內存。進程對共享資源用各自的count來記數。在兩個進程對資源的使用完畢之前,Linux絕不會釋放此資源,例如復制進程要共享虛擬內存,則其task_struct將包含指向原來進程的mm_struct的指針。mm_struct將增加count變量以表示當前進程共享的次數。
復制進程虛擬空間所用技術的十分巧妙。復制將產生一組新的vm_area_struct結構和對應的mm_struct結構,同時還有被復制進程的頁表。該進程的任何虛擬內存都沒有被拷貝。由於進程的虛擬內存有的可能在物理內存中,有的可能在當前進程的可執行映象中,有的可能在交換文件中,所以拷貝將是一個困難且繁瑣的工作。Linux使用一種"copy on write"技術:僅當兩個進程之一對虛擬內存進行寫操作時才拷貝此虛擬內存塊。但是不管寫與不寫,任何虛擬內存都可以在兩個進程間共享。只讀屬性的內存,如可執行代碼,總是可以共享的。為了使"copy on write"策略工作,必須將那些可寫區域的頁表入口標記為只讀的,同時描敘它們的vm_area_struct數據都被設置為"copy on write"。當進程之一試圖對虛擬內存進行寫操作時將產生頁面錯誤。這時Linux將拷貝這一塊內存並修改兩個進程的頁表以及虛擬內存數據結構。
4.7 時鐘和定時器
核心跟蹤著進程的創建時間以及在其生命期中消耗的CPU時間。每個時鐘滴答時,核心將更新當前進程在系統 模式與用戶模式下所消耗的時間(記錄在jiffies中)。
除了以上記時器外,Linux還支持幾種進程相關的時間間隔定時器。
進程可以使用這些定時器在到時時向它發送各種信號,這些定時器如下:
Real
此定時器按照實時時鐘記數,當時鐘到期時,向進程發送SIGA
LRM信號。
Virtual
此定時器僅在進程運行時記數,時鐘到期時將發送SIGVTALRM信號。
Profile
此定時器在進程運行和核心為其運行時都記數。當到時時向進程發送SIGPROF信號。
以上時間間隔定時器可以同時也可以單獨運行,Linux將所有這些信息存儲在進程的task_struct數據結構中。通過系統調用可以設置這些時間間隔定時器並啟動、終止它們或讀取它們的當前值。Virtual和Profile定時器以相同方式處理。
每次時鐘滴答後當前進程的時間間隔定時器將遞減,當到時之後將發送適當的信號。
Real時鐘間隔定時器的機制有些不同,這些將在kernel一章中詳細討論。每個進程有其自身的timer_list數 據結構,當時間間隔定時器運行時,它們被排入系統的定時器鏈表中。當定時器到期後,底層處理過程將把它從隊列中刪除並調用時間間隔處理過程。此過程將向進程發送SIGALRM信號並重新啟動定時器,將其重新放入系統時鐘隊列。
4.8 程序執行
象Unix一樣,Linux程序通過命令解釋器來執行。命令解釋器是一個用戶進程,人們將其稱為shell程序。
在Linux中有多個shell程序,最流行的幾個是sh、bash和tcsh。除了幾個內置命令如cd和pwd外,命令都是 一個可執行二進制文件。當鍵入一個命令時,Shell程序將搜索包含在進程PATH環境變量中查找路徑中的目 錄來定位這個可執行映象文件。如果找到,則它被加載且執行。shell使用上面描敘的fork機制來復制自身 然後用找到的二進制可執行映象的內容來代替其子進程。一般情況下,shell將等待此命令的完成或者子進 程的退出。你可以通過按下control-Z鍵使子進程切換到後台而shell再次開始運行。同時還可以使用shell 命令bg將命令放入後台執行,shell將發送SIGCONT信號以重新啟動進程直到進程需要進行終端輸出和輸入。
可執行文件可以有許多格式,甚至是一個腳本文件。腳本文件需要恰當的命令解釋器來處理它們;例如 /bin/sh解釋shell腳本。可執行目標文件包含可執行代碼和數據,這樣操作系統可以獲得足夠的信息將其 加載到內存並執行之。Linux最常用的目標文件是ELF,但是理論上Linux可以靈活地處理幾乎所有目標文件 格式。
圖4.3 已注冊的二進制格式
通過使用文件系統,Linux所支持的二進制格式既可以構造到核心又可以作為模塊加載。核心保存著一個可以支持的二進制格式的鏈表(見圖4.3),同時當執行一個文件時,各種二進制格式被依次嘗試。
Linux上支持最廣的格式是a.out和ELF。執行文件並不需要全部讀入內存,而使用一種請求加載技術。進程 使用的可執行映象的每一部分被調入內存而沒用的部分將從內存中丟棄。
4.8.1 ELF
ELF(可執行與可連接格式)是
Unix系統實驗室設計的一種目標文件格式,現在已成為Linux中使用最多的格式。但與其它目標文件格式相比,如ECOFF和a.out,ELF的開銷稍大,它的優點是更加靈活。ELF可執行文件 中包含可執行代碼,即正文段:text和數據段:data。位於可執行映象中的表描敘了程序應如何放入進程的 虛擬地址空間中。靜態連接映象是通過連接器ld得到,在單個映象中包含所有運行此映象所需代碼和數據。 此映象同時也定義了映象的內存分布和首先被執行的代碼的地址。
圖4.4 ELF可執行文件格式
圖4.4給出了一個靜態連接的ELF可執行映象的構成。
這是一個打印"Hello World"並退出的簡單C程序。文件頭將其作為一個帶兩個物理文件頭(e_phnum = 2)的ELF映象來描敘,物理文件頭位於映象文件起始位置52字節處。第一個物理文件頭描敘的是映象中的可執行代碼。它從虛擬地址0x8048000開始,長度為65532字節。這是因為它包含了printf()函數庫代碼以輸出"Hello World"的靜態連接映象。映象的入口點,即程序的第一條指令,不是位於映象的起始位置 而在虛擬地址0x8048090(e_entry)處。代碼正好接著第二個物理文件頭。這個物理文件頭描敘了此程序使用的數據,? ?患釉氐叫槟饽诖嬷?x8 的大小在文件中是2200字節(p_filesz)但是在內存中的大小為4248字節。這是因為開始的2200字節包含的是預先初始化數據而接下來的2048字節包含的是被執行代碼初始化的數據。
當Linux將一個ELF可執行映象加載到進程的虛擬地址空間時,它並不真的加載映象。首先它建立其虛擬內存數據結構:進程的vm_area_struct樹和頁表。當程序執行時將產生頁? bf9 娲恚??鸪絛虼?牒褪?荽游锢砟诖嬷腥〕觥3絛蛑忻揮惺褂玫降牟糠執永炊疾換峒釉氐僥诖嬷腥ァR壞〦LF二進制格式加載器發現這個映象是有效的ELF可執行映象,它將把進程的當前可執行映象從虛擬內存沖刷出去。當進程是一個復制映象時(所有的進程都是),父進程執行的是老的映象程序,例如象bash這樣的命令解釋器。同時還將清除任何信號處理過程並且關閉打開的文件,在沖刷的最後,進程已經為新的可執行映象作好了准備。不管可執行映象是哪種格式,進程的mm_struct結構中將存入相同信息,它們是指向映象代碼和數據的指針。當ELF可執行映象從文件中讀出且相關程序代碼被映射到進程虛擬地址空間後,這些指針的值都被確定下來。同時vm_area_struct也被建立起來,進程的頁表也被修改。mm_struct結構中還包含傳遞給程序和進程環境變量的參數的指針。
ELF 共享庫
另一方面,動態連接映象並不包含全部運行所需要的代碼和數據。其中的一部分僅在運行時才連接到共享庫中。ELF共享庫列表還在運行時連接到共享庫時被動態連接器使用。Linux使用幾個動態連接器,如ld.so.1,libc.so.1和ld-
linux.so.1,這些都放置在/lib目錄中。這些庫中包含常用代碼,如C語言子程序等。如果沒有動態連接,所有程序將不得不將所有庫過程拷貝一份並連接進來,這樣將需要更多的磁盤與虛擬內存空間。通過動態連接,每個被引用庫過程的信息都可以包含在ELF映象列表中。這些信息用來幫助動態連接器定位庫過程並將它連入程序的地址空間。
4.8.2 腳本文件
腳本文件的運行需要解釋器的幫助。Linux中有許許多多的解釋器;例如wish、perl以及命令外殼程序tcsh。 Linux使用標准的Unix規則,在腳本文件的第一行包含了腳本解釋器的名字。典型的腳本文件的開頭如下:
#!/usr/bin/wish
此時命令解釋器會試著尋找腳本解釋器。 然後它打開此腳本解釋器的執行文件得到一個指向此文件的VFS inode並使此腳本解釋器開始解釋此腳本。這時腳本文件名變成了腳本解釋器的0號參數(第一個參數)並且其余參數向上挪一個位置(原來的第一個參數變成第二個)。腳本解釋器的加載過程與其他可執行文件相同。Linux會逐個嘗試各種二進制可執行格式直到它可以執行。