多进程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(status):如果子进程正常结束则为非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

posted @ 2018-05-20 22:01  前进的code  阅读(1478)  评论(0编辑  收藏  举报