進程管理

掌握不深,能力有限,僅作學習探討,需在以後更深層次的去理解。

本篇主要是對於進程知識的一個梳理,順便簡介了一下線程:

進程是Unix操作系統最基本的抽象之一,一個進程就是處於執行期的程序(目標碼存放在某種存儲介質上)。

        執行線程,簡稱線程,實在進程中活動的對象。每個線程都擁有一個獨立的程序計數器、進程棧和一組進程寄存器。

        內核調度的對象是線程,而不是進程。Linux系統的線程實現非常特別——它對線程和進程並不特別區分。對Linux而言,線程只不過是一種特殊的進程罷了。

        進程的另一個名字是任務(task),Linux內核通常把進程也叫做任務。


1 進程描述符及任務結構

      內核把進程存放叫做任務隊列(task list)的雙向循環鏈表中。鏈表中的每一項都是類型爲task_struct、稱爲進程描述符(process descriptor)的結構,該結構定義在<linux.sched.h>中。進程描述符中包含一個具體進程的所有信息。

  進程描述符中包含的數據能完整的描述一個正在執行的程序:它打開的文件、進程的地址空間、掛起的信號、進程的狀態,還有其它更多的信息(如下圖)。


1.1 分配進程描述符

    在2.6以前的內核中,各個進程的task_struct存放在它們內核棧的尾端。這樣做是爲了讓那些想x86這樣寄存器較少的硬件體系結構只要通過棧指針就能計算出它的位置,從而避免使用額外的寄存器專門記錄。由於現在用slab分配器動態生成task_struct,所以只需在棧底(對於向下增長的棧來說)或棧頂(對於向上增長的棧來說)創建一個新的結構struct thread_info(如下圖)。這個新的結構能使在彙編代碼中計算其偏移變得相當容易。



    在x86上,thread_info結構在文件<asm/thread_info.h>中定義如下:    
struct thread_info {
    struct task_struct    *任務;
    struct exec_domain  *exec_domain;
    unsigned long           flags;
    unsigned long           status;
    __u32                      cpu;
    __s32                       preempt_count;
    mm_segment_t        addr_limit;
    struct restart_block   restart_block;
    unsigned long            previous_esp;
    __u8                        supervisor_stack[0];
};
    每個任務的thread_info結構在它的內核棧的尾端分配。結構中task域中存放的是指向該任務實際task_struct的指針。

1.2 進程描述符的存放

    內核通過一個惟一的進程標識值(process identification value)或PID來標識每個進程。PID的最大值默認爲32768(short int短整型的最大值),其表示系統中允許同時存在的進程的最大數目。如果有需要的話,可以通過修改/proc/sys/kernel/pid_max來提高上限。
    內核中大部分處理進程的代碼都是直接通過task_struct進行的。因此,通過current宏查找當前正在運行進程的進程描述符的速度就很重要了。硬件體系結構不同,該宏的實現也不同,它必須針對專門的硬件體系結構作處理。有的硬件體系結構可以拿出一個專門寄存器來存放當前進程task_struct的指針,用於加快訪問速度。而像x86的體系結構(寄存器並不富餘),就只能在內核的尾端創建thread_info結構,通過計算偏移間接地查找task_struct結構。
    在x86系統上,current把棧指針後的13個有效位屏蔽掉,用來計算出thread_info的偏移。該操作通過current_thread_info()函數完成的。彙編代碼如下:
movl  $-8192, %eax
andl   %esp, %eax
    這裏假定棧的大小爲8KN。當4KB的棧啓用時,就要用4096,而不是8192。
    最後,current再從thread_info的task域中提取並返回task_struct的地址:
    current_thread_info()->task;
    對比一下這部分在PowerPC上的實現(IBM基於RISC的現代微處理器),我們可以發現當前task_struct的地址是保存在一個寄存器中的。也就是說,在PPC上,current宏只需把r2寄存器中的值返回就行了。與x86不一樣,PPC有足夠多的寄存器,所以它的實現有這樣的餘地。而訪問進程描述符是一個重要的頻繁操作,所以PPC的內核開發者覺得完全有必要爲此使用一個專門的寄存器。

