天天看點

Linux 程序管理剖析(建立、管理、排程和銷毀)

Linux 是一種動态系統,能夠适應不斷變化的計算需求。Linux 計算需求的表現是以程序 的通用抽象為中心的。程序可以是短期的(從指令行執行的一個指令),也可以是長期的(一種網絡服務)。是以,對程序及其排程進行一般管理就顯得極為重要。

在使用者空間,程序是由程序辨別符(PID)表示的。從使用者的角度來看,一個 PID 是一個數字值,可惟一辨別一個程序。一個 PID 在程序的整個生命期間不會更改,但 PID 可以在程序銷毀後被重新使用,是以對它們進行緩存并不見得總是理想的。

在使用者空間,建立程序可以采用幾種方式。可以執行一個程式(這會導緻新程序的建立),也可以在程式内,調用一個<code>fork</code> 或<code>exec</code> 系統調用。<code>fork</code> 調用會導緻建立一個子程序,而

<code>exec</code> 調用則會用新程式代替目前程序上下文。接下來,我将對這幾種方法進行讨論以便您能很好地了解它們的工作原理。

在本文中,我将按照下面的順序展開對程序的介紹,首先展示程序的核心表示以及它們是如何在核心内被管理的,然後來看看程序建立和排程的各種方式(在一個或多個處理器上),最後介紹程序的銷毀。

在 Linux 核心内,程序是由相當大的一個稱為 <code>task_struct</code> 的結構表示的。此結構包含所有表示此程序所必需的資料,此外,還包含了大量的其他資料用來統計(accounting)和維護與其他程序的關系(父和子)。對<code>task_struct</code> 的完整介紹超出了本文的範圍,清單 1 給出了<code>task_struct</code> 的一小部分。這些代碼包含了本文所要探索的這些特定元素。<code>task_struct</code>

位于 ./linux/include/linux/sched.h。

在清單 1 中,可以看到幾個預料之中的項,比如執行的狀态、堆棧、一組标志、父程序、執行的線程(可以有很多)以及開放檔案。我稍後會對其進行詳細說明,這裡隻簡單加以介紹。<code>state</code> 變量是一些表明任務狀态的比特位。最常見的狀态有:<code>TASK_RUNNING</code> 表示程序正在運作,或是排在運作隊列中正要運作;<code>TASK_INTERRUPTIBLE</code> 表示程序正在休眠、<code>TASK_UNINTERRUPTIBLE</code>

表示程序正在休眠但不能叫醒;<code>TASK_STOPPED</code> 表示程序停止等等。這些标志的完整清單可以在 ./linux/include/linux/sched.h 内找到。

<code>flags</code> 定義了很多訓示符,表明程序是否正在被建立(<code>PF_STARTING</code>)或退出(<code>PF_EXITING</code>),或是程序目前是否在配置設定記憶體(<code>PF_MEMALLOC</code>)。可執行程式的名稱(不包含路徑)占用<code>comm</code>(指令)字段。

每個程序都會被賦予優先級(稱為 <code>static_prio</code>),但程序的實際優先級是基于加載以及其他幾個因素動态決定的。優先級值越低,實際的優先級越高。

<code>tasks</code> 字段提供了連結清單的能力。它包含一個 <code>prev</code> 指針(指向前一個任務)和一個<code>next</code> 指針(指向下一個任務)。

程序的位址空間由 <code>mm</code> 和 <code>active_mm</code> 字段表示。<code>mm</code> 代表的是程序的記憶體描述符,而<code>active_mm</code> 則是前一個程序的記憶體描述符(為改進上下文切換時間的一種優化)。

<code>thread_struct</code> 則用來辨別程序的存儲狀态。此元素依賴于 Linux 在其上運作的特定架構,在 ./linux/include/asm-i386/processor.h 内有這樣的一個例子。在此結構内,可以找到該程序自執行上下文切換後的存儲(硬體系統資料庫、程式計數器等)。

現在,讓我們來看看如何在 Linux 内管理程序。在很多情況下,程序都是動态建立并由一個動态配置設定的<code>task_struct</code> 表示。一個例外是<code>init</code> 程序本身,它總是存在并由一個靜态配置設定的

