Linux Kernel 核心中文手冊(4)--進程

Processes (進程)
 
    本章描述進程是什麼以及 Linux 如何創建、管理和刪除系統中的進程。
 
    進程執行操作系統中的任務。程序是存放在磁盤上的包括一系列機器代碼指令
和數據的可執行的映像,因此,是一個被動的實體。進程可以看作是一個執行中的
計算機程序。它是動態的實體,在處理器執行機器代碼指令時不斷改變。處理程序
的指令和數據,進程也包括程序計數器和其他 CPU 的寄存器以及包括臨時數據(
例如例程參數、返回地址和保存的變量)的堆棧。當前執行的程序,或者說進程,
包括微處理器中所有的當前的活動。 Linux 是一個多進程的操作系統。進程是分
離的任務,擁有各自的權利和責任。如果一個進程崩潰,它不應該讓系統中的另一
個進程崩潰。每一個獨立的進程運行在自己的虛擬地址空間,除了通過安全的核心
管理的機制之外無法影響其他的進程。
 
    在一個進程的生命週期中它會使用許多系統資源。它會用系統的 CPU 執行它


的指令,用系統的物理內存來存儲它和它的數據。它會打開和使用文件系統中的文
件,會直接或者間接使用系統的物理設備。 Linux 必須跟蹤進程本身和它使用的
系統資源以便管理公平地管理該進程和系統中的其他進程。如果一個進程獨佔了系
統的大部分物理內存和 CPU ,對於其他進程就是不公平的。
 
    系統中最寶貴的資源就是 CPU 。通常系統只有一個。 Linux 是一個多進程的
操作系統。它的目標是讓進程一直在系統的每一個 CPU 上運行,充分利用 CPU 。
如果進程數多於 CPU (多數是這樣),其餘的進程必須等到 CPU 被釋放才能運行
。多進程是一個簡單的思想:一個進程一直運行,直到它必須等待,通常是等待一
些系統資源,等擁有了資源,它纔可以繼續運行。在一個單進程的系統,比如 DOS
 , CPU 被簡單地設爲空閒,這樣等待的時間就會被浪費。在一個多進程的系統中
,同一時刻許多進程在內存中。當一個進程必須等待時操作系統將 CPU 從這個進
程拿走,並將它交給另一個更需要的進程。是調度程序選擇了
 
    下一次最合適的進程。 Linux 使用了一系列的調度方案來保證公平。
 
    Linux 支持許多不同的可執行文件格式, ELF 是其中之一, Java 是另一個
。 Linux 必須透明地管理這些文件,因爲進程使用系統的共享的庫。
 
4.1 Linux Processes ( Linux 的進程)
 
    Linux 中,每一個進程用一個 task_struct (在 Linux 中 task 和 process


 互用)的數據結構來表示,用來管理系統中的進程。 Task 向量表是指向系統中
每一個 task_struct 數據結構的指針的數組。這意味着系統中最大進程數受 task
 向量表的限制,缺省是 512 。當新的進程創建的時候,從系統內存中分配一個新
的 task_struct ,並增加到 task 向量表中。爲了更容易查找,用 current 指針
指向當前運行的進程。
 
參見 include/linux/sched.h
 
    除了普通進程, Linux 也支持實時進程。這些進程必須對於外界事件迅速反
應(因此叫做“實時”),調度程序必須和普通用戶進程區分對待。雖然
task_struct 數據結構十分巨大、複雜,但是它的域可以分爲以下的功能:
 
    State 進程執行時它根據情況改變狀態 (state) 。 Linux 進程使用以下狀態
:(這裏漏掉了 SWAPPING ,因爲看來沒用到)
 
    Running 進程在運行 ( 是系統的當前進程 ) 或者準備運行(等待被安排到系
統的一個 CPU 上)
 
    Waiting 進程在等待一個事件或資源。 Linux 區分兩種類型的等待進程:可
中斷和不可中斷的( interruptible and uninterruptible )。可中斷的等待進
程可以被信號中斷,而不可中斷的等待進程直接等待硬件條件,不能被任何情況中
斷。


 
    Stopped 進程停止了,通常是接收到了一個信號。正在調試的進程可以在停止
狀態。
 
    Zombie 終止的進程,因爲某種原因,在 task 向量表重任舊有一個