1.3 進程狀態

    進程描述符中的state域描述了進程的當前狀態(參見下圖)。系統中的每個進程都必然處於五種進程狀態中的一種。該域的值也必爲下列五種狀態標誌之一:
  • TASK_RUNNING(運行)——進程是可執行的;它或者正在執行,或者在運行隊列中等待執行。這是進程在用戶空間中執行惟一可能的狀態;也可以應用到內核空間中正在執行的進程。
  • TASK_INTERRUPTIBLE(可中斷)——進程正在睡眠(也就是被阻塞),等待某些條件的達成。一旦這些條件達成,內核就會把進程狀態設置爲運行。處於此狀態的進程也會因爲接收到信號而提前被喚醒並投入運行。
  • TASK_UNINTERRUPTIBLE(不可中斷)——除了不會因爲接收到信號而被喚醒從而投入運行外,這個狀態與可打斷狀態相同。這個狀態與可打斷狀態相同。這個狀態通常在進程必須在等待時不受干擾或等待事件很快就會發生時出現。由於處於此狀態的任務對信號不作響應,所以較之可中斷狀態,使用的較少。
  • TASK_ZOMBIE(僵死)——該進程已經結束了,但是其父進程還沒有調用wait4()系統調用。爲了父進程能夠獲知它的消息,子進程的描述符仍然被保留着。一旦父進程調用了wait4(),進程描述符就會被釋放。
  • TASK_STOPPED(停止)——進程停止執行;進程沒有投入運行也不能投入運行。通常這種狀態發生在接收到SIGSTOP、SIGTSTP、SIGTTIN、SIGTTOU等信號的時候。此外,在調試期間接收到任何信號,都會使進程進入這種狀態。

1.4 設置當前進程狀態

     內核經常需要調整某個進程的狀態。這時最好使用set_task_state(task, state)函數:
     set_task_state(task, state); /*將任務‘task’的狀態設置爲‘state’*/
     該函數將指定的進程設置爲指定的狀態。必要的時候,它會設置內存屏障來強制其他處理器作重新排序(一般只在SMP系統中由此必要),否則,他等價於:
     task->state = state;
     方法set_current_state(state)和set_task_state(current, state)含義是等同的。

1.5 進程上下文

    可執行程序代碼是進程的重要組成部分。這些代碼從可執行文件載入到進程的地址空間執行。一般程序在用戶空間執行。當一個程序調用執行了系統調用或者觸發了某個異常,它就陷入了內核空間。此時,我們稱內核“代表進程執行”並處於進程上下文中。在此上下文中current宏是有效的。除非在此間隙有更高優先級的進程需要執行並有調度器做出了相應調整,否則在內核退出的時候,程序恢復用戶空間繼續執行。
    系統調用和異常處理程序是對內核明確定義的接口。進程只有通過這些接口才能進入內核執行——對內核的所有訪問方式都必須通過這些接口。

1.6 進程家族樹

    所有進程都是PID爲1的init進程的後代。內核在系統啓動的最後階段啓動init進程。該進程讀取系統的初始化腳本並執行其他的相關程序,最終完成系統啓動的整個過程。

    系統中的每個進程必有一個父進程。相應地,每個進程也可以擁有零個或多個子進程。擁有同一個指向其父進程tast_struct、叫做parent的指針,還包含一個成爲children的子進程鏈表。所以對於當前進程,可以通過下面的代碼獲得其父進程的進程描述符:

struct task_struct *my_parent = current->parent;
同樣,也可以按以下方式依次訪問子進程:

struct task_struct *task;
struct list_head *list;

list_for_each(list, ¤t->children) {
    task = list_entry(list, struct task_struct, sibling);
    /*task 現在指向當前的某個子進程*/
}
    init進程的進程描述符是作爲init_task靜態分配的。下面的代碼可以很好地演示所有進程之間的關係:

struct task_struct *task;

for (task = current; task != &init_task; task = task->parent)
;
/*task 現在指向init*/

    任務隊列是一個雙向循環鏈表。對於給定的進程,獲取鏈表的下一個進程:

    list_entry(task->tasks.next, struct task_struct, tasks)

    獲取前一個進程的方法相同:

    list_entry(task->tasks.prev, struct task_struct, tasks)

    這兩個例程分別通過next_task(task)宏和prev_task(task)宏實現。而實際上,for_each_process(task)宏提供了依次訪問整個任務隊列的能力。每次訪問,任務指針都指向鏈表中的下一個元素:

    struct  task_struct *task;


    for_each_process(task) {

                      /*它打印出每一個任務的名稱和PID*/

                      printk("%s[%d]\n", task->comm, task->pid);

    }

    需要注意的是,在一個擁有大量進程的系統中通過重複來遍歷所有的進程是非常耗時的。因此,如果沒有充足的理由的話別這樣做。