<code>task_struct</code> 表示。在 ./linux/arch/i386/kernel/init_task.c 内可以找到這樣的一個例子。

Linux 内所有程序的配置設定有兩種方式。第一種方式是通過一個哈希表,由 PID 值進行哈希計算得到;第二種方式是通過雙鍊循環表。循環表非常适合于對任務清單進行疊代。由于清單是循環的,沒有頭或尾;但是由于<code>init_task</code> 總是存在,是以可以将其用作繼續向前疊代的一個錨點。讓我們來看一個周遊目前任務集的例子。

任務清單無法從使用者空間通路,但該問題很容易解決,方法是以子產品形式向核心内插入代碼。清單 2 中所示的是一個很簡單的程式,它會疊代任務清單并會提供有關每個任務的少量資訊(<code>name</code>、<code>pid</code> 和<code>parent</code> 名)。注意,在這裡,此子產品使用<code>printk</code> 來發出結果。要檢視具體的結果,可以通過

<code>cat</code> 實用工具(或實時的 <code>tail -f /var/log/messages</code>)檢視 /var/log/messages 檔案。<code>next_task</code> 函數是 sched.h 内的一個宏,它簡化了任務清單的疊代(傳回下一個任務的<code>task_struct</code> 引用)。

可以用清單 3 所示的 Makefile 編譯此子產品。在編譯時,可以用 <code>insmod procsview.ko</code> 插入子產品對象,也可以用<code>rmmod procsview</code> 删除它。

插入後,/var/log/messages 可顯示輸出,如下所示。從中可以看到,這裡有一個空閑任務(稱為<code>swapper</code>)和<code>init</code> 任務(pid 1)。

注意,還可以辨別目前正在運作的任務。Linux 維護一個稱為 <code>current</code> 的符号,代表的是目前運作的程序(類型是<code>task_struct</code>)。如果在<code>init_module</code> 的尾部插入如下這行代碼:

會看到:

注意到,目前的任務是 <code>insmod</code>,這是因為 <code>init_module</code> 函數是在<code>insmod</code> 指令執行的上下文運作的。<code>current</code> 符号實際指的是一個函數(<code>get_current</code>)并可在一個與 arch 有關的頭部中找到(比如 ./linux/include/asm-i386/current.h 内找到)。

讓我們不妨親自看看如何從使用者空間建立一個程序。使用者空間任務和核心任務的底層機制是一緻的,因為二者最終都會依賴于一個名為<code>do_fork</code> 的函數來建立新程序。在建立核心線程時,核心會調用一個名為<code>kernel_thread</code> 的函數(參見 ./linux/arch/i386/kernel/process.c),此函數執行某些初始化後會調用<code>do_fork</code>。

建立使用者空間程序的情況與此類似。在使用者空間,一個程式會調用 <code>fork</code>,這會導緻對名為<code>sys_fork</code> 的核心函數的系統調用(參見 ./linux/arch/i386/kernel/process.c)。函數關系如圖 1 所示。

從圖 1 中,可以看到 <code>do_fork</code> 是程序建立的基礎。可以在 ./linux/kernel/fork.c 内找到<code>do_fork</code> 函數(以及合作函數<code>copy_process</code>)。

<code>do_fork</code> 函數首先調用 <code>alloc_pidmap</code>,該調用會配置設定一個新的 PID。接下來,<code>do_fork</code> 檢查調試器是否在跟蹤父程序。如果是,在<code>clone_flags</code> 内設定<code>CLONE_PTRACE</code> 标志以做好執行 fork 操作的準備。之後

<code>do_fork</code> 函數還會調用 <code>copy_process</code>,向其傳遞這些标志、堆棧、系統資料庫、父程序以及最新配置設定的 PID。

新的程序在 <code>copy_process</code> 函數内作為父程序的一個副本建立。此函數能執行除啟動程序之外的所有操作,啟動程序在之後進行處理。<code>copy_process</code> 内的第一步是驗證<code>CLONE</code> 标志以確定這些标志是一緻的。如果不一緻,就會傳回<code>EINVAL</code> 錯誤。接下來,詢問 Linux Security Module (LSM) 看目前任務是否可以建立一個新任務。