task_struct 數據結構的條目。就想聽起來一樣,是一個死亡的進程。
 
    Scheduling Information 調度者需要這個信息用於公平地決定系統中的進程
哪一個更應該運行。
 
    Identifiers 系統中的每一個進程都有一個進程標識符。進程標識符不是
task 向量表中的索引,而只是一個數字。每一個進程也都有用戶和組( user and
 group )的標識符。用來控制進程對於系統中文件和設備的訪問。
 
    Inter-Process Communication Linux 支持傳統的 UNIX-IPC 機制,即信號,
管道和信號燈( semaphores ),也支持系統 V 的 IPC 機制,即共享內存、信號
燈和消息隊列。關於 Linux 支持的 IPC 機制在第 5 章中描述。
 
    Links 在 Linux 系統中,沒有一個進程是和其他進程完全無關的。系統中的
每一個進程,除了初始的進程之外,都有一個父進程。新進程不是創建的,而是拷
貝,或者說從前一個進程克隆的( cloned )。每一個進程的 task_struct 中都
有指向它的父進程和兄弟進程(擁有相同的父進程的進程)以及它的子進程的的指


針。在 Linux 系統中你可以用 pstree 命令看到正在運行的進程的家庭關係。
 
init(1)-+-crond(98)
 
|-emacs(387)
 
|-gpm(146)
 
|-inetd(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)
 
    另外系統中的所有的進程信息還存放在一個 task_struct 數據結構的雙向鏈
表中,根是 init 進程。這個表讓 Linux 可以查到系統中的所有的進程。它需要
這個表以提供對於 ps 或者 kill 等命令的支持。
 
    Times and Timers 在一個進程的生命週期中,核心除了跟蹤它使用的 CPU 時
間還記錄它的其他時間。每一個時間片( clock tick ),核心更新 jiffies 中


當前進程在系統和用戶態所花的時間綜合。 Linux 也支持進程指定的時間間隔的
計數器。進程可以使用系統調用建立計時器,在計時器到期的時候發送信號給自己
。這種計時器可以是一次性的,也可是週期性的。
 
    File system 進程可以根據需要打開或者關閉文件,進程的 task_struct 結
構存放了每一個打開的文件描述符的指針和指向兩個 VFS I 節點( inode )的指
針。每一個 VFS I 節點唯一描述一個文件系統中的一個文件或目錄,也提供了對
於底層文件系統的通用接口。 Linux 下如何支持文件系統在第 9 章中描述。第一
個 I 節點是該進程的根(它的主目錄),第二個是它的當前或者說 pwd 目錄。
Pwd 取自 Unix 命令:印出工作目錄。這兩個 VFS 節點本身有計數字段,隨着一
個或多個進程引用它們而增長。這就是爲什麼你不能刪除一個進程設爲工作目錄的
目錄。
 
    Virtual memory 多數進程都有一些虛擬內存(核心線程和核心守護進程沒有
), Linux 核心必須知道這些虛擬內存是如何映射到系統的物理內存中的。
 
    Processor Specific Context 進程可以看作是系統當前狀態的總和。只要進
程運行,它就要使用處理器的寄存器、堆棧等等。當一個進程暫停的時候,這些進
程的上下文、和 CPU 相關的上下文必須保存到進程的 task_struct 結構中。當調
度者重新啓動這個進程的時候,它的上下文就從這裏恢復。
 
4.2 Identifiers (標識)


 
    Linux ,象所有的 Unix ,使用用戶和組標識符來檢查對於系統中的文件和映
像的訪問權限。 Linux 系統中所有的文件都有所有權和許可,這些許可描述了系
統對於該文件或目錄擁有什麼樣的權限。基本的權限是讀、寫和執行,並分配了 3
 組用戶:文件屬主、屬於特定組的進程和系統中的其他進程。每一組用戶都可以
擁有不同的權限,例如一個文件可以讓它的屬主讀寫,它的組讀,而系統中的其他
進程不能訪問。
 
    Linux 使用組來給一組用戶賦予對文件或者目錄的權限,而不是對系統中的單