2 進程創建

    Unix的進程創建很特別。許多其他的操作系統都提供了產生(spawn)的機制,首先在新的地址空間裏創建進程,讀入可執行文件,最後開始執行。在Unix中,將上述步驟分解到兩個單獨的函數中去執行:fork()和exec()。fork()通過拷貝當前進程創建一個子進程。子進程與父進程的區別僅僅在於PID(每個進程惟一)、PPID(父進程的進程號,子進程將其設置爲被拷貝進程的PID)和某些資源和統計量(例如掛起信號,它沒有必要被繼承)。exec()函數負責讀取可執行文件並將其載入地址空間開始運行。把這兩個函數組合起來使用的效果跟其他系統使用的單一函數的效果相似。


2.1 寫時拷貝

    傳統的fork()系統調用直接把所有的資源複製給新創建的進程。這種實現過於簡單並且效率低下,因爲它拷貝的數據也許並不共享,更糟的情況是,如果新進程打算立即執行一個新的映像,那麼所有的拷貝都將前功盡棄。Linux的fork()使用寫時拷貝(copy-on-write)頁實現。寫時拷貝是一種可以推遲甚至免除拷貝數據的技術。內核此時並不複製整個進程地址空間,而是讓父進程和子進程共享同一個拷貝。只有在需要寫入的時候,數據纔會被複制,從而使各個進程擁有各自的拷貝。也就是說,資源的複製只有在需要寫入的時候,數據纔會被複制,從而使各個進程擁有各自的拷貝。也就是說,資源的複製只有在需要寫入的時候才進行,在此之前,只是以只讀方式共享。這種技術使地址空間上的頁的拷貝被推遲到實際發生寫入的時候。在頁根本不會被寫入的情況下——舉例來說,fork()後立即調用exec()——它們就無需複製了。fork()的實際開銷就是複製父進程的頁表以及給子進程創建惟一的進程描述符。在一般情況下,進程創建後都會馬上運行一個可執行的文件,這種優化可以避免拷貝大量根本就不會被使用的數據(地址空間裏常常包含數十兆的數據)。由於Unix強調進程快速執行的能力,所以這個優化是很重要的。


2.2 fork()

    Linux通過clone()系統調用實現fork()。這個調用通過一系列的參數標誌來指明父、子進程需要共享的資源。fork()、vfork()和__clone()庫函數都根據各自需要的參數標誌區調用clone()。然後由clone()去調用do_fork()。

    do_fork完成了創建中的大部分工作,它的定義在kernel/fork.c文件中。該函數調用copy_process()函數,然後讓進程開始運行。copy_process()函數的工作過程:

調用dup_task_struct()爲新進程創建一個內核棧、thread_info結構和task_struct,這些值與當前進程的值相同。此時,子進程和父進程的描述是完全相同。

  • 檢查新創建的這個子進程後,當前用戶所擁有的進程數目沒有超出給他分配的資源的限制。
  • 現在,子進程着手使自己與父進程區別開來。進程描述符內的許多成員都要被清0或設爲初始值。進程描述符的成員值並不是繼承而來的,而主要是統計信息。進程描述符中的大多數數據都是共享的。
  • 接下來,子進程的狀態被設置爲TASK_UNINTERRUPTIBLE以保證它不會投入運行。
  • copy_process()調用copy_flags以更新task_struct的flags成員。表明進程是否擁有超級用戶權限的PF_SUPERPRIV標誌被清0。表明進程還有沒有調用exec()函數的PF_FORKNOEXEC標誌被設置。
  • 調用get_pid()爲新進程過去一個有效的PID。
  • 根據傳遞給clone的參數標誌,copy_process()拷貝或共享打開的文件、文件系統信息、信號處理函數、進程地址空間和命名空間等。在一般情況下,這些資源會被給定進程的所有線程共享;否則,這些資源對每個進程是不同的,因此被拷貝到這裏。
  • 讓父進程和子進程平分剩餘的時間片。
  • 最後,copy_process()做掃尾工作並返回一個指向子進程的指針。
    再回到do_fork()函數,如果copy_process()函數成功返回,新創建的子進程被喚醒並讓其投入運行。內核有意選擇子進程首先執行。因爲一般子進程都會馬上調用exec()函數,這樣可以避免寫時拷貝的額外開銷,如果父進程首先執行的話,有可能會開始向地址空間寫入。


