[Linux內核設計與實現]Linux進程管理


進程描述符及任務結構


進程是Unix操作系統最基本的抽象之一(另一個抽象是文件)。進程是正在執行的程序代碼的活標本,不但包括正在執行的代碼還包括其他資源。比如打開的文件、掛起的信號、內核內部數據、處理器狀態、地址空間及一個或者多個執行線程(thread of executing)、存放全局變量的數據段等。執行線程簡稱線程,是在進程中活動的對象。Linux線程是一種特殊的進程(共享資源),內核調度的對象是線程而不是進程。進程提供兩種虛擬機制:虛擬處理器和虛擬內存。虛擬處理器給進程一種假象,讓其認爲自己在獨享處理器;而虛擬內存讓進程在獲取和使用內存時覺得自己擁有整個系統的所有內存資源。Linux中進程創建通過fork()系統調用實現。


內核把進程存放在叫做任務隊列(task list)的雙向循環鏈表中。鏈表中的每一項都是類型爲task_struct、稱爲進程描述符(process descriptor)的結構,該結構定義在<linux/sched.h>文件中。進程描述符中包含一個具體進程的所有信息。Linux通過slab分配器(內存分配機制)分配task_struct結構,使得創建進程非常迅速。每個任務有一個thread_info結構,它在內核棧的尾部分配,結構中的task域中存放的是指向該任務實際task_struct的指針。thread_info結構在文件<asm/thread_info.h>中定義。

struct thread_info {
        struct task_struct      *task;          /* main task structure */
        struct exec_domain      *exec_domain;   /* execution domain */
        __u32                   flags;          /* low level flags */
        __u32                   status;         /* thread synchronous flags */
        __u32                   cpu;            /* current CPU */
        int                     preempt_count;  /* 0 => preemptable,
                                                   <0 => BUG */
        mm_segment_t            addr_limit;
        struct restart_block    restart_block;
        void __user             *sysenter_return;
#ifdef CONFIG_X86_32
        unsigned long           previous_esp;   /* ESP of the previous stack in
                                                   case of nested (IRQ) stacks
                                                */
        __u8                    supervisor_stack[0];
#endif
        int                     uaccess_err;
};

內核通過一個唯一的進程標識符(process identification value)或pid來標識每個進程。


進程包括下面五種狀態:

  1. TASK_RUNNING(運行)進程是可執行的;它或者正在執行,或者在運行隊列中等待執行。這是進程在用戶空間中執行唯一可能的狀態;也可以應用到內核空間中正在執行的進程
  2. TASK_INTERRUPTIBLE(可中斷)進程正在睡眠(或者阻塞),等待某些條件的達成。一旦條件達成,內核就會把進程狀態修改爲運行。處於此狀態的進程也會因爲接收到信號而提前被喚醒並投入運行。
  3. TASK_UNINTERRUPTIBLE(不可中斷)除了不會接收到信號而被喚醒從而投入運行外,這個狀態和可中斷狀態相同。
  4. TASK_ZOMBLE(僵死)該進程已經結束了,但是其父進程還沒有調用wait4系統調用。爲了父進程能夠獲得它的信息,子進程的進程描述符仍然被保留着。一旦父進程調用了wait4,進程描述符就會釋放。
  5. TASK_STOPPED(停止)進程停止執行;進程沒有投入運行也不能投入運行。通常這種狀態發生在接收到SIGSTOP SIGTSTP SIGTTIN SIGTTOU等信號的時候。此外,在調試期間接收到任何信號,都會使進程進入這種狀態。


一般程序在用戶空間執行,當一個程序執行了系統調用或者觸發了某個異常,它就會陷入內核空間。此時,我們稱內核“代表進程執行”並處於進程上下文中。除非在此間隙有更高優先級的進程需要執行並有調度器做出了相應的調整,否則在內核退出的時候,程序恢復到用戶空間繼續執行。Unix系統的進程之間存在一個明顯的繼承關係,在Linux系統中也是如此。所有的進程都是PID爲1的init進程的後代。


進程創建

Unix採用兩個步驟來創建進程:fork()和exec()。首先fork()通過拷貝當前進程創建一個子進程。子進程與父進程的區別僅僅在於PID、PPID和某些資源與統計量。exec()函數負責讀取可執行文件並將其載入地址空間開始運行。


寫時拷貝

Linux的fork()使用寫時拷貝(copy-on-write)頁實現。寫時拷貝是一種可以推遲甚至避免拷貝數據的技術。內核此時並不複製整個進程地址空間,而是讓父進程和子進程共享一個拷貝。只有在需要寫入的時候,數據纔會被複制,從而使各個進程擁有各自的拷貝。也就是說,資源的複製只有在需要寫入的時候才進行,在此之前,只是以只讀的方式共享。這種技術使地址空間的頁的拷貝被推遲到實際發生寫入的時候。在頁根本不需要寫入的情況下,數據就無需複製了。比如fork()之後,子進程立即執行exec()函數。fork()的實際開銷就是複製父進程的頁表以及給子進程創建唯一的進程描述符。這個優化對提高進程創建很有幫助。


fork()

