多進程wait、僵屍進程、孤兒進程、prctl


1、概念

1、孤兒進程:一個父進程退出,而它的一個或多個子進程還在運行,那么那些子進程將成為孤兒進程。孤兒進程將被init進程(進程號為1)所收養,從而保證每個進程都會有一個父進程。而Init進程會自動wait其子進程,因此被Init接管的所有進程都不會變成僵屍進程。

      補充:孤兒進程是沒有父進程的進程,孤兒進程這個重任就落到了init進程身上。每當出現一個孤兒進程的時候,內核就把孤兒進程的父進程設置為init,而init進程會循環地wait()它的已經退出的子進程。這樣,當一個孤兒進程結束了其生命周期的時候,init進程就會處理它的一切善后工作。因此孤兒進程並不會有什么危害。

 

2、僵屍進程:一個進程使用fork創建子進程,如果子進程退出,而父進程並沒有調用wait或waitpid獲取子進程的狀態信息,那么子進程的進程描述符仍然保存在系統中,這種進程稱之為僵屍進程。當用ps命令觀察進程的執行狀態時,看到這些進程的狀態欄為defunct。僵屍進程是一個早已死亡的進程,但在進程表(processs table)中仍占了一個位置(slot)。

      補充(內核):一個進程終止后,內核會釋放終止進程(調用了exit系統調用)所使用的所有存儲區,關閉所有打開的文件等。但內核為每一個終止子進程保存了一定量的信息,設置僵死狀態來維護子進程的信息,以便父進程在以后某個時候獲取,這些信息至少包括進程ID,進程的終止狀態,以及該進程使用的CPU時間,所以當終止子進程的父進程調用wait或waitpid時就可以得到這些信息。任何一個子進程(init除外)在exit后並非馬上就消失,而是留下一個稱外僵屍進程的數據結構,等待父進程處理。這是每個子進程都必需經歷的階段。另外子進程退出的時候會向其父進程發送一個SIGCHLD信號。

 

      嚴格來說,僵屍進程並不是問題的根源,罪魁禍首是產生出大量僵屍進程的那個父進程。因此,把產生大量僵屍進程的那個父進程kill掉(通過kill發送SIGTERM或者SIGKILL信號)之后,它產生的僵死進程就變成了孤兒進程,這些孤兒進程會被init進程接管,init進程會wait()這些孤兒進程,釋放它們占用的系統進程表中的資源。

 

2、如何避免僵屍進程?

(1)通過signal(SIGCHLD, SIG_IGN)通知內核對子進程的結束不關心,由內核回收。如果不想讓父進程掛起,可以在父進程中加入一條語句:signal(SIGCHLD,SIG_IGN);表示父進程忽略SIGCHLD信號,該信號是子進程退出的時候向父進程發送的。

(2)父進程調用wait/waitpid等函數等待子進程結束,如果尚無子進程退出wait會導致父進程阻塞waitpid可以通過傳遞WNOHANG使父進程不阻塞立即返回

(3)如果父進程很忙可以用signal注冊信號處理函數,在信號處理函數調用wait/waitpid等待子進程退出。

(4)通過兩次調用fork。父進程首先調用fork創建一個子進程然后waitpid等待子進程退出,子進程再fork一個孫進程后退出。這樣子進程退出后會被父進程等待回收,而對於孫子進程其父進程已經退出所以孫進程成為一個孤兒進程,孤兒進程由init進程接管,孫進程結束后,init會等待回收。

      第一種方法忽略SIGCHLD信號,這常用於並發服務器的一個技巧,因為並發服務器常常fork很多子進程,子進程終結之后需要服務器進程去wait清理資源。如果將此信號的處理方式設為忽略,可讓內核把僵屍子進程轉交給init進程去處理,省去了大量僵屍進程占用系統資源。

 

3、wait()、waitpid()

wait()

函數原型:
#include <sys/types.h>  
#include <sys/wait.h> 
pid_t wait(int *status)
工作原理:

(1)子進程結束時,系統向其父進程發送SIGCHILD信號;

(2)父進程調用wait函數后阻塞;

(3)父進程被SIGCHILD信號喚醒,然后去回收僵屍子進程;