個用戶或者進程賦予權限。比如你可以爲一個軟件項目中的所有用戶創建一個組,
使得只有他們才能夠讀寫項目的源代碼。一個進程可以屬於幾個組(缺省是 32 個
),這些組放在每一個進程的 task_struct 結構中的 groups 向量表中。只要進
程所屬的其中一個組對於一個文件有訪問權限,則這個進程就又對於這個文件的適
當的組權限。
 
一個進程的 task_struct 中有 4 對進程和組標識符。
 
Uid,gid 該進程運行中所使用的用戶的標識符和組的標識符
 
    Effective uid and gid 一些程序把執行進程的 uid 和 gid 改變爲它們自己
的(在 VFS I 節點執行映像的屬性中)。這些程序叫做 setuid 程序。這種方式
有用,因爲它可以限制對於服務的訪問,特別是那些用其他人的方式運行的,例如


網絡守護進程。有效的 uid 和 gid 來自 setuid 程序,而 uid 和 gid 仍舊是原
來的。核心檢查特權的時候檢查有效 uid 和 gid 。
 
    File system uid and gid 通常和有效 uid 和 gid 相等,檢查對於文件系統
的訪問權限。用於通過 NFS 安裝的文件系統。這時用戶態的 NFS 服務器需要象一
個特殊進程一樣訪問文件。只有文件系統 uid 和 gid 改變(而非有效 uid 和
gid )。這避免了惡意用戶向 NFS 的服務程序發送 Kill 信號。 Kill 用一個特
別的有效 uid 和 gid 發送給進程。
 
    Saved uid and gid 這是 POSIX 標準的要求,讓程序可以通過系統調用改變
進程的 uid 和 gid 。用於在原來的 uid 和 gid 改變之後存儲真實的 uid 和
gid 。
 
4.3 Scheduling (調度)
 
    所有的進程部分運行與用戶態,部分運行於系統態。底層的硬件如何支持這些
狀態各不相同但是通常有一個安全機制從用戶態轉入系統態並轉回來。用戶態比系
統態的權限低了很多。每一次進程執行一個系統調用,它都從用戶態切換到系統態
並繼續執行。這時讓核心執行這個進程。 Linux 中,進程不是互相爭奪成爲當前
運行的進程,它們無法停止正在運行的其它進程然後執行自身。每一個進程在它必
須等待一些系統事件的時候會放棄 CPU 。例如,一個進程可能不得不等待從一個
文件中讀取一個字符。這個等待發生在系統態的系統調用中。進程使用了庫函數打


開並讀文件,庫函數又執行系統調用從打開的文件中讀入字節。這時,等候的進程
會被掛起,另一個更加值得的進程將會被選擇執行。進程經常調用系統調用,所以
經常需要等待。即使進程執行到需要等待也有可能會用去不均衡的 CPU 事件,所
以 Linux 使用搶先式的調度。用這種方案,每一個進程允許運行少量一段時間,
 200 毫秒,當這個時間過去,選擇另一個進程運行,原來的進程等待一段時間直
到它又重新運行。這個時間段叫做時間片。
 
    需要調度程序選擇系統中所有可以運行的進程中最值得的進程。一個可以運行
的進程是一個只等待 CPU 的進程。 Linux 使用合理而簡單的基於優先級的調度算
法在系統當前的進程中進行選擇。當它選擇了準備運行的新進程,它就保存當前進
程的狀態、和處理器相關的寄存器和其他需要保存的上下文信息到進程的
task_struct 數據結構中。然後恢復要運行的新的進程的狀態(又和處理器相關)
,把系統的控制交給這個進程。爲了公平地在系統中所有可以運行( runnable )
的進程之間分配 CPU 時間,調度程序在每一個進程的 task_struct 結構中保存了
信息:
 
參見 kernel/sched.c schedule()
 
    policy 進程的調度策略。 Linux 有兩種類型的進程:普通和實時。實時進程
比所有其它進程的優先級高。如果有一個實時的進程準備運行,那麼它總是先被運
行。實時進程有兩種策略:環或先進先出( round robin and first in first
out )。在環的調度策略下,每一個實時進程依次運行,而在先進先出的策略下,


每一個可以運行的進程按照它在調度隊列中的順序運行,這個順序不會改變。
 
    Priority 進程的調度優先級。也是它允許運行的時候可以使用的時間量(
jiffies )。你可以通過系統調用或者 renice 命令來改變一個進程的優先級。
 
 
    Rt_priority Linux 支持實時進程。這些進程比系統中其他非實時的進程擁有