Linux通過clone()系統調用實現fork()。這個系統調用通過一系列的參數標誌來指明父、子進程所要共享的資源。fork()、 vfork()、 __clone()庫函數都根據各自需要的參數標誌去調用clone(),然後由clone()去調用do_fork()。do_fork()完成了創建進程的大部分工作,它的定義在kernel/fork.c文件中。該函數調用copy_process()函數,然後讓進程開始運行。copy_process()函數完成以下功能:

  1. 調用dup_task_struct()爲新進程創建一個內核棧、thread_info機構和task_struct,這些值與當前進程相同,此時,子進程與父進程的進程描述符是完全相同的。
  2. 檢查新創建的這個子進程後,當前用戶所擁有的進程數目沒有超出給它分配的資源的限制。
  3. 現在,子進程開始着手使自己與父進程區別開來。進程描述符內的許多成員都要被清零或者設置爲初始指。
  4. 接下來,子進程的狀態被設置爲TASK_UNINTERRUPTIBLE以保證它不會投入運行。
  5. copy_process()調用copy_flags()以更新task_struct的flags成員。表明進程是否擁有超級用戶權限的PF_SUPERPRIV標誌被清零。表明進程還沒有調用exec()函數的PF_FORKNOEXEC標誌被設置。
  6. 調用get_pid()爲新進程獲取一個有效的PID。
  7. 根據傳遞給clone()的參數標誌,copy_process()函數拷貝或共享打開的文件、文件系統信息、信號處理函數、進程地址空間和命名空間等。在一般情況下,這些資源會被給定進程的所有線程共享;否則,這些資源對每個進程是不同的,因此被拷貝到這裏。
  8. 讓父進程和子進程平分剩餘的時間片。
  9. 最後,copy_process()作掃尾工作並返回一個指向子進程的指針。


再回到do_fork()函數,如果copy_process()函數成功返回,新創建的子進程被喚醒並讓其投入執行。內核有意選擇子進程首先執行,因爲一般子進程都會馬上調用exec()函數,這樣可以避免寫時拷貝的額外開銷,如果父進程首先執行的話,有可能會開始向地址空間寫入數據。


vfork()

vfork()系統調用和fork()的功能相同,除了不拷貝父進程的頁表項。子進程作爲父進程的一個單獨的線程在它的地址空間裏運行,父進程被阻塞,直到子進程退出或者調用exec()。子進程不能向地址空間寫入。vfork()系統調用的實現是通過向clone()系統調用傳遞一個特殊標誌來進行。

  1. 在調用copy_process()時,task_struct的vfork_done成員被設置爲NULL。
  2. 在執行do_fork()時,如果給定特別標誌,則vfork_done會指向一個特殊地址。
  3. 子進程開始執行後,父進程不是馬上恢復執行,而是一直等待,直到子進程通過vfork_done指針向它發送信號。
  4. 在調用mm_release()時,該函數用於進程退出內存地址空間,並且檢查vfork_done是否爲空,如果不爲空,則會向父進程發送信號。
  5. 回到do_fork(),父進程醒來並返回。


負責創建進程的函數的層次結構

負責創建進程的函數的層次結構


線程在Linux中的實現

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


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



進程終結

一般來說,進程的析構發生在它調用exit()之後,既可能顯示的調用這個系統調用,也可能隱式的從某個程序的主函數返回(其實C語言編譯器會在main()函數的返回點後面放置調用exit()的代碼)。當進程接受到它既不能處理也不能忽略的信號或者異常時,它還能被動的終結。進程終結一般通過do_exit()來完成。

  1. 首先,將task_struct中的標誌成員設置爲PF_EXITING。
  2. 其次,調用del_timer_sync()刪除任一內核定時器。通過返回的結果,它確保沒有定時器在排隊,也沒有定時器處理程序在運行。
  3. 如果BSD的進程記賬功能開啓,do_exit()調用acct_process()來輸出記賬信息。
  4. 然後調用_exit_mm()函數放棄進程佔用的mm_struct,如果沒有別的進程使用它們,就徹底釋放它們。
  5. 接下來調用exit__sem()函數。如果進程排隊等候IPC信號,它則離開隊列。
  6. 調用_exit_files() _exit_fs() exit_namespace()和exit_sighand(),以分別遞減文件描述符、文件系統數據、進程名字空間和信號處理函數的引用計數。如果某些引用計數的數值降爲0,那麼就代表沒有進程在使用這些相應的資源,此時可以釋放。
  7. 接着把存放在task_struct的exit_code成員中的任務退出代碼設置爲exit()提供的代碼中,或者去完成任何其他由內核機制規定的退出動作。退出代碼存放在這裏供父進程隨時檢索。
  8. 調用exit_notify()想父進程發送信號,將子進程的父進程重新設置爲線程組中的其他線程或init進程,並把進程狀態設成TASK_ZOMBLE。
  9. 最後,do_exit()調用schedule()函數切換到其他進程。

至此,與進程相關聯的所有資源都被釋放掉了,進程不可運行並處於TASK_ZOMBLE狀態。它佔用的資源就是內核桟、thread_info結構和task_struct結構。此時進程存在的惟一目的就是向它的父進程提供信息。


刪除進程描述符

在調用了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高速緩存。


至此,進程描述符和所有進程獨享的資源就會全部釋放掉了。如果父進程在子進程之前退出,系統會將子進程重新尋找一個父進程。這樣系統中就不會出現孤兒進程。


實現進程銷燬的函數的層次結構
實現進程銷燬的函數的層次結構


小結

討論了進程的一般特性,以及進程與線程之間的關係。然後討論了Linux如何存放和表示進程(用task_struct和thread_info),如何創建進程(通過clone()和fork()),如何把新的執行映像裝入到地址空間(通過exec()系統調用族),如何表示進程的層次關係,父進程又是如何收集其後代的信息(通過wait()系統調用族),以及進程最終如何死亡(強制或自願的調用exit())。



參考資料:

《Linux內核設計與實現》,第二版

Linux 進程管理剖析

--------------------------------------------------------------------------------

2012-12-24  第一次完成

2013-1-3  增加進程創建與銷燬引用圖片



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