2.3 vfork()

    vfork()系統調用和fork()的功能相同,除了不拷貝父進程的頁表項。子進程作爲父進程的一個單獨的線程在它的地址空間裏運行,父進程被阻塞,直到子進程退出或執行exec()。子進程不能向地址空間寫入。
    vfork()系統調用的實現是通過向clone()系統調用傳遞一個特殊標誌來進行的。
  • 在調用copy_process()時,task_struct的vfork_done成員被設置爲NULL。
  • 在執行do_fork()時,如果給定特別標誌,則vfork_done會指向一個特殊地址。
  • 子進程開始執行後,父進程不是馬上恢復執行,而是一直等待,直到子進程通過vfork_done指針向它發送信號。
  • 在調用mm_release()時,該函數用於進程退出內存地址空間,並且檢查vfork_done是否爲空,如果不爲空,則會向父進程發送信號。
  • 回到do_fork(),父進程醒來並返回。
     如果一切執行順利,子進程在新的地址空間裏運行而父進程也恢復了在原地址空間的運行。這樣的實現,開銷確實降低了,不過它的設計並不是優良的。

3 線程在Linux中的實現

    線程機制是現代編程技術中常用的一種抽象。該機制提供了在同一程序內共享內存地址空間運行的一組線程。這些線程還可以共享打開的文件和其他資源。線程機制支持併發程序設計技術(concurrent programming),在多處理器系統上,它能保證真正的並行處理(parallelism)。

    Linux實現線程的機制非常獨特。從內核的角度來說,他並沒有線程這個概念。Linux把所有線程都當作進程來實現。內核並沒有準備特別的調度算法或是定義特別的數據結構來表徵線程。相反,線程僅僅被視爲一個與其他進程共享某些資源的進程。每個線程都擁有惟一隸屬於自己的task_struct,所以在內核中,他看起來就像是一個普通的進程(只是該進程和其它一些進程共享某些資源,如地址空間)。

    上述線程機制的實現與Microsoft Windows或是Sun Solaris等操作系統的實現差異非常大。這些系統都在內核中提供了專門支持線程的機制(這些系統嚐嚐把線程稱作輕量級進程,lightweight process)。“輕量級進程”這種叫法本身就概括了Linux在此處與其它系統的差異。在其它的系統中,相較於重量級的進程,線程被抽象成一種耗費較少資源,運行迅速的執行單元。而對於Linux來說,它只是一種進程間共享資源的手段(Linux的進程本身就夠輕了)。舉個例子來說,假如我們有一個包含4個線程的進程,在提供專門線程支持的系統中,通常會有一個包含指向四個不同線程指針的進程描述符。該描述符負責描述像地址空間、打開的文件這樣的共享資源。線程本身再去描述它獨佔的資源。相反,Linux僅僅創建4個進程並分配4個普通的task_struct結構。建立這4個進程時指定它們共享某些資源就行了。

    線程的創建和普通進程的創建類似,只不過在調用clone()的時候需要傳遞一些參數標誌來指明需要共享的資源:

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

    上面的代碼產生的結果和調用fork()差不多,只是父子倆共享地址空間、文件系統資源、文件描述符合信號處理程序。換個說話就是,新建的進程和它的父進程就是流行的所謂線程。

     對比一下,一個普通的fork()的實現就是:

    clone(SIGCHLD, 0);

     而vfork()的實現是:

    clone(CLONE_VFORK | CLONE_VM | SIGCHLD, 0);

    傳遞給clone()的參數標誌決定了新創建進程的行爲方式和父子進程之間共享的資源種類。下表列舉了這些clone用到的的參數標誌以及它們的作用,這些是在<linux/sched.h>中定義的。

clone()參數標誌
參數標誌 含義
CLONE_FILES 父子進程共享打開文件
CLONE_FS 父子進程共享文件系統信息
CLONE_IDLETASK 將PID設置爲0(只供idle進程使用)
CLONE_NEWNS 爲子進程創建新的命名空間
CLONE_PARENT 指定子進程與父進程擁有同一個父進程
CLONE_SETTID 將TID回寫至用戶空間
CLONE_SETTLS 爲子進程差創建新的TLS
CLONE_SIGHAND 父子進程共享信號處理函數
CLONE_SYSVSEM 父子進程共享Sytem V SEM_UNDO語義
CLONE_THREAD 父子進程放入相同的線程組
CLONE_VFORK 調用vfork(),所以父進程準備睡眠等待子進程將其喚醒
CLONE_UNTRACED 防止跟蹤進程在子進程上強制執行CLONE_PTRACE
CLONE_STOP 以TASK_STOPPED狀態開始進程
CLONE_SETTLS 爲子進程創建新的TLS(thread-local storage)
CLONE_CHILD_CLEARTID 清楚子進程的TID
CLONE_CHILD_SEETID 設置子進程的TID
CLONE_PARENT_SETTID 設置父進程的TID
CLONE_VM 父子進程共享地址空間
   

