天天看點

exit()與_exit()函數wait與waitpid及孤兒僵屍

exit()與_exit()函數wait與waitpid及孤兒僵屍

注:exit()就是退出,傳入的參數是程式退出時的狀态碼,0表示正常退出,其他表示非正常退出,一般都用-1或者1,标準C裡有EXIT_SUCCESS和EXIT_FAILURE兩個宏,用exit(EXIT_SUCCESS);可讀性比較好一點。

作為系統調用而言,_exit和exit是一對孿生兄弟,它們究竟相似到什麼程度,我們可以從Linux的源碼中找到答案:

#define __NR__exit __NR_exit /* 摘自檔案include/asm-i386/unistd.h第334行 */
           

"__NR_"是在Linux的源碼中為每個系統調用加上的字首,請注意第一個exit前有2條下劃線,第二個exit前隻有1條下劃線。

這時随便一個懂得C語言并且頭腦清醒的人都會說,_exit和exit沒有任何差別,但我們還要講一下這兩者之間的差別,這種差別主要展現在它們在函數庫中的定義。_exit在Linux函數庫中的原型是:

  1. #include

  2. void _exit(int status)

和exit比較一下,exit()函數定義在stdlib.h中,而_exit()定義在unistd.h中,從名字上看,stdlib.h似乎比 unistd.h進階一點,那麼,它們之間到底有什麼差別呢?

_exit()函數的作用最為簡單:直接使程序停止運作,清除其使用的記憶體空間,并銷毀其在核心中的各種資料結構;exit() 函數則在這些基礎上作了一些包裝,在執行退出之前加了若幹道工序,也是因為這個原因,有些人認為exit已經不能算是純粹的系統調用。

exit()函數與_exit()函數最大的差別就在于exit()函數在調用exit系統調用之前要檢查檔案的打開情況,把檔案緩沖區中的内容寫回檔案,就是"清理I/O緩沖"。 

exit()在結束調用它的程序之前,要進行如下步驟:

1.調用atexit()注冊的函數(出口函數);按ATEXIT注冊時相反的順序調用所有由它注冊的函數,這使得我們可以指定在程式終止時執行自己的清理動作.例如,儲存程式狀态資訊于某個檔案,解開對共享資料庫上的鎖等.

2.cleanup();關閉所有打開的流,這将導緻寫所有被緩沖的輸出,删除用TMPFILE函數建立的所有臨時檔案.

3.最後調用_exit()函數終止程序。

_exit做3件事(man):

1,Any  open file descriptors belonging to the process are closed

2,any children of the process are inherited  by process 1, init

3,the process's parent is sent a SIGCHLD signal

exit執行完清理工作後就調用_exit來終止程序。

此外,另外一種解釋:

簡單的說,exit函數将終止調用程序。在退出程式之前,所有檔案關閉,緩沖輸出内容将重新整理定義,并調用所有已重新整理的“出口函數”(由atexit定義)。

_exit:該函數是由Posix定義的,不會運作exit handler和signal handler,在UNIX系統中不會flush标準I/O流。

簡單的說,_exit終止調用程序,但不關閉檔案,不清除輸出緩存,也不調用出口函數。

共同:

不管程序是如何終止的,核心都會關閉程序打開的所有file descriptors,釋放程序使用的memory!

為何在一個fork的子程序分支中使用_exit函數而不使用exit函數?

‘exit()’與‘_exit()’有不少差別在使用‘fork()’,特别是‘vfork()’時變得很

突出。

‘exit()’與‘_exit()’的基本差別在于前一個調用實施與調用庫裡使用者狀态結構(user-mode constructs)有關的清除工作(clean-up),而且調用使用者自定義的清除程式 (自定義清除程式由atexit函數定義,可定義多次,并以倒序執行),相對應,_exit函數隻為程序實施核心清除工作。

在由‘fork()’建立的子程序分支裡,正常情況下使用‘exit()’是不正确的,這是 因為使用它會導緻标準輸入輸出(stdio: Standard Input Output)的緩沖區被清空兩次,而且臨時檔案被出乎意料的删除(臨時檔案由tmpfile函數建立在系統臨時目錄下,檔案名由系統随機生成)。在C++程式中情況會更糟,因為靜态目标(static objects)的析構函數(destructors)可以被錯誤地執行。

(還有一些特殊情況,比如守護程式,它們的父程序需要調用‘_exit()’而不是子程序;

适用于絕大多數情況的基本規則是,‘exit()’在每一次進入‘main’函數後隻調用一次。)

在由‘vfork()’建立的子程序分支裡,‘exit()’的使用将更加危險,因為它将影響父程序的狀态。

#include        
#include        
int    glob = 6;               /* external variable in initialized data */
int main(void)
{
        int    var;            /* automatic variable on the stack */
        pid_t    pid;
 
        var = 88;
        printf("before vfork\n");       /* we don't flush stdio */
 
        if ( (pid = vfork()) < 0)
                printf("vfork error\n");
        else if (pid == 0) {            /* child */
                glob++;                                 /* modify parent's variables */
                var++;
               exit(0);                               /* child terminates */  //子程序中最好還是用_exit(0)比較安全。
        }
 
        /* parent */
        printf("pid = %d, glob = %d, var = %d\n", getpid(), glob, var);
        exit(0);
}
           