更高的優先級。這個域允許調度程序賦予每一個實時進程一個相對的優先級。實時
進程的優先級可以用系統調用來修改
 
    Coutner 這時進程可以運行的時間量( jiffies )。進程啓動的時候等於優
先級( priority ),每一次時鐘週期遞減。
 
    調度程序從核心的多個地方運行。它可以在把當前進程放到等待隊列之後運行
,也可以在系統調用之後進程從系統態返回進程態之前運行。需要運行調度程序的
另一個原因是系統時鐘剛好把當前進程的計數器 (counter) 置成了 0 。每一次調
度程序運行它做以下工作:
 
參見 kernel/sched.c schedule()
 
kernel work 調度程序運行 bottom half handler 並處理系統的調度任務隊列。
這些輕量級的核心線程在第 11 章詳細描述


 
Current pocess 在選擇另一個進程之前必須處理當前進程。
 
如果當前進程的調度策略是環則它放到運行隊列的最後。
 
如果任務是可中斷的而且它上次調度的時候收到過一個信號,它的狀態變爲
RUNNING
 
如果當前進程超時,它的狀態成爲 RUNNING
 
如果當前進程的狀態爲 RUNNING 則保持此狀態
 
不是 RUNNING 或者 INTERRUPTIBLE 的進程被從運行隊列中刪除。這意味着當調度
程序查找最值得運行的進程時不會考慮這樣的進程。
 
    Process Selection 調度程序查看運行隊列中的進程,查找最值得運行的進程
。如果有實時的進程(具有實時調度策略),就會比普通進程更重一些。普通進程
的重量是它的 counter ,但是對於實時進程則是 counter 加 1000 。這意味着如
果系統中存在可運行的實時進程,就總是在任何普通可運行的進程之前運行。當前
的進程,因爲用掉了一些時間片(它的 counter 減少了),所以如果系統中由其
他同等優先級的進程,就會處於不利的位置:這也是應該的。如果幾個進程又同樣
的優先級,最接近運行隊列前段的那個就被選中。當前進程被放到運行隊列的後面


。如果一個平衡的系統,擁有大量相同優先級的進程,那麼回按照順序執行這些進
程。這叫做環型調度策略。不過,因爲進程需要等待資源,它們的運行順序可能會
變化。
 
    Swap Processes 如果最值得運行的進程不是當前進程,當前進程必須被掛起
,運行新的進程。當一個進程運行的時候它使用了 CPU 和系統的寄存器和物理內
存。每一次它調用例程都通過寄存器或者堆棧傳遞參數、保存數值比如調用例程的
返回地址等。因此,當調度程序運行的時候它在當前進程的上下文運行。它可能是
特權模式:核心態,但是它仍舊是當前運行的進程。當這個進程要掛起時,它的所
有機器狀態,包括程序計數器 (PC) 和所有的處理器寄存器,必須存到進程的
task_struct 數據結構中。然後,必須加載新進程的所有機器狀態。這種操作依賴
於系統,不同的 CPU 不會完全相同地實現,不過經常都是通過一些硬件的幫助。
 
 
    交換出去進程的上下文發生在調度的最後。前一個進程存儲的上下文,就是當
這個進程在調度結束的時候系統的硬件上下文的快照。相同的,當加載新的進程的
上下文時,仍舊是調度結束時的快照,包括進程的程序計數器和寄存器的內容。
 
    如果前一個進程或者新的當前進程使用虛擬內存,則系統的頁表需要更新。同
樣,這個動作適合體系結構相關。 Alpha AXP 處理器,使用 TLT ( Translation
 Look-aside Table )或者緩存的頁表條目,必須清除屬於前一個進程的緩存的頁
表條目。


 
4.3.1 Scheduling in Multiprocessor Systems (多處理器系統中的調度)
 
    在 Linux 世界中,多 CPU 系統比較少,但是已經做了大量的工作使 Linux
成爲一個 SMP (對稱多處理)的操作系統。這就是,可以在系統中的 CPU 之間平
衡負載的能力。負載均衡沒有比在調度程序中更重要的了。
 
    在一個多處理器的系統中,希望的情況是:所有的處理器都繁忙地運行進程。