內核線程

    內核經常需要在後臺一些操作。這種任務可以通過內核線程(kernel thread)完成——獨立運行在內核空間的標準進程。內核線程和普通的進程間的區別在於內核線程沒有獨立的地址空間(實際上它的mm指針被設置爲NULL)。它們只在內核空間運行,從來不切換到用戶空間去。內核進程和普通進程一樣,可以被調度,也可以被搶佔。

    Linux確實會把一些任務交給內核線程去做,想pdflush和ksoftirqd這些任務就是明顯的例子。這些線程在系統啓動時由另外一些內核線程啓動。實際上,內核線程也只能由其它內核線程創建。在現有內核線程中創建一個新的內核線程的方法如下:

    int kernel_thread(int (*fn)(void *), void *arg, unsigned long flags)

    新的任務也是通過向普通的cline()系統調用傳遞特定的flags參數而創建的。在上面的函數返回時,父線程退出,並返回一個指向子線程task_struct的指針。子線程開始運行fn指向的函數,arg是運行時需要用到的參數。一個特殊的clone標誌CLONE_KERNEL定義了內核線程常用到的參數標誌:CLONE_FS、CLONE_FILES、CLONE_SIGHAND。大部分的內核線程把這個標誌傳遞給它們的flags參數。


4 進程的終結

    當一個進程終結時,內核必須釋放它所佔有的資源並把這一不幸告知其父進程。
    一般來說,進程的析構發生在它調用exit()之後,既可能顯式的調用這個系統調用,也可能隱式的從某個程序的主函數返回(C語言編譯器會在main()函數的返回點後面放置調用exit()的代碼)。當進程接受到它既不能處理也不能忽略的信號和異常時,它還可能被動地終結。不管進程是怎麼終結的,該任務大部分都要靠的do_exit()來完成,它要做下面這些繁瑣的工作:
  • 首先,將task_struct中的標誌成員設置爲PF_EXITING
  • 其次,調用del_timer_sync()刪除任一內核定時器。根據返回的結果,它確保沒有定時器在排隊,也沒有定時器處理程序在運行。
  • 如果BSD的進程計賬功是開啓的,do_exit()調用acct_process()來輸出計帳信息。
  • 然後調用_exit_mm()函數放棄進程佔用的mm_struct,如果沒有別的進程使用它們(也就是說,它們沒有被共享),就徹底釋放它們。
  • 接下來調用exit_sem()函數。如果進程排隊等候IPC信號,它則離開隊列。
  • 調用_exit_files()、_exit_fs()、exit_namespace()和exit_sighand(),已分別遞減文件描述符、文件系統數據、進程名字空間和信號處理函數的引用計數。如果其中某些引用計數的數值降爲零,那麼就代表沒有進程在使用相應的資源,此時可以釋放。
  • 接着把存放在task_struct的exit_code成員中的任務退出代碼置位exit()提供的代碼中去,或者去完成任何其它由內核機制規定的退出動作。退出代碼存放在這裏供父進程隨時檢索。
  • 調用exit_notify()向父進程發送信號,將子進程的父進程重新設置爲線程組中的其它線程或init進程,並把進程狀態設成TASK_ZOMBIE。
  • 最後,do_exit()調用schedule()切換到其它進程。因爲處於TASK_ZOMBIE狀態的進程不會再被調度,所以這是進程所執行的最後一段代碼。
     do_exit()的實現在kernel/exit.c文件中可以找到。
     至此,與進程相關聯的所有資源都被釋放掉了(假設進程是這些資源的惟一使用者)。進程不可運行(實際上也沒有地址空間讓它運行)並處於TASK_ZOMBIE狀態。它佔用的所有資源就是內核棧、thread_info結構和tast_strcut結構。此時進程存在的惟一目的就是向它的父進程提供信息。父進程檢索到信息後,或者通知內核那是無關的信息後,由金冊所持有的剩餘內存被釋放,歸還給系統使用。
    

