進程調度之5:系統調用exit與wait4

date: 2014-10-27 10:16linux

1 進程控制原語

這部分詳情請參考APUE(第2版)第8章shell

1.1 進程退出

有2個函數用來正常終止一個進程:_exit當即進入內核,exit則先執行一些清理工做,包括調用執行各終止處理程序(經過atexit函數註冊)和關閉全部標準I/O流(爲全部打開流執行fclose函數),而後調用_exit進入內核。編程

<unistd.h>
    void _exit(int status);
    <stdlib.h>
    void exit(int status);

兩個exit函數都帶有一個int類型的參數,稱之爲終止狀態或退出狀態(exit_status)。main函數中返回一個整型值與用該值調用exit是等價的。因而main函數中,exit(0)等價於return 0。session

此外,進程也可能由於其餘一些狀況而異常終止(好比收到一個越界訪問的信號SIGSEGV)。無論進程如何終止,最後都會執行內核中的同一段代碼(此即後面要討論的系統調用exit的內核代碼)。這段代碼關閉進程全部打開的文件描述,並釋放掉進程所佔用的資源。函數

無論進程如何終止,咱們都但願進程能通知其父進程,能夠理解爲子進程去世時給父進程發一個「報喪」信號,告之本身是如何終止的。父進程能夠調用wait函數獲取子進程的退出狀態。線程

討論下面三個特別的問題:設計

  1. 若是父進程在子進程退出以前退出呢?子進程退出時該把報喪信號發給誰?這種狀況下將由init進程「領養」父進程的全部子進程。
  2. 若是子進程已經終止了,但父進程沒有調用wait函數獲取它的終止狀態又如何?內核爲每一個終止進程保存了必定量的信息,包括子進程的ID、進程終止狀態以及進程使用的CPU時間總量,能夠理解爲子進程雖已去世,但還留着「屍體」等着父進程「收屍」。屍體要保留到父進程調用wait函數來收屍爲止,在此以前,該子進程便成爲一個殭屍進程(zombile)。
  3. 若是被init進程「領養」的進程終止了,系統中豈不會有大量的殭屍進程?不用擔憂,init進程被設計成「不管什麼時候,只要有一個子進程終止,init就會調用wait函數來爲之收屍」,從而防止了在系統中有不少殭屍進程。

1.2 等待子進程終止

有4個wait相關的函數(此外還有一個waitid函數,這裏沒列出來,具體參考APUE)code

<wait.h>
    pid_t  wait4 (pid_t pid, int *status, int options, struct rusage *rusage);
    pid_t  wait( int*  status );
    pid_t  wait3(int*  status, int options, struct rusage*  rusage);
    pid_t  waitpid(pid_t  pid, int*  status, int  options);

wait用來等待任一子進程終止,waitpid可用來等待特定的子進程退出(固然也能夠等待任意子進程退出),wait3多了一個rusage參數,要求內核返回由終進程及其全部子進程使用的資源彙總。這三個函數都是經過系統調用wait4來實現,咱們來分析下wait4的參數:blog

wait4參數分析

這四個函數的返回值都是對應終止子進程的pid,父進程據此能夠知道哪一個子進程終止了。繼承

1.3 進程組與會話

每一個進程除了有一個進程ID以外,還屬於一個進程組。進程組是一個或多個進程的集合,每一個進程組有一個惟一標識進程組ID,進程組ID相似於進程ID,可存放在pid_t數據類型中。進程task_struct結構中pgrp成員即表示進程所屬進程組的ID。

每一個進程組均可以有一個組長進程。組長進程的標識是,其進程ID等於其進程組ID。

一個進程能夠調用setpgid來加入一個現有組(做爲組的成員)或者建立一個新的進程組(做爲組長)。

一個用戶login到系統中之後,可能會啓動許多不一樣的進程(組),全部這些進程使用同一個控制終端(或用來模擬一個終端的窗口),這些使用同一個控制終端的進程(組)屬於同一個會話(session)。

會話能夠是一個或多個進程組的集合。一般由shell的管道線將幾個進程編程一組。一個會話中的幾個進程組能夠分爲一個前臺進程組以及若干個後臺進程組。好比以下shell命令

proc1 | proc2 &
    proc3 | proc4 | proc5

將構成一個會話,該會話中有三個進程組:

  • 其1、前臺進程組即{proc3, proc4, proc5},它們在控制終端的前臺運行;
  • 其2、後臺進程組{proc1, proc2};
  • 其3、shell進程單成一個後臺進程組。

一個會話也有一個惟一標識session ID,相似進程組ID,也能夠存放在pid_t數據類型中。進程task_struct結構中session成員即表示進程所屬會話。一個會話有一個會話首進程(session leader),會話首進程是建立該會話的進程,其task_struct結構中的leader成員非0。

2 系統調用exit