(4)父子進程之間是異步的,SIGCHILD信號機制就是為了解決父子進程之間的異步通信問題,讓父進程可以及時的去回收僵屍子進程。

(5)若父進程沒有任何子進程則wait返回錯誤。

 
作用:
進程一旦調用了wait,就立即阻塞自己,直到有信號到來或者子進程結束。如果有信號到來或者讓它找到了一個已經變成僵屍的子進程,wait就會收集這個子進程的信息,並把它徹底銷毀后返回;如果沒有找到這樣一個子進程,wait就會一直阻塞在這里,直到有一個出現為止;如果在調用wait時子進程已經結束,wait會立即返回。
 
參數:
status用來保存被收集進程退出時的一些狀態,它是一個指向int類型的指針,指出了子進程是正常退出還是被非正常結束,以及正常結束時的返回值,或被哪一個信號結束等信息。但如果對這個子進程是如何死掉的毫不在意,只想把這個僵屍進程消滅掉,就可以設定這個參數為NULL:
pid = wait(NULL); 
 
返回值:如果執行成功則返回子進程識別碼(PID), 如果有錯誤發生則返回-1. 失敗原因存於errno 中。

錯誤代碼:(更多errno含義見cat /usr/include/asm-generic/errno-base.h和/usr/include/asm-generic/errno.h)

(1)ECHILD:沒有等待的子進程;

(2)EINTR:未抓住信號,或該信號未設置,或未找到該信號。

#include <errno.h>
int waitreturn;
waitreturn = wait(&val);
if(waitreturn == -1)
{
        printf("errno:%d\n", errno);
}

 

得到status信息:

由於status信息被存放在一個整數的不同二進制位中,不同平台有不同定義,所以用常規的方法讀取會非常麻煩,人們就設計了一套專門的宏(macro)來完成這項工作,最常用的有:

(1)WIFEXITED(status) 這個宏用來指出子進程是否為正常退出(不是信號導致的退出),如果是,它會返回一個非零值。參數status是wait參數指針指向的整數。

(2)WEXITSTATUS(status) 當WIFEXITED返回非零值時,可以用這個宏來提取子進程的返回值,如果子進程調用exit(5)退出,WEXITSTATUS(status) 就會返回5;如果子進程調用exit(7),WEXITSTATUS(status)就會返回7。請注意,如果進程不是正常退出的,也就是說, WIFEXITED返回0,這個值就毫無意義。

其余:

(1)WIFSTOPPED/WSTOPSIG:當子進程是因為被一個信號暫停而返回時則WIFSTOPPED(status)為真,在這種情況下WSTOPSIG(status)返回這個暫停子進程信號的編號。

(2)WIFCONTINUED:當一個暫停的子進程被信號SIGCONT喚醒而返回狀態,則WIFCONTINUED(status)為真,否則為假。

(3)WIFSIGNALED/WTERMSIG/WCOREDUMP:當程序異常終止時WIFSIGNALED(staus)為真,這種情況下WTERMSIG(status)返回終止進程的信號編號。並且程序異常終止時產生了core文件的話,則WCOREDUMP(status)為真,否則為假。

 

waitpid()

函數原型:

#include <sys/types.h>
#include<sys/wait.h>
pid_t waitpid(pid_t pid, int *status, int options);

作用:

系統調用waitpid是wait的封裝,waitpid只是多出了兩個可由用戶控制的參數pid和options,為編程提供了靈活性。

 

參數:

(1)pid

pid>0時,只等待進程ID等於pid的子進程,不管其它已經有多少子進程運行結束退出了,只要指定的子進程還沒有結束,waitpid就會一直等下去

pid=-1時,等待任何一個子進程退出,沒有任何限制,此時waitpid和wait的作用一模一樣

pid=0時,等待同一個進程組中的任何子進程,如果子進程已經加入了別的進程組,waitpid不會對它做任何理睬

pid<-1時,等待一個指定進程組中的任何子進程,這個進程組的ID等於pid的絕對值

(2)options

如果使用了WNOHANG參數,即使沒有子進程退出,它也會立即返回,不會像wait那樣永遠等下去

如果使用了WUNTRACED參數,則子進程進入暫停則馬上返回,但結束狀態不予以理會