接下來,調用 <code>dup_task_struct</code> 函數(在 ./linux/kernel/fork.c 内),這會配置設定一個新<code>task_struct</code> 并将目前程序的描述符複制到其内。在新的線程堆棧設定好後,一些狀态資訊也會被初始化,并且會将控制傳回給<code>copy_process</code>。控制回到

<code>copy_process</code> 後,除了其他幾個限制和安全檢查之外,還會執行一些正常管理,包括在新<code>task_struct</code> 上的各種初始化。之後,會調用一系列複制函數來複制此程序的各個方面,比如複制開放檔案描述符(<code>copy_files</code>)、複制符号資訊(<code>copy_sighand</code> 和<code>copy_signal</code>)、複制程序記憶體(<code>copy_mm</code>)以及最終複制線程(<code>copy_thread</code>)。

之後,這個新任務會被指定給一個處理程式,同時對允許執行程序的處理程式進行額外的檢查(<code>cpus_allowed</code>)。新程序的優先級從父程序的優先級繼承後,執行一小部分額外的正常管理,而且控制也會被傳回給<code>do_fork</code>。在此時,新程序存在但尚未運作。<code>do_fork</code> 函數通過調用<code>wake_up_new_task</code> 來修複此問題。此函數(可在 ./linux/kernel/sched.c

内找到)初始化某些排程程式的正常管理資訊,将新程序放置在運作隊列之内,然後将其喚醒以便執行。最後,一旦傳回至<code>do_fork</code>,此 PID 值即被傳回給調用程式,程序完成。

存在于 Linux 的程序也可通過 Linux 排程程式被排程。雖然排程程式超出了本文的讨論範圍,但 Linux 排程程式維護了針對每個優先級别的一組清單,其中儲存了<code>task_struct</code> 引用。任務通過<code>schedule</code> 函數(在 ./linux/kernel/sched.c 内)調用,它根據加載及程序執行曆史決定最佳程序。

程序銷毀可以通過幾個事件驅動 — 通過正常的程序結束、通過信号或是通過對 <code>exit</code> 函數的調用。不管程序如何退出,程序的結束都要借助對核心函數<code>do_exit</code>(在 ./linux/kernel/exit.c 内)的調用。此過程如圖 2 所示。

<code>do_exit</code> 的目的是将所有對目前程序的引用從作業系統删除(針對所有沒有共享的資源)。銷毀的過程先要通過設定<code>PF_EXITING</code> 标志來表明程序正在退出。核心的其他方面會利用它來避免在程序被删除時還試圖處理此程序。将程序從它在其生命期間獲得的各種資源分離開來是通過一系列調用實作的,比如<code>exit_mm</code>(删除記憶體頁)和<code>exit_keys</code>(釋放線程會話和程序安全鍵)。<code>do_exit</code>

函數執行釋放程序所需的各種統計,這之後,通過調用<code>exit_notify</code> 執行一系列通知(比如,告知父程序其子程序正在退出)。最後,程序狀态被更改為<code>PF_DEAD</code>,并且還會調用<code>schedule</code> 函數來選擇一個将要執行的新程序。請注意,如果對父程序的通知是必需的(或程序正在被跟蹤),那麼任務将不會徹底消失。如果無需任何通知,就可以調用<code>release_task</code> 來實際收回由程序使用的那部分記憶體。

Linux 還在不斷演進,其中一個有待進一步創新和優化的領域就是程序管理。在堅持 UNIX 原理的同時,Linux 也在不斷突破。新的處理器架構、對稱多處理(SMP)以及虛拟化都将促使在核心領域内取得新進展。其中的一個例子就是 Linux 版本 2.6 中引入的新的 O(1) 排程程式,它為具有大量任務的系統提供了可伸縮性。另外一個例子就是使用 Native POSIX Thread Library (NPTL) 更新了的線程模型,與之前的 LinuxThreads

模型相比,它帶來了更為有效的線程處理。

繼續閱讀