根據對進程控制原語的瞭解,以及進程建立過程的瞭解,不難想象出exit所要作的工做:

  1. 根據進程的財產登記表卡task_struct結構清算進程財產並回收;
  2. 解散進程的家譜,將該進程的子進程交由init進程「領養」。
  3. 保留屍體(task_struct結構自己以及task_struct結構所在的兩個頁面)並給父進程發報喪信號,等到父進程來收屍。
  4. 當前進程終止了,固然須要調度器啓動一次調度。

另外進程調用exit表示進程要最終退出歷史舞臺了,意即當前進程在exit函數的執行過程當中逐步走向消亡,不會從這個函數中返回了。

2.1 主要流程

exit系統調用內核入口爲sys_exit:

<kernel/exit.c>
    asmlinkage long sys_exit(int error_code)
    {
    	do_exit((error_code&0xff)<<8);
    }

可見其核心是do_exit,do_exit的主要流程以下:

do_exit流程

關於關於流程圖,在重點討論下幾個問題。

2.2 進程的p_opptr與p_pptr

task_struct結構中有兩個成員用來指向其父進程,p_oppt和p_pptr,前者能夠理解爲進程的生父(orginal parent),後者能夠理解爲進程的養父或者監護人。在進程建立之初,進程的生父與監護人一致。但在運行中,進程的監護人能夠暫時改變。好比一個進程經過系統調用ptrace來跟蹤另外一進程時,被跟蹤繼承的p_pptr將被設置爲跟蹤進程,跟蹤進程暫時成了被跟蹤進程的監護人,而被跟蹤進程的生父仍然不變。

有趣的是,在判斷當前進程所在的進程組是否爲孤兒進程組、在給父進程發報喪信號時以及將子進程加入新的家譜時(在此以前,已經將子進程的p_pptr設置爲子進程的p_opptr),都只認監護人p_pptr,而不多關注其生父p_opptr。看來進程行事時只認其監護人而不認其親生父親,與現實世界何其類似也。

2.3 爲何要讓父進程來收屍(task_struct結構以及其所在的系統空間的兩個頁面),而不是子進程自行消亡?##

一方面,進程的task_struct結構中有不少統計信息,好比CPU使用時間等,讓父進程來料理後事能夠將這些信息併入父進程的統計信息而不至於丟失;另外一方面,也是更重要的一方面,不管如何系統必須得有一個當前進程,在中斷以及異常的服務程序中要用到當前進程的系統空間堆棧。若是在下一個進程投入運行以前,就把當前進程的系統空間回收,這樣就存在一個空檔,若是恰巧此時有中斷髮生就會形成問題。

3 系統調用wait4

進程在調用exit以後,系統還保留着進程的屍體等待其父進程來料理後事,父進程正在wait4中等着哩。

瞭解了wait4的原語,理解其內核實現應該很容易了。wait4的內核入口是sys_wait,一樣定義在exit.c文件中。其主要流程以下:

wait4流程

函數的主題爲兩層循環,若是當前進程爲線程,外層循環則遍歷同線程組全部進程。內存循環是變量進程的全部子進程。還記得進程的家譜嗎,經過進程的家譜則能夠遍歷全部子進程。當知足下列條件之一時,經過goto end_wait4來結束這個系統調用:

  • 所等待的子進程狀態爲TASK_STOPPED或者爲TASK_ZOMBIE;
  • 所等待的子進程存在,但不在上述兩個狀態;而入參options設置了WNOHANG標誌,表示非阻塞。
  • 所等待的子進程不存在(進程號爲pid的進程或者不存在,或者不是當前進程的子進程)。

不然,當前進程將本身的狀態設置爲TASK_INTERRUPTIBLE,並再循環外調用schedule來進入淺度睡眠而讓其餘的進程先運行。別忘了,在此以前(sys_wait4函數開始處)定義了一個等待節點wait,並加入了當前進程的等待隊列頭wait_chldexit。此後,若是有子進程退出,子進程調用do_notify_parent來通知父進程。父進程被喚醒後,繼續從repeat標號處從新開始執行。

等待隊列節點wait_queue_t類型以及等待隊列頭wait_queue_head_t類型定義在<include/linux/wait.h>中:

struct __wait_queue {
    	unsigned int flags;
    #define WQ_FLAG_EXCLUSIVE	0x01
    	struct task_struct * task;
    	struct list_head task_list;
    #if WAITQUEUE_DEBUG
    	long __magic;
    	long __waker;
    #endif
    };
    typedef struct __wait_queue wait_queue_t;
    
    struct __wait_queue_head {
    	wq_lock_t lock;
    	struct list_head task_list;
    #if WAITQUEUE_DEBUG
    	long __magic;
    	long __creator;
    #endif
    };
    typedef struct __wait_queue_head wait_queue_head_t;

等待隊列節點經過task_list鏈入到等待隊列頭所領銜的鏈表中,同時每一個等待隊列節點都關聯了一個進程的task_struct結構,當經過wake_up系列函數來喚醒等待隊列頭所領銜的等待隊列時,將喚醒全部或者其中一個等待節點(若是傳入WQ_FLAG_EXCLUSIVE標誌將獨佔喚醒,只喚醒其中一個節點)所關聯的進程。

相關文章
相關標籤/搜索