Linux 程序管理剖析 建立、管理、排程和銷毀 |
Linux® 的使用者空間程序的建立和管理所涉及的原理與 UNIX® 有很多共同點,但也有一些特定于 Linux 的獨特之處。在本文中,了解 Linux 程序的生命周期,探索使用者程序建立、記憶體管理、排程和銷毀的核心内幕。 Linux 是一種動态系統,能夠适應不斷變化的計算需求。Linux 計算需求的表現是以程序 的通用抽象為中心的。程序可以是短期的(從指令行執行的一個指令),也可以是長期的(一種網絡服務)。是以,對程序及其排程進行一般管理就顯得極為重要。 在使用者空間,程序是由程序辨別符(PID)表示的。從使用者的角度來看,一個 PID 是一個數字值,可惟一辨別一個程序。一個 PID 在程序的整個生命期間不會更改,但 PID 可以在程序銷毀後被重新使用,是以對它們進行緩存并不見得總是理想的。 在使用者空間,建立程序可以采用幾種方式。可以執行一個程式(這會導緻新程序的建立),也可以在程式内,調用一個 fork 或 exec 系統調用。fork 調用會導緻建立一個子程序,而 exec 調用則會用新程式代替目前程序上下文。接下來,我将對這幾種方法進行讨論以便您能很好地了解它們的工作原理。 在本文中,我将按照下面的順序展開對程序的介紹,首先展示程序的核心表示以及它們是如何在核心内被管理的,然後來看看程序建立和排程的各種方式(在一個或多個處理器上),最後介紹程序的銷毀。 程序表示
在清單 1 中,可以看到幾個預料之中的項,比如執行的狀态、堆棧、一組标志、父程序、執行的線程(可以有很多)以及開放檔案。我稍後會對其進行詳細說明,這裡隻簡單加以介紹。state 變量是一些表明任務狀态的比特位。最常見的狀态有:TASK_RUNNING 表示程序正在運作,或是排在運作隊列中正要運作;TASK_INTERRUPTIBLE 表示程序正在休眠、TASK_UNINTERRUPTIBLE 表示程序正在休眠但不能叫醒;TASK_STOPPED 表示程序停止等等。這些标志的完整清單可以在 ./linux/include/linux/sched.h 内找到。 flags 定義了很多訓示符,表明程序是否正在被建立(PF_STARTING)或退出(PF_EXITING),或是程序目前是否在配置設定記憶體(PF_MEMALLOC)。可執行程式的名稱(不包含路徑)占用 comm(指令)字段。 每個程序都會被賦予優先級(稱為 static_prio),但程序的實際優先級是基于加載以及其他幾個因素動态決定的。優先級值越低,實際的優先級越高。 tasks 字段提供了連結清單的能力。它包含一個 prev 指針(指向前一個任務)和一個 next 指針(指向下一個任務)。 程序的位址空間由 mm 和 active_mm 字段表示。mm 代表的是程序的記憶體描述符,而 active_mm 則是前一個程序的記憶體描述符(為改進上下文切換時間的一種優化)。 thread_struct 則用來辨別程序的存儲狀态。此元素依賴于 Linux 在其上運作的特定架構,在 ./linux/include/asm-i386/processor.h 内有這樣的一個例子。在此結構内,可以找到該程序自執行上下文切換後的存儲(硬體系統資料庫、程式計數器等)。
現在,讓我們來看看如何在 Linux 内管理程序。在很多情況下,程序都是動态建立并由一個動态配置設定的 task_struct 表示。一個例外是 init 程序本身,它總是存在并由一個靜态配置設定的 task_struct 表示。在 ./linux/arch/i386/kernel/init_task.c 内可以找到這樣的一個例子。 Linux 内所有程序的配置設定有兩種方式。第一種方式是通過一個哈希表,由 PID 值進行哈希計算得到;第二種方式是通過雙鍊循環表。循環表非常适合于對任務清單進行疊代。由于清單是循環的,沒有頭或尾;但是由于 init_task 總是存在,是以可以将其用作繼續向前疊代的一個錨點。讓我們來看一個周遊目前任務集的例子。 任務清單無法從使用者空間通路,但該問題很容易解決,方法是以子產品形式向核心内插入代碼。清單 2 中所示的是一個很簡單的程式,它會疊代任務清單并會提供有關每個任務的少量資訊(name、pid 和 parent 名)。注意,在這裡,此子產品使用 printk 來發出結果。要檢視具體的結果,可以通過 cat 實用工具(或實時的 tail -f /var/log/messages)檢視 /var/log/messages 檔案。next_task 函數是 sched.h 内的一個宏,它簡化了任務清單的疊代(傳回下一個任務的 task_struct 引用)。 清單 2. 發出任務資訊的簡單核心子產品(procsview.c)
讓我們不妨親自看看如何從使用者空間建立一個程序。使用者空間任務和核心任務的底層機制是一緻的,因為二者最終都會依賴于一個名為 do_fork 的函數來建立新程序。在建立核心線程時,核心會調用一個名為 kernel_thread 的函數(參見 ./linux/arch/i386/kernel/process.c),此函數執行某些初始化後會調用 do_fork。 建立使用者空間程序的情況與此類似。在使用者空間,一個程式會調用 fork,這會導緻對名為 sys_fork 的核心函數的系統調用(參見 ./linux/arch/i386/kernel/process.c)。函數關系如圖 1 所示。 圖 1. 負責建立程序的函數的層次結構從圖 1 中,可以看到 do_fork 是程序建立的基礎。可以在 ./linux/kernel/fork.c 内找到 do_fork 函數(以及合作函數 copy_process)。 do_fork 函數首先調用 alloc_pidmap,該調用會配置設定一個新的 PID。接下來,do_fork 檢查調試器是否在跟蹤父程序。如果是,在 clone_flags 内設定 CLONE_PTRACE 标志以做好執行 fork 操作的準備。之後 do_fork 函數還會調用 copy_process,向其傳遞這些标志、堆棧、系統資料庫、父程序以及最新配置設定的 PID。 新的程序在 copy_process 函數内作為父程序的一個副本建立。此函數能執行除啟動程序之外的所有操作,啟動程序在之後進行處理。copy_process 内的第一步是驗證 CLONE 标志以確定這些标志是一緻的。如果不一緻,就會傳回 EINVAL 錯誤。接下來,詢問 Linux Security Module (LSM) 看目前任務是否可以建立一個新任務。要了解有關 LSM 在 Security-Enhanced Linux (SELinux) 上下文中的更多資訊,請參見 參考資料 小節。 接下來,調用 dup_task_struct 函數(在 ./linux/kernel/fork.c 内),這會配置設定一個新 task_struct 并将目前程序的描述符複制到其内。在新的線程堆棧設定好後,一些狀态資訊也會被初始化,并且會将控制傳回給 copy_process。控制回到 copy_process 後,除了其他幾個限制和安全檢查之外,還會執行一些正常管理,包括在新 task_struct 上的各種初始化。之後,會調用一系列複制函數來複制此程序的各個方面,比如複制開放檔案描述符(copy_files)、複制符号資訊(copy_sighand 和 copy_signal)、複制程序記憶體(copy_mm)以及最終複制線程(copy_thread)。 之後,這個新任務會被指定給一個處理程式,同時對允許執行程序的處理程式進行額外的檢查(cpus_allowed)。新程序的優先級從父程序的優先級繼承後,執行一小部分額外的正常管理,而且控制也會被傳回給 do_fork。在此時,新程序存在但尚未運作。do_fork 函數通過調用 wake_up_new_task 來修複此問題。此函數(可在 ./linux/kernel/sched.c 内找到)初始化某些排程程式的正常管理資訊,将新程序放置在運作隊列之内,然後将其喚醒以便執行。最後,一旦傳回至 do_fork,此 PID 值即被傳回給調用程式,程序完成。
程序排程 存在于 Linux 的程序也可通過 Linux 排程程式被排程。雖然排程程式超出了本文的讨論範圍,但 Linux 排程程式維護了針對每個優先級别的一組清單,其中儲存了 task_struct 引用。任務通過 schedule 函數(在 ./linux/kernel/sched.c 内)調用,它根據加載及程序執行曆史決定最佳程序。在本文的 參考資料 小節可以了解有關 Linux 版本 2.6 排程程式的更多資訊。 程序銷毀 程序銷毀可以通過幾個事件驅動 — 通過正常的程序結束、通過信号或是通過對 exit 函數的調用。不管程序如何退出,程序的結束都要借助對核心函數 do_exit(在 ./linux/kernel/exit.c 内)的調用。此過程如圖 2 所示。 圖 2. 實作程序銷毀的函數的層次結構 do_exit 的目的是将所有對目前程序的引用從作業系統删除(針對所有沒有共享的資源)。銷毀的過程先要通過設定 PF_EXITING 标志來表明程序正在退出。核心的其他方面會利用它來避免在程序被删除時還試圖處理此程序。将程序從它在其生命期間獲得的各種資源分離開來是通過一系列調用實作的,比如 exit_mm(删除記憶體頁)和 exit_keys(釋放線程會話和程序安全鍵)。do_exit 函數執行釋放程序所需的各種統計,這之後,通過調用 exit_notify 執行一系列通知(比如,告知父程序其子程序正在退出)。最後,程序狀态被更改為 PF_DEAD,并且還會調用 schedule 函數來選擇一個将要執行的新程序。請注意,如果對父程序的通知是必需的(或程序正在被跟蹤),那麼任務将不會徹底消失。如果無需任何通知,就可以調用 release_task 來實際收回由程序使用的那部分記憶體。結束語 Linux 還在不斷演進,其中一個有待進一步創新和優化的領域就是程序管理。在堅持 UNIX 原理的同時,Linux 也在不斷突破。新的處理器架構、對稱多處理(SMP)以及虛拟化都将促使在核心領域内取得新進展。其中的一個例子就是 Linux 版本 2.6 中引入的新的 O(1) 排程程式,它為具有大量任務的系統提供了可伸縮性。另外一個例子就是使用 Native POSIX Thread Library (NPTL) 更新了的線程模型,與之前的 LinuxThreads 模型相比,它帶來了更為有效的線程處理。有關這些創新及其前景的更多資訊,請參見 參考資料。 |