1. 程式人生 > >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()這些孤兒程序,釋放它們佔用的系統程序表中的資源,這樣,這些已經僵死的孤兒程序 就能瞑目而去了。