Linux中只支持WNOHANG和WUNTRACED兩個選項,這是兩個常數,可以用"|"運算符把它們連接起來使用,比如:

ret=waitpid(-1,NULL,WNOHANG | WUNTRACED);

如果不想使用它們,也可以把options設為0,如:ret=waitpid(-1,NULL,0);

 

返回值:

(1)當正常返回的時候waitpid返回收集到的子進程的進程ID;

(2)如果設置了選項WNOHANG,而調用中waitpid發現沒有已退出的子進程可收集,則返回0;

(3)如果調用中出錯,則返回-1,這時errno會被設置成相應的值以指示錯誤所在;

(4)當pid所指示的子進程不存在,或此進程存在,但不是調用進程的子進程,waitpid就會出錯返回,這時errno被設置為ECHILD。

 

得到status信息(和上面一樣):

WIFEXITED(st atus):如果子進程正常結束則為非0 值;
WEXITSTATUS(status):取得 子進exit()返回的結束代碼, 一般會先用WIFEXITED 來判斷是否正常結束才能使用此宏;
WIFSIGNALED(status):如果子進程是因為信號而結束則此宏值為真;
WTERMSIG(status):取得子進程因信號而中止的信號代碼, 一般會先用WIFSIGNALED 來判斷后才使用此宏;
WIFSTOPPED(status):如果子進程處於暫停執行情況則此宏值為真. 一般只有使用WUNTRACED時才會有此情況;
WSTOPSIG(status):取得引發子進程暫停的信號代碼, 一般會先用 WIFSTOPPED 來判斷后才使用此宏。
 

4、實驗 

以下實驗了多種情況,用於理解父進程wait多個子進程

#include <stdlib.h>
#include <unistd.h>
#include <sys/types.h>
#include <sys/wait.h>
int main()
{
    pid_t pid;
    if(fork() == 0)
    {
        printf("1--This is the child process. pid =%d\n", getpid());
        sleep(3);
        printf("%d exit\n", getpid());
        exit(0);
    }
    if(fork() == 0)
    {
        printf("2--This is the child process. pid =%d\n", getpid());
        sleep(10);
        printf("%d exit\n", getpid());
        exit(0);
    }
    if(fork() == 0)
    {
        printf("3--This is the child process. pid =%d\n", getpid());
        sleep(17);
        printf("%d exit\n", getpid());
        exit(0);
    }
    // while(1)
    // {
    //     pid = wait(NULL);
    //     printf("parent:%d,return of wait:%d\n", getpid(), pid);
    //     if(pid == -1)
    //     {
    //         break;
    //     }
    // }
    //循環wait每一個子進程輸出,最后返回值是-1表示沒有子進程了,該父進程也退出


    //while(wait(NULL) != -1);
    //wait最后一個子進程退出后,該父進程退出


    //printf("parent:%d,return of wait:%d\n", getpid(), wait(NULL));
    //wait第一個返回的進程后,該父進程就返回,其他未返回進程變孤兒進程,由init進程接管
 

    // while(wait(NULL) != -1)
    // {
    //     printf("parent:%d,return of wait:%d\n", getpid(), wait(NULL));
    // }
    //while里wait第一個退出的子進程,printf里wait第二個子進程輸出,while再wait第三個子進程,最后printf里是-1,表明已經沒有子進程,父進程退出

    signal(SIGCHLD,SIG_IGN);//忽略子進程exit,內核直接轉交init處理defunct,這樣就算父進程沒退出也不會有僵屍進程
    sleep(20);//父進程沒有wait任何子進程,沒有設置SIG_IGN,且還在運行中時子進程就退出了,此時所有子進程變僵屍進程,直到父進程退出后,子進程被init進程接管處理
    printf("exit\n");
    return 0;
}

 

總結:

調用一個wait,則第一個子進程返回后該父進程也返回,那其他子進程還在運行,為何沒有變成僵屍進程,而是直接由init接管

-----父進程返回了,所以其他子進程變孤兒進程,直接由init接管,所以ps –ef | grep defunct沒有僵屍進程

 

