進程控制3——進程終止和等待

一個進程不可能永遠的存在,必然存在着結束,我們需要進行一些操作來終止這些進程。而父進程創建子進程後,如何知道子進程什麼時候終止?如何知道子進程怎麼終止?所以又帶來了進程等待的問題。

下面先將一些關於進程終止 的函數,
exit,_exit都是用於終止進程的函數
區別:
_exit: 直接使進程停止,清除其使用的內存,並清除緩衝區中內容
exit:在停止進程之前,要檢查文件的打開情況,並把文件緩衝區中的內容寫回文件才停止進程。

exit()

表頭文件: #include  <stdlib.h>
定義函數: void exit(int status);

函數說明:exit()用來正常終結目前進程的執行,並把參數status返回給父進程,而進程所有的緩衝區數據會自動寫回並關閉未關閉的文件。
_exit()

表頭文件: #include<unistd.h>
定義函數: void _exit(int status);

函數說明
此函數調用後不會返回,並且會傳遞SIGCHLD信號給父進程,父進程可以由wait函數取得子進程結束狀態。

這兩種退出方式沒有太多好說的,自己簡單試一試就可以發現一些區別了。

子進程比父進程先退出:殭屍進程
殭屍進程指的是那些雖然已經終止的進程,但仍然保留一些信息,等待其父進程爲其收屍。

殭屍進程產生的過程:
1.父進程調用fork創建子進程後,子進程運行直至其終止,它立即從內存中移除,但進程描述符仍然保留在內存中(進程描述符佔有極少的內存空間)。

2.子進程的狀態變成EXIT_ZOMBIE,並且向父進程發送SIGCHLD 信號,父進程此時應該調用 wait() 系統調用來獲取子進程的退出狀態以及其它的信息。在 wait 調用之後,殭屍進程就完全從內存中移除。

3.因此一個殭屍存在於其終止到父進程調用 wait 等函數這個時間的間隙,一般很快就消失,但如果編程不合理,父進程從不調用 wait 等系統調用來收集殭屍進程,那麼這些進程會一直存在內存中。

pid_t pid = fork(); 
switch (pid)
{
    case -1:
        perror ("fork");
        break;
    case 0: // 子進程
        printf ("我是子進程,我的Id 是%d\n", getpid());
        exit(0);
    default: // 父進程
        printf ("我是父進程,Id = %d\n", getpid());
        while (1);
        break;
}

這裏寫圖片描述
我們繼續用那條指令抓一下進程,會發現在子進程先結束後留下了,它的ID號還在,後面加上了一個<defunct>,這個單詞的意思是死者,很明顯,這個進程就是我們所說的殭屍進程了。如果不進行一定處理的話,那麼保留的那段信息就不會釋放,其進程號就會一直被佔用,但是系統所能使用的進程號是有限的,如果大量的產生僵死進程,將因爲沒有可用的進程號而導致系統不能產生新的進程. 此即爲殭屍進程的危害,應當避免。

子進程先於父進程結束,如果不做適當處理會產生殭屍進程,那父進程先於子進程結束呢?
若父進程比子進程先終止,則該父進程的所有子進程的父進程都改變爲init進程。我們稱這些進程由init進程領養。其執行順序大致如下:在一個進程終止時,內核逐個檢查所有活動進程,以判斷它是否是正要終止的進程的子進程,如果是,則該進程的父進程ID就更改爲1(init進程的ID); 有init領養的進程不會稱爲僵死進程,因爲只要init的子進程終止,init就會調用一個wait函數取得其終止狀態。這樣也就防止了在系統中有很多僵死進程。
下面是一個父進程提前結束的案例:

pid_t pid = fork();

    switch (pid)
    {
        case -1:
            perror ("fork");
            break;
        case 0: // 子進程
            printf ("我是子進程,我的Id 是%d\n", getpid());
            while (1)
            break;
        default: // 父進程
            printf ("我是父進程,Id = %d\n", getpid());
            printf ("我走啦\n");
        //  while (1);
            break;
    }

這裏寫圖片描述
從圖中不難看出,父進程已經結束了,而子進程依舊存在,但此時接手它的已經成爲了1,1是誰?沒錯1就是init進程,我們通常所說的祖父進程,從此這個子進程不再受出了1之外的任何人控制了。
很明顯父進程先於子進程結束,產生的危害就小多了,甚至還給我們一個思路,一個讓程序運行在後臺的思路,就讓我們產生了一個關於守護進程的想法。
守護進程(daemon)是一類在後臺運行的特殊進程,用於執行特定的系統任務。很多守護進程在系統引導的時候啓動,並且一直運行直到系統關閉。另一些只在需要的時候才啓動,完成任務後就自動結束。
下面是一個創建守護進程的思路:

int daemonize(int nochdir, int noclose)
{
    // 1、創建子進程,關閉父進程
    pid_t pid = fork();
    if (pid > 0)
    {
        exit(0);
    }
    else if (pid < 0)
    {
        return -1;
    }

    // 2、設置文件的掩碼, mode & ~umask
    umask(0);

    // 3、設置新的會話: 脫離當前會話和終端的控制
    if (setsid() < 0)
    {
        return -1;
    }

    if (nochdir == 0 )
    {
        // 4、改變當前的工作目錄
        if (chdir("/") < 0)
        {
            return -1;
    }
    }

    // 標準輸入、關閉標準輸出、標準錯誤
    close(STDIN_FILENO);
    close(STDOUT_FILENO);
    close(STDERR_FILENO);

    if (noclose == 0)
    {
        // 重定向標準輸入、關閉標準輸出、標準錯誤
        open("/dev/null", O_RDONLY);   // 0 
        open("/dev/null", O_RDWR);   // 1
        open("/dev/null", O_RDWR);   // 2
    }

    return 0;
}

創建一個守護進程需要經歷這麼幾步,創建子進程並退出父進程,設置文件掩碼,設置新會話,改變工作目錄,關閉並重定向標準輸入,標準輸出和標準錯誤。雖然有這樣一個函數daemon()可以幫助我們完成這一些,但一些原理我們最好還是需要知道的。

上文我們提到了殭屍進程,知道了它的危害性,既然危害這麼大,我們是不是該採取一些措施呢?讓他的父進程做一些處理呢?下面我們就來講如何處理殭屍進程。
我們先講兩個函數wait()和waitpid()

#include <sys/wait.h>
pid_t wait(int *status);

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

調用wait或waitpid的進程可能發生的情況有:
如果所有子進程都還在運行,則阻塞(Block)。
如果一個子進程已終止,正等待父進程獲取其終止狀態,則取得該子進程的終止狀態立即返回。
如果它沒有任何子進程,則立即出錯返回。
在一個子進程終止前,wait使其調用者阻塞,而waitpid有一個選項,可使調用者不阻塞。
waitpid並不等待在其調用之後的第一個終止的子進程。它有若干個選項,可以控制它所等待的進程。
如果一個子進程已經終止,並且是一個殭屍進程,wait立即返回並取得該子進程的狀態,否則wait使其調用者阻塞直到一個子進程終止。如果調用者阻塞並且它有多個子進程,則在其一個子進程終止時,wait就立即返回。因爲wait返回終止子進程的ID,所以總能瞭解到是哪一個子進程終止了。
現在我們就使用wait()來處理之前的殭屍進程,

pid_t pid = fork();
switch(pid)
{
    case -1:
        perror("fork");
        break;
    case 0:
        printf("子進程,ID是%d\n",getpid());
        sleep(5);
        exit(0);
        break;
    default:
        printf("等待子進程結束\n");
        sleep(20);
        pid_t childId = wait(NULL);

        printf("成功處理掉一個子進程,該子進程是%d\n",childId);
        sleep(5);
        break;
}

這裏寫圖片描述
在程序執行5s後,子進程離開了,這時候產生了一個殭屍進程8623
這裏寫圖片描述
20s之後我們在終端發現提示8623已經被處理了,
這裏寫圖片描述
再抓取一下進程發現子進程已經不見了
除了解決殭屍進程的問題,我們還可以通過幾個宏來了解一下終止的狀態:
WIFEXITED(status)
若子進程正常終止,該宏返回true。
此時,可以通過WEXITSTATUS(status)獲取子進程的退出狀態(exit status)。
WIFSIGNALED(status)
若子進程由信號殺死,該宏返回true。
此時,可以通過WTERMSIG(status)獲取使子進程終止的信號值。
WIFSTOPPED(status)
若子進程被信號暫停(stopped),該宏返回true。
此時,可以通過WSTOPSIG(status)獲取使子進程暫停的信號值。
WIFCONTINUED(status)
若子進程通過SIGCONT恢復,該宏返回true。
有興趣可以測試一下。

waitpid()
pid_t waitpid (pid_t pid, int * status, int options)
功能:會暫時停止目前進程的執行,直到有信號來到或子進程結束
參數:如果不在意結束狀態值,則參數status可以設成NULL。
參數pid爲欲等待的子進程識別碼:
pid<-1 等待進程組識別碼爲pid絕對值的任何子進程。
pid=-1 等待任何子進程,相當於wait()。
pid=0 等待進程組識別碼與目前進程相同的任何子進程。
pid>0 等待任何子進程識別碼爲pid的子進程。
參數option可以爲0 或下面的OR 組合
WNOHANG: 如果沒有任何已經結束的子進程則馬上返回,不予以等待。
WUNTRACED :如果子進程進入暫停執行情況則馬上返回,但結束狀態不予以理會。
返回值:如果執行成功則返回子進程識別碼(PID),如果有錯誤發生則返回-1。失敗原因存於errno中。
waitpid()擁有wait的一部分功能,同時能根據ID號處理殭屍進程,有興趣的可以研究一下,在這就不多說了。

發佈了53 篇原創文章 · 獲贊 11 · 訪問量 1萬+
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章