在Linux系統上運作,父程序printf的内容輸出:pid = 29650, glob = 7, var = 89

子程序 關閉的是自己的, 雖然他們共享标準輸入、标準輸出、标準出錯等 “打開的檔案”, 子程序exit時,也不過是遞減一個引用計數,不可能關閉父程序的,是以父程序還是有輸出的。

但在其它UNIX系統上,父程序可能沒有輸出,原因是子程序調用了e x i t,它重新整理關閉了所有标準I / O流,這包括标準輸出。雖然這是由子程序執行的,但卻是在父程序的位址空間中進行的,是以所有受到影響的标準I/O FILE對象都是在父程序中的。當父程序調用p r i n t f時,标準輸出已被關閉了,于是p r i n t f傳回- 1。

在Linux的标準函數庫中,有一套稱作"進階I/O"的函數,我們熟知的printf()、fopen()、fread()、fwrite()都在此 列,它們也被稱作"緩沖I/O(buffered I/O)",其特征是對應每一個打開的檔案,在記憶體中都有一片緩沖區,每次讀檔案時,會多讀出若幹條記錄,這樣下次讀檔案時就可以直接從記憶體的緩沖區中讀取,每次寫檔案的時候,也僅僅是寫入記憶體中的緩沖區,等滿足了一定的條件(達到一定數量,或遇到特定字元,如換行符和檔案結束符EOF),再将緩沖區中的 内容一次性寫入檔案,這樣就大大增加了檔案讀寫的速度,但也為我們程式設計帶來了一點點麻煩。如果有一些資料,我們認為已經寫入了檔案,實際上因為沒有滿足特定的條件,它們還隻是儲存在緩沖區内,這時我們用_exit()函數直接将程序關閉,緩沖區中的資料就會丢失,反之,如果想保證資料的完整性,就一定要使用exit()函數。

Exit的函數聲明在stdlib.h頭檔案中。

_exit的函數聲明在unistd.h頭檔案當中。

下面的執行個體比較了這兩個函數的差別。printf函數就是使用緩沖I/O的方式,該函數在遇到“\n”換行符時自動的從緩沖區中将記錄讀出。執行個體就是利用這個性質進行比較的。

exit.c源碼

#include

#include 

int main(void)

{

    printf("Using exit...\n");

    printf("This is the content in buffer");

    exit(0);

}

輸出資訊:

Using exit...

This is the content in buffer

#include

#include 

int main(void)

{

    printf("Using exit...\n");   //如果此處不加“\n”的話,這條資訊有可能也不會顯示在終端上。

    printf("This is the content in buffer");

    _exit(0);

}

則隻輸出:

Using exit...

說明:在一個程序調用了exit之後,該程序并不會馬上完全消失,而是留下一個稱為僵屍程序(Zombie)的資料結構。僵屍程序是一種非常特殊的程序,它幾乎已經放棄了所有的記憶體空間,沒有任何可執行代碼,也不能被排程,僅僅在程序清單中保留一個位置,記載該程序的退出狀态等資訊供其它程序收集,除此之外,僵屍程序不再占有任何記憶體空間。

#include ;

int main()

{

    printf("%c", 'c');

    _exit(0);

}

程式并沒有輸出"c", 說明_exit()沒有進行io flush

#include <sys/types.h>

#include <sys/wait.h>

pid_t wait(int *status)

程序一旦調用了wait,就立即阻塞自己,由wait自動分析是否目前程序的某個子程序已經退出,如果讓它找到了這樣一個已經變成僵屍的子程序,wait就會收集這個子程序的資訊,并把它徹底銷毀後傳回;如果沒有找到這樣一個子程序,wait就會一直阻塞在這裡,直到有一個出現為止。

參數status用來儲存被收集程序退出時的一些狀态,它是一個指向int類型的指針。但如果我們對這個子程序是如何死掉的毫不在意,隻想把這個僵屍程序消滅掉,我們就可以設定這個參數為NULL

pid = wait(NULL);//一般都是這樣運用的

如果成功,wait會傳回被收集的子程序的程序ID,如果調用程序沒有子程序,調用就會失敗,此時wait傳回-1,同時errno被置為ECHILD。

是以,調用wait和waitpid不僅可以獲得子程序的終止資訊,還可以使父程序阻塞等待子程序終止,起到程序間同步的作用。如果參數status不是空指針,則子程序的終止資訊通過這個參數傳出,如果隻是為了同步而不關心子程序的終止資訊,可以将status參數指定為NULL。

#include <sys/wait.h>

pid_t wait(int *statloc);

pid_t waitpid(pid_t pid,int *statloc, int options);

//statloc指向終止程序的終止狀态,如果不關心終止狀态可指定為空指針

//pid有四種情況:

//1.    pid  ==  -1         等待任意子程序

//2.    pid  >   0           等待程序ID與pid相等的子程序

//3.    pid  ==  0          等待組ID等于調用程序組ID的任意子程序

//4.   pid   <   -1          等待組ID等于pid絕對值的任意子程序

//options控制waitpid的操作:

//1,2是支援作業控制

//1.WCONTINUED                            wcontinued

//2.WUNTRACED                             wuntraced

//3.WNOHANG  waitpid不阻塞          wnohang  

這兩個函數差別如下:

//在子程序終止前,wait使其調用者阻塞,waitpid有一個選項可使調用者不阻塞。

//waitpid并不等待在其調用之後第一個終止的子程序,它有若幹選項。換言之可以不阻塞。

//事實上:

pid_t wait(int *statloc)

{

    return waitpid(-1, statloc, 0);

}

避免僵屍程序

當我們隻fork()一次後,存在父程序和子程序。這時有兩種方法來避免産生僵屍程序:

1.父程序調用waitpid()等函數來接收子程序退出狀态。 

2.父程序先結束,子程序則自動托管到Init程序(pid = 1)。

2、基本概念

  我們知道在unix/linux中,正常情況下,子程序是通過父程序建立的,子程序在建立新的程序。子程序的結束和父程序的運作是一個異步過程,即父程序永遠無法預測子程序 到底什麼時候結束。 當一個 程序完成它的工作終止之後,它的父程序需要調用wait()或者waitpid()系統調用取得子程序的終止狀态。

  孤兒程序:一個父程序退出,而它的一個或多個子程序還在運作,那麼那些子程序将成為孤兒程序。孤兒程序将被init程序(程序号為1)所收養,并由init程序對它們完成狀态收集工作。

  僵屍程序:一個程序使用fork建立子程序,如果子程序退出,而父程序并沒有調用wait或waitpid擷取子程序的狀态資訊,那麼子程序的程序描述符仍然儲存在系統中。這種程序稱之為僵死程序。

3、問題及危害

  unix提供了一種機制可以保證隻要父程序想知道子程序結束時的狀态資訊, 就可以得到。這種機制就是: 在每個程序退出的時候,核心釋放該程序所有的資源,包括打開的檔案,占用的記憶體等。 但是仍然為其保留一定的資訊(包括程序号the process ID,退出狀态the termination status of the process,運作時間the amount of CPU time taken by the process等)。直到父程序通過wait / waitpid來取時才釋放。 但這樣就導緻了問題,如果程序不調用wait / waitpid的話, 那麼保留的那段資訊就不會釋放,其程序号就會一直被占用,但是系統所能使用的程序号是有限的,如果大量的産生僵死程序,将因為沒有可用的程序号而導緻系統不能産生新的程序. 此即為僵屍程序的危害,應當避免。

  孤兒程序是沒有父程序的程序,孤兒程序這個重任就落到了init程序身上,init程序就好像是一個民政局,專門負責處理孤兒程序的善後工作。每當出現一個孤兒程序的時候,核心就把孤 兒程序的父程序設定為init,而init程序會循環地wait()它的已經退出的子程序。這樣,當一個孤兒程序凄涼地結束了其生命周期的時候,init程序就會代表黨和政府出面處理它的一切善後工作。是以孤兒程序并不會有什麼危害。

  任何一個子程序(init除外)在exit()之後,并非馬上就消失掉,而是留下一個稱為僵屍程序(Zombie)的資料結構,等待父程序處理。這是每個 子程序在結束時都要經過的階段。如果子程序在exit()之後,父程序沒有來得及處理,這時用ps指令就能看到子程序的狀态是“Z”。如果父程序能及時 處理,可能用ps指令就來不及看到子程序的僵屍狀态,但這并不等于子程序不經過僵屍狀态。  如果父程序在子程序結束之前退出,則子程序将由init接管。init将會以父程序的身份對僵屍狀态的子程序進行處理。

  僵屍程序危害場景:

  例如有個程序,它定期的産 生一個子程序,這個子程序需要做的事情很少,做完它該做的事情之後就退出了,是以這個子程序的生命周期很短,但是,父程序隻管生成新的子程序,至于子程序 退出之後的事情,則一概不聞不問,這樣,系統運作上一段時間之後,系統中就會存在很多的僵死程序,倘若用ps指令檢視的話,就會看到很多狀态為Z的程序。 嚴格地來說,僵死程序并不是問題的根源,罪魁禍首是産生出大量僵死程序的那個父程序。是以,當我們尋求如何消滅系統中大量的僵死程序時,答案就是把産生大 量僵死程序的那個元兇槍斃掉(也就是通過kill發送SIGTERM或者SIGKILL信号啦)。槍斃了元兇程序之後,它産生的僵死程序就變成了孤兒進 程,這些孤兒程序會被init程序接管,init程序會wait()這些孤兒程序,釋放它們占用的系統程序表中的資源,這樣,這些已經僵死的孤兒程序 就能瞑目而去了。

繼續閱讀