4.1 刪除進程描述符

    在調用了do_exit()之後,儘管線程已經僵死不能再運行了,但是系統還保留了它的進程描述符。前面說過,這樣做可以讓系統有辦法在子進程終結後仍能獲得它的信息。因此,進程終結時所需的清理工作和進程描述符的刪除被分開執行。在父進程獲得已終結的子進程信息後,或者通知內核它並不關注那些信息後,子進程的task_struct結構才被釋放。

    wait()這一族函數都是通過惟一(但是很複雜)的一個系統調用wait4()實現的。它的標準動作是掛起調用它的進程,直到其中一個子進程退出,此時函數會返回該子進程的PID。此外,調用該函數時提供的指針會包含子函數退出時的退出代碼。

    當最終需要釋放進程描述符時,release_task()會被調用,用以完成以下工作:

  • 首先,它調用free_uid()來減少該進程擁有者的進程使用計數。Linux用一個單用戶高速緩存統計和記錄每個用戶佔用的進程數目、文件數目。如果這些數目都將爲0,表明這個用戶沒有任何進程和文件,那麼這塊緩存就可以銷燬了。
  • 然後,release_task()調用unhash_process()從pidhash上刪除該進程,同時也要從task_list中刪除該進程。
  • 接下來,如果這個進程正在被ptrace跟蹤、release_task()將跟蹤的父進程重設爲其最初的父進程並將它從ptrace list上刪除。
  • 最後,release_task()調用put_task_struct()釋放進程內核棧和thread_info結構所佔的頁,並釋放task_struct所佔用的slab高速緩存。
    至此,進程描述符和所有進程獨享的資源就全部釋放掉了。

4.2 孤兒進程長成的進退維谷

    如果父進程在子進程之前退出,必須有機制來保證子進程能找到一個新的父親,否則的話這些成爲孤兒的進程就會在退出時永遠處於僵死狀態,白白的耗費內存 。前面的部分已經有所暗示,對於這個問題,解決方法是給子進程在當前線程組內找一個線程作爲父親,如果不行,就讓init做它們的父進程。在do_exit()中會有調用notify_parent(),該函數會通過forget_original_parent()來執行尋找父過程:
struct task_struct *p, *reaper = father;
struct list_head *list;

if (father->exit_signal != -1) 
    reaper = prev_thread(reaper);
else
    reaper = child_reaper;

if (reaper == father)
    reaper = child_reaper;
    這段代碼將reaper設置爲該進程所在的線程組內的其他進程。如果線程組內沒有其他進程,它就將reaper設置爲child_reaper,也就是init進程。現在,合適的父進程也已經找到了,只需要遍歷所有子進程併爲他們設置新的父進程:
list_for_each(list, &father->children) {
    p = list_entry(list, struct task_struct, sibling);
    reparent_thread(p, reaper, child_reaper);
}
list_for_each(listm &father->ptrace_children) {
    p = list_entry(list, struct task_struct, ptrace_list);
    reparent_thread(p, reaper, child_reaper);
}
    這段代碼遍歷了兩個鏈表:子進程鏈表和ptrace子進程鏈表,給每個子進程設置新的父進程。這兩個鏈表同時存在的原因很有意思,它也是2.6內核的一個新特性。當一個進程被跟蹤是,它被暫時設定爲調試進程的子進程。此時如果它的父進程退出了,系統會爲它和它的所有兄弟重新找一個父進程。在以前的內核中,這就需要遍歷系統所有的進程來找這些子進程。現在的解決辦法是在一個單獨的被ptrace跟蹤的子進程鏈表中搜索相關的兄弟進程——用兩個相關鏈表減輕了遍歷帶來的消耗。
    一旦系統給進程成功的找到和設置了新的父進程,就不會再有出現駐留僵死進程的危險了。init進程會例行調用wait()來等待其子進程,清除所有與其相關的僵死進程。

5 進程小結

    我們討論了進程的一般特性,它爲何如此重要,以及進程與線程之間的關係。然後討論了Linux如何存放和表示進程(用task_struct和thread_info),如何創建進程(通過clone()和fork()),如何把新的執行映像裝入到地址空間(通過exec()系統調用族),如何表示進程的層次關係,父進程又是如何手機其後代的信息(通過wait()系統調用族),以及進程最終如何死亡(強制或自願地調用exit())。
    進程是最基本、最重要的一種抽象,位於每個現代操作系統的核心位置,也是最終導致我們擁有操作系統的根源(通過執行程序)。
    
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章