如果父進程fork多個子進程,子進程退出后,該父進程還在運行中,但沒wait任何子進程,則此時的子進程會變僵屍進程,直到父進程結束,然后由init進程接管

-----父進程不wait阻塞,而且在子進程結束后還在運行。此時為了不產生僵屍進程,可以在父進程中設置signal(SIGCHLD,SIG_IGN); 使得父進程忽略子進程退出,把子進程直接交由init接管

 

5、父進程退出(正常/異常退出)讓子進程也退出

C語言。直接在子進程中加:prctl(PR_SET_PDEATHSIG, SIGHUP);

python。在子進程中:

import signal
import prctl
prctl.set_pdeathsig(signal.SIGHUP)

需要下載對應python-prctl:

1、ubuntu:
apt-get install build-essential libcap-dev
pip install python-prctl
2、centos: yum install gcc glibc
-devel libcap-devel easy_install python-prctl

在 Linux 中,進程可以要求內核在父進程退出的時候給自己發信號。如上在子進程中使用prctl系統調用,父進程掛了后,子進程就會收到SIGHUP信號,系統對SIGHUP信號的默認處理是終止收到該信號的進程。所以若程序中沒有捕捉該信號,當收到該信號時,進程就會退出。

(1)關於linux信號

各種信號的說明參見:signal

linux下:
ctrl-c 發送 SIGINT 信號給前台進程組中的所有進程。常用於終止正在運行的程序。
ctrl-z 發送 SIGTSTP 信號給前台進程組中的所有進程。常用於掛起一個進程,暫停執行,放入后台,可通過jobs顯示當前暫停的進程,使用fg讓最后一個進程在前台運行(fg %N使第N個到前台,bg %N到后台)。
ctrl-d 不是發送信號,而是表示一個特殊的二進制值,表示 EOF。退出當前shell,相當於exit命令。
ctrl-\ 發送 SIGQUIT 信號給前台進程組中的所有進程,終止前台進程並生成 core 文件。

Key Function
Ctrl-c Kill foreground process
Ctrl-z Suspend foreground process
Ctrl-d Terminate input, or exit shell
Ctrl-s Suspend output
Ctrl-q Resume output
Ctrl-o Discard output
Ctrl-l Clear screen

 

(2)關於prctl

a.使用:

int prctl ( int option,unsigned long arg2,unsigned long arg3,unsigned long arg4,unsigned long arg5 )

option可選(各種options含義):

PR_SET_PDEATHSIG :arg2作為處理器信號pdeath被輸入,正如其名,如果父進程不能再用,進程接受這個信號。

PR_SET_NAME :把參數arg2作為調用進程的進程名字。(SinceLinux 2.6.11)(此方法改變了task_struct的comm。現象是:/proc/$pid/status和pstree -p都變為新名字,/proc/$pid/cmdline和ps -aux |grep $pid沒變)(其他修改進程名的方法

b.內核源碼:

 

SYSCALL_DEFINE5(prctl, int, option, unsigned long, arg2, unsigned long, arg3,
        unsigned long, arg4, unsigned long, arg5)
{
    struct task_struct *me = current;
    unsigned char comm[sizeof(me->comm)];
    long error;

    error = security_task_prctl(option, arg2, arg3, arg4, arg5);
    if (error != -ENOSYS)
        return error;

    error = 0;
    switch (option) {
    case PR_SET_PDEATHSIG:
        if (!valid_signal(arg2)) {
            error = -EINVAL;
            break;
        }
        me->pdeath_signal = arg2;//父進程dies,傳遞給子進程的signal
        break;
   ...
        case PR_SET_NAME:
        comm[sizeof(me->comm) - 1] = 0;
        if (strncpy_from_user(comm, (char __user *)arg2,
                      sizeof(me->comm) - 1) < 0)
            return -EFAULT;
        set_task_comm(me, comm);//修改task_struct->comm
        proc_comm_connector(me);
        break;    
   ...
}

 

概念參考:

https://www.cnblogs.com/wuchanming/p/4020463.html

https://www.cnblogs.com/Anker/p/3271773.html 

https://blog.csdn.net/astrotycoon/article/details/41172389

http://blog.51cto.com/no001/493589

https://www.jianshu.com/p/e0c6749dbcdc


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM