Linux程式管理、程式建立、執行緒實現、殭屍程式

FreeeLinux發表於2017-01-08

目錄

概述

在 Linux 系統中,通過複製一個現有的程式來建立一個全新的程式。呼叫 fork() 的程式稱為父程式,新產生的程式稱為子程式。呼叫結束時,在返回點這個相同位置上,父程式恢復執行,子程式開始執行。fork() 系統呼叫從核心返回兩次:一次回到父程式,另一次回到新產生的子程式。

通常,建立新的程式都是為了執行不同的程式,接著呼叫exec()這組函式就可建立新地址空間,並將新程式載入其中。fork()底層是由clone()系統呼叫實現的。

最終,通過 exit() 系統呼叫推出執行,這個函式終結程式並釋放資源。父程式可通過 wait4() 系統呼叫查詢子程式是否終結(這使得父程式擁有等待特定程式的能力)。程式退出設定為僵死狀態,直到其父程式呼叫 wait() 或 waitpid() 為止。

程式描述符及任務結構

每個程式都是一個 task_struct 結構體,核心使用雙向連結串列 task_list 來儲存。task_struct 包含的資料用來描述程式:開啟的檔案、地址空間、掛起訊號、程式狀態及其他資訊。

分配程式描述符

Linux 通過 slab 分配器分配 task_struct,這樣能達到物件複用和快取著色(cache coloring) 的目的。slab 分配器在核心棧底建立新的結構 thread_info,該結構含指向 task_struct 的指標。

struct thread_info {
    struct task_struct    *task;
    struct exec_domain    *exec_domain;
    __u32                 flags;
    __u32                 status;
    __u32                 cpu;
    int                   preempt_count;
    mm_segment_t          addr_limit;
    struct restart_block  restart_block;
    void                  *sysenter_return;
    int                   uaccess_err;
};


效果如圖:
下過

程式描述符的存放

程式描述符 pid 預設最大值被設定為 32768。
系統管理員修改可通過/proc/sys/kernel/pid_max 來提高上限。

程式狀態

task_struct 的 state 域描述程式狀態。程式五狀態如下:

  • TASK_RUNNING:就緒或者執行態。
  • TASK_INTERRUPTIBLE: 程式被阻塞或者睡眠。條件達成或收到訊號可進入執行態。
  • TASK_UNINTERRUPTIBLE:也是阻塞或者睡眠,只有條件達成響應,對訊號或中斷不響應。
  • __TASK_TRACED: 被其他程式跟蹤的程式,如 pstrace 對除錯程式跟蹤。
  • __TASK_STOPPPED:停止執行。通常收到 SIGSTOP 等訊號進入此狀態。


狀態轉化如圖:
這裡寫圖片描述

程式家族樹

所有程式都是 pid 為 1 的 init 程式後代。
task_struct 中有指向父程式 task_struct 的 parent 指標,還包含一個稱為 children 的子程式連結串列。

程式建立

首先使用 fork() 拷貝當前程式建立一個子程式。子程式與父程式唯一區別僅為 pid,ppid 及某些資源的統計量(例如,掛起的訊號)。exec() 函式負責讀取可執行檔案並將其載入地址空間開始執行。

寫時拷貝

為避免把所有資源都複製給新程式的效率浪費,fork() 採用 copy-on-write 頁實現。fork() 的實際開銷就是複製父程式的頁表以及給子程式建立唯一的程式描述符。一般情況下,程式建立後馬上執行可執行檔案,這種優化可避免拷貝大量根本就不會使用的資料。

fork()

fork()、vfork() 和 __clone() 庫函式都通過相應引數標誌呼叫 clone(),然後clone()呼叫 do_fork(),do_fork() 呼叫 copy_process() 函式。
該函式之中為新程式建立核心棧、task_info、task_struct,並清零 task_struct 某些成員(主要是統計資訊),大多數資料依然未修改。子程式設定為 TASK_UNINTERRUPTIBLE 確保不會投入執行。分配 pid,根據傳遞給 clone() 的引數標誌,選擇拷貝或共享開啟的檔案、檔案系統資訊、訊號處理函式、程式地址空間和名稱空間等。最後返回一個子程式的指標。

vfork()

vfork() 與 fork() 的唯一區別是不拷貝父程式的頁表項。子程式作為父程式一個單獨的執行緒在它的地址空間執行,父程式被阻塞,直到子程式退出或執行 exec()。子程式不能像地址空間寫入。

執行緒實現

建立執行緒

建立執行緒和建立程式只是呼叫clone()時引數標誌不一樣:

    clone(CLONE_VM | CLONE_FD | CLONE_FILES | CLONE_SIGHAND, 0);

引數就是共享的資源。由上可知,執行緒和父程式共享虛擬空間(VM)、檔案系統資源、檔案描述符和訊號處理程式,而fork()的實現是:

    clone(SIGCHLD, 0);

核心執行緒

核心執行緒和普通執行緒區別在於沒有獨立的地址空間,只在核心空間執行。核心程式和普通程式一樣,可以被排程,也可以被搶佔。

程式終結

由 do_exit() 函式負責。程式終結會設定相關 task_struct 成員,刪除相關核心定時器,輸出記賬資訊,釋放其地址空間(如果沒有共享),減少相關資源引用計數,呼叫 exit_notify() 向父程式傳送訊號,給自己的子程式尋找養父,養父為執行緒組的其他程式或為 init 程式,並把 state 設定為 EXIT_ZOMBIE 狀態。do_exit()函式再呼叫 schedule() 切換到新的程式,而自己成為殭屍程式不會再被排程,所以這是程式執行的最後一段程式碼。do_exit() 永不返回。至此程式佔用的記憶體僅剩核心棧、thread_info 結構和 task_struct 結構。此時程式還存在的唯一目的就是向其父程式提供資訊,父程式檢索到資訊後,或者通知核心那是無關的資訊後,核心釋放程式剩餘記憶體。

刪除程式描述符

在呼叫 do_exit() 後,儘管程式成為殭屍程式,但是系統還保留了它的程式描述符。這樣可讓系統有辦法在該程式終結後仍能獲得它的資訊。因此,程式終結時所需的清理工作和程式描述符的刪除分開執行。在該程式的父程式獲知該程式的資訊後,或者父程式通知核心它並不關注那些資訊後,它的子程式(當前殭屍程式)的 task_struct 結構被釋放。

如何處理殭屍程式

1.使用父程式呼叫 wait() 或 waitpid() 函式等待子程式,父程式阻塞。

2.非同步方式,安裝 signal_handler,捕獲SIGCHILD訊號,在訊號處理函式中呼叫wait()函式等待。但是由於訊號不支援排隊,為避免訊號高發時丟失訊號,在處理函式中應使用 waitpid() 函式的非阻塞版本,迴圈對每一個子程式進行 wait 處理。方法如下:

    while(pid = waitpid(-1, &status, WNOHANG)) > 0){
        //printf("child %d exit\n", pid);
    }

引數設定為-1是因為:

-1 meaning wait for any child process.
WNOHANG 則是非阻塞模式標誌。

3.直接使用 sigaction 結構體,當中的選項設定 SA_NOCLDWAIT,這樣就不需要任何處理,告訴核心直接終結子程式,不要進入殭屍狀態。

    struct sigaction act;
    act.sa_flags = SA_NOCLDWAIT;
    sigaction(SIGCHLD, &act, NULL);

相關文章