每一個進程都獨立地運行調度程序直到它的當前的進程用完時間片或者不得不等待
系統資源。 SMP 系統中第一個需要注意的是系統中可能不止一個空閒( idle )
進程。在一個單處理器的系統中,空閒進程是 task 向量表中的第一個任務,在一
個 SMP 系統中,每一個 CPU 都有一個空閒的進程,而你可能有不止一個空閒 CPU
 。另外,每一個 CPU 有一個當前進程,所以 SMP 系統必須記錄每一個處理器的
當前和空閒進程。
 
    在一個 SMP 系統中,每一個進程的 task_struct 都包含進程當前運行的處理
器編號( processor )和上次運行的處理器編號( last_processor )。爲什麼
進程每一次被選擇運行時不要在不同的 CPU 上運行是沒什麼道理的,但是
Linux 可以使用 processor_mask 把進程限制在一個或多個 CPU 上。如果位 N 置
位,則該進程可以運行在處理器 N 上。當調度程序選擇運行的進程的時候,它不
會考慮 processor_mask 相應位沒有設置的進程。調度程序也會利用上一次在當前
處理器運行的進程,因爲把進程轉移到另一個處理器上經常會有性能上的開支。


 
 
 
 
4.4 Files (文件)
 
    圖 4.1 顯示了描述系統每一個進程中的用於描述和文件系統相關的信息的兩
個數據結構。第一個 fs_struct 包括了這個進程的 VFS I 節點和它的 umask 。
 Umask 是新文件創建時候的缺省模式,可以通過系統調用改變。
 
參見 include/linux/sched.h
 
    第二個數據結構, files_struct ,包括了進程當前使用的所有文件的信息。
程序從標準輸入讀取,向標準輸出寫,錯誤信息輸出到標準錯誤。這些可以是文件
,終端輸入 / 輸出或者世紀的設備,但是從程序的角度它們都被看作是文件。每
一個文件都有它的描述符, files_struct 包括了指向 256 個 file 數據結果,
每一個描述進程形用的文件。 F_mode 域描述了文件創建的模式:只讀、讀寫或者
只寫。 F_pos 記錄了下一次讀寫操作在文件中的位置。 F_inode 指向描述該文件
的 I 節點, f_ops 是指向一組例程地址的指針,每一個地址都是一個用於處理文
件的函數。例如寫數據的函數。這種抽象的接口非常強大,使得 Linux 可以支持
大量的文件類型。我們可以看到,在 Linux 中 pipe 也是用這種機制實現的。
 


    每一次打開一個文件,就使用 files_struct 中的一個空閒的 file 指針指向
這個新的 file 結構。 Linux 進程啓動時有 3 個文件描述符已經打開。這就是標
準輸入、標準輸出和標準錯誤,這都是從創建它們的父進程中繼承過來的。對於文
件的訪問都是通過標準的系統調用,需要傳遞或返回文件描述符。這些描述符是進
程的 fd 向量表中的索引,所以標準輸入、標準輸出和標準錯誤的文件描述符分別
是 0 , 1 和 2 。對於文件的所有訪問都是利用 file 數據結構中的文件操作例
程和它的 VFS I 節點一起來實現的。
 
4.5 Virtual Memory (虛擬內存)
 
    進程的虛擬內存包括多種來源的執行代碼和數據。第一種是加載的程序映像,
例如 ls 命令。這個命令,象所有的執行映像一樣,由執行代碼和數據組成。映像
文件中包括將執行代碼和相關的程序數據加載到進程地虛擬內存中所需要的所有信
息。第二種,進程可以在處理過程中分配(虛擬)內存,比如用於存放它讀入的文
件的內容。新分配的虛擬內存需要連接到進程現存的虛擬內存中才能使用。第三中
, Linux 進程使用通用代碼組成的庫,例如文件處理。每一個進程都包括庫的一
份拷貝沒有意義, Linux 使用共享庫,幾個同時運行的進程可以共用。這些共享
庫裏邊的代碼和數據必須連接到該進程的虛擬地址空間和其他共享該庫的進程的虛
擬地址空間。
 
    在一個特定的時間,進程不會使用它的虛擬內存中包括的所有代碼和數據。它
可能包括旨在特定情況下使用的代碼,比如初始化或者處理特定的事件。它可能只


是用了它的共享庫中一部分例程。如果把所有這些代碼都加載到物理內存中而不使
用只會是浪費。把這種浪費和系統中的進程數目相乘,系統的運行效率會很低。
Linux 改爲使用 demand paging 技術,進程的虛擬內存只在進程試圖使用的時候
才調入物理內存中。所以, Linux 不把代碼和數據直接加載到內存中,而修改進
程的頁表,把這些虛擬區域標誌爲存在但是不在內存中。當進程試圖訪問這些代碼
或者數據,系統硬件會產生一個 page fault ,把控制傳遞給 Linux 核心處理。
因此,對於進程地址空間的每一個虛擬內存區域, Linux 需要直到它從哪裏來和
如何把它放到內存中,這樣纔可以處理這些 page fault 。
 
    Linux 核心需要管理所有的這些虛擬內存區域,每一個進程的虛擬內存的內容
通過一個它的 task_struct 指向的一個 mm_struct mm_struc 數據結構描述。該
進程的 mm_struct 數據結構也包括加載的執行映像的信息和進程頁表的指針。它
包括了指向一組 vm_area_struct 數據結構的指針,每一個都表示該進程中的一個
虛擬內存區域。
 
    這個鏈接表按照虛擬內存順序排序。圖 4.2 顯示了一個簡單進程的虛擬內存
分佈和管理它的核心數據結構。因爲這些虛擬內存區域來源不同, Linux 通過
vm_area_struct 指向一組虛擬內存處理例程(通過 vm_ops )的方式抽象了接口
。這樣進程的所有虛擬內存都可以用一種一致的方式處理,不管底層管理這塊內存
的服務如何不同。例如,會有一個通用的例程,在進程試圖訪問不存在的內存時調
用,這就是 page fault 的處理。
 


    當 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 數據結構來描述這塊虛擬內存,連接到進程的虛擬內
存列表中。當進程試圖寫這個新的虛擬內存區域的時候,系統會發生 page
fault 。處理器試圖解碼這個虛擬地址,但是沒有對應該內存的頁表條目,它會放
棄併產生一個 page fault 異常,讓 Linux 核心處理。 Linux 檢查這個引用的虛
擬地址是不是在進程的虛擬地址空間, 如果是, Linux 創建適當的 PTE 併爲該
進程分配物理內存頁。也許需要從文件系統或者交換磁盤中加載相應的代碼或者數
據,然後進程從引起 page fault 的指令重新運行,因爲這次該內存實際存在,可
以繼續。


 
4.6 Creating a Process (創建一個進程)
 
    當系統啓動的時候它運行在覈心態,這時,只有一個進程:初始化進程。象所
有其他進程一樣,初始進程有一組用堆棧、寄存器等等表示的機器狀態。當系統中
的其他進程創建和運行的時候這些信息存在初始進程的 task_struct 數據結構中
。在系統初始化結束的時候,初始進程啓動一個核心線程(叫做 init )然後執行
空閒循環,什麼也不做。當沒有什麼可以做的時候,調度程序會運行這個空閒的進
程。這個空閒進程的 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 。
 
參見 kernel/fork.c do_fork()
 
    克隆進程的時候, 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 ”
。當一個進程試圖寫向着這個虛擬內存的時候會產生 page fault 。這時 Linux
將會製作這塊內存的一份拷貝並處理兩個進程的頁表和虛擬內存的數據結構。
 
 
Times and Timer (時間和計時器)
 
    核心跟蹤進程的 CPU 時間和其他一些時間。每一個時鐘週期,核心更新當前
進程的 jiffies 來表示在系統和用戶態下花費的時間總和。
 
    除了這些記賬的計時器, Linux 還支持進程指定的間隔計時器( interval
timer )。進程可以使用這些計時器在這些計時器到期的時候發送給自身信號。支
持三種間隔計時器:
 
參見 kernel/itimer.c
 
Real 這個計時器使用實時計時,當計時器到期,發送給進程一個 SIGALRM 信號。
 
 
Virtual 這個計時器只在進程運行的時候計時,到期的時候,發送給進程一個
SIGVTALARM 信號。
 


Profile 在進程運行的時候和系統代表進程執行的時候都及時。到期的時候,會發
送 SIGPROF 信號。
 
    可以運行一個或者所有的間隔計時器, Linux 在進程的 task_struct 數據結
構中記錄所有的必要信息。可以使用系統調用建立這些間隔計時器,啓動、停止它
們,讀取當前的數值。虛擬和 profile 計時器的處理方式相同:每一次時鐘週期
,當前進程的計時器遞減,如果到期,就發出適當的信號
 
參見 kernel/sched.c do_it_virtual() , do_it_prof()
 
    實時間隔計時器稍微不同。 Linux 使用計時器的機制在第 11 章描述。每一
個進程都有自己的 timer_list 數據結構,當時使用實時計時器的時候,使用系統
的 timer 表。當它到期的時候,計時器後半部分處理把它從隊列中刪除並調用間
隔計時器處理程序。它產生 SIGALRM 信號並重啓動間隔計時器,把它加回到系統
計時器隊列。
 
參見: kernel/iterm.c it_real_fn()
 
 
Executing Programs (執行程序)
     在 Linux 中,象 Unix 一樣,程序和命令通常通過命令解釋器執行。命令解
釋程序是和其他進程一樣的用戶進程,叫做 shell (想象一個堅果,把核心作爲


中間可食的部分,而 shell 包圍着它,提供一個接口)。 Linux 中有許多 shell
 ,最常用的是 sh 、 bash 和 tcsh 。除了一些內部命令之外,比如 cd 和
pwd ,命令是可執行的二進制文件。對於輸入的每一個命令, shell 在當前進程
的搜索路徑指定的目錄中(放在 PATH 環境變量)查找匹配的名字。如果找到了文
件,就加載並運行。 Shell 用上述的 fork 機制克隆自身,並在子進程中用找到
的執行映像文件的內容替換它正在執行的二進制映像( shell )。通常 shell 等
待命令結束,或者說子進程退出。你可以通過輸入 control-Z 發送一個
SIGSTOP 信號給子進程,把子進程停止並放到後臺,讓 shell 重新運行。你可以
使用 shell 命令 bg 讓 shell 向子進程發送 SIGCONT 信號,把子進程放到後臺
並重新運行,它會持續運行直到它結束或者需要從終端輸入或輸出。
 
 
 
    執行文件可以由許多格式甚至可以是一個腳本文件( script file )。腳本
文件必須用合適的解釋程序識別並運行。例如 /bin/sh 解釋 shell script 。可
執行的目標文件包括了執行代碼和數據以及足夠的其他信息,時的操作系統可以把
它們加載到內存中並執行。 Linux 中最常用的目標文件類型是 ELF ,而理論上,
 Linux 靈活到足以處理幾乎所有的目標文件格式。
 
    好像文件系統一樣, Linux 可以支持的二進制格式也是在覈心連接的時候直
接建立在覈心的或者是可以作爲模塊加載的。核心保存了支持的二進制格式(見圖
 4.3 )的列表,當試圖執行一個文件的時候,每一個二進制格式都被嘗試,直到


可以工作。通常, Linux 支持的二進制文件是 a.out 和 ELF 。可執行文件不需
要完全讀入內存,而使用叫做 demand loading 的技術。當進程使用執行映像的一
部分的時候它才被調入內存,未被使用的映像可以從內存中廢棄。
 
參見 fs/exec.c do_execve()
 
 
 
 
ELF
 
 
ELF ( Executable and Linkable Format 可執行可連接格式)目標文件,由
Unix 系統實驗室設計,現在成爲 Linux 最常用的格式。雖然和其他目標文件格式
比如 ECOFF 和 a.out 相比,有性能上的輕微開支, ELF 感覺更靈活。 ELF 可執
行文件包括可執行代碼(有時叫做 text )和數據( data )。執行映像中的表描
述了程序應該如何放到進程的虛擬內存中。靜態連接的映像是用連接程序( ld )
或者連接編輯器創建的,單一的映像中包括了運行該映像所需要的所有的代碼和數
據。這個映像也描述了該映像在內存中的佈局和要執行的第一部分代碼在映像中的
地址。
 
    圖 4.4 象是了靜態連接的 ELF 可執行映像的佈局。這是個簡單的 C 程序,


打印“ hello world ”然後退出。頭文件描述了它是一個 ELF 映像,有兩個物理
頭( e_phnum 是 2 ),從映像文件的開頭第 52 字節開始( e_phoff )。第一
個物理頭描述映像中的執行代碼,在虛擬地址 0x8048000 ,有 65532 字節。因爲
它是靜態連接的,所以包括輸出“ hello world ”的調用 printf ()的所有的
庫代碼。映像的入口,即程序的第一條指令,不是位於映像的起始位置,而在虛擬
地址 0x8048090 ( e_entry )。代碼緊接着在第二物理頭後面開始。這個物理頭
描述了程序的數據,將會加載到虛擬內存地址 0x8059BB8 。這塊數據可以讀寫。
你會注意到文件中數據的大小是 2200 字節( p_filesz )而在內存中的大小是
4248 字節。因爲前 2200 字節包括預先初始化的數據,而接着的 2048 字節包括
會被執行代碼初始化的數據。
 
參見 include/linux/elf.h
 
    當 Linux 把 ELF 可執行映像加載到進程的虛擬地址空間的時候,它不是實際
的加載映像。它設置虛擬內存數據結構,即進程的 vm_area_struct 和它的頁表。
當程序執行了 page fault 的時候,程序的代碼和數據會被放到物理內存中。沒有
用到的程序部分將不會被放到內存中。一旦 ELF 二進制格式加載程序滿足條件,
映像是一個有效的 ELF 可執行映像,它把進程的當前可執行映像從它的虛擬內存
中清除。因爲這個進程是個克隆的映像(所有的進程都是),舊的映像是父進程執
行的程序的映像(例如命令解釋程序 shell bash )。清除舊的可執行映像會廢棄
舊的虛擬內存的數據結構,重置進程的頁表。它也會清除設置的其他信號處理程序
,關閉打開的文件。在清除過程的最後,進程準備運行新的可執行映像。不管可執


行映像的格式如何,進程的 mm_struct 中都要設置相同的信息。包括指向映像中
代碼和數據起始的指針。這些數值從 ELF 可執行映像的物理頭中讀入,它們描述
的部分也被映射到了進程的虛擬地址空間。這也發生在進程的 vm_area_struct 數
據結構建立和頁表修改的時候。 mm_struct 數據結構中也包括指針,指向傳遞給
程序的參數和進程的環境變量。
 
ELF Shared Libraries ( ELF 共享庫)
 
    動態連接的映像,反過來,不包含運行所需的所有的代碼和數據。其中一些放
在共享庫並在運行的時候連接到映像中。當運行時動態庫連接到映像中的時候,動
態連接程序( dynamic linker )也要使用 ELF 共享庫的表。 Linux 使用幾個動
態連接程序, ld.so.1 , libc.so.1 和 ld-linux.so.1 ,都在 /lib 目錄下。
這些庫包括通用的代碼,比如語言子例程。如果沒有動態連接,所有的程序都必須
有這些庫的獨立拷貝,需要更多的磁盤空間和虛擬內存。在動態連接的情況下,
ELF 映像的表中包括引用的所有庫例程的信息。這些信息指示動態連接程序如何定
位庫例程以及如何連接到程序的地址空間。
 
Scripts Files
 
    腳本文件是需要解釋器才能運行的可執行文件。 Linux 下有大量的解釋器,
例如 wish 、 perl 和命令解釋程序比如 tcsh 。 Linux 使用標準的 Unix 約定
,在腳本文件的第一行包括解釋程序的名字。所以一個典型的腳本文件可能開頭是


#!/usr/bin/wish
 
    腳本文件加載器試圖找出文件所用的解釋程序。它試圖打開腳本文件第一行指
定的可執行文件。如果可以打開,就得到一個指向該文件的 VFS I 節點的指針,
然後執行它去解釋腳本文件。腳本文件的名字成爲了參數 0 (第一個參數),所
有的其他參數都向上移動一位(原來的第一個參數成爲了第二個參數等等)。加載
解釋程序和 Linux 加載其他可執行程序一樣。 Linux 依次嘗試各種二進制格式,
直到可以工作。這意味着理論上你可以把幾種解釋程序和二進制格式堆積起來,讓
 Linux 的二進制格式處理程序更加靈活。
 
參見 fs/binfmt_script.c do_load_script()


發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章