守护进程,僵尸进程和孤儿进程

&nbsp

孤儿进程与僵尸进程:

       在linux当中,子进程是由父进程创建的,子进程再创建新的进程。子进程的结束和父进程的运行是一个异步的过程,即父进程永远无法预测子进程到底什么时候结束。unix系统提供了一种机制可以让父进程获得子进程结束时的状态信息,也就是说在每个进程退出的时候,内核释放该进程所有的资源,包括打开的文件,占用的内存,但是仍然为其保留一定的信息,如进程号,退出状态,运行时间等。那么,当一个子进程完成它的工作终止之后,它的父进程就要调用wait()或者waitpid()系统调用来取得子进程的终止状态。

       孤儿进程
       一个父进程退出,而它的一个或多个子进程还在运行,那么这些子进程将成为孤儿进程。孤儿进程将被init进程(进程号为1)所收养,并由init进程完成对它们状态收集工作。

       僵尸进程

       一个进程使用 fork创建子进程,如果子进程退出,而父进程没有调用wait或waitpid函数获取子进程的状态信息,那么子进程的进程描述符仍然会保存在系统中。这种进程称为僵尸进程。

危害:

       僵尸进程
       在子进程退出时,如果父进程不调用wait或waitpid函数的话,那么系统保留的子进程的信息就不会被释放,其子进程号会一直被占用,但是系统所能使用的进程号是有限的,如果产生大量的僵尸进程,最终可能导致系统没有可用的进程号,从而不能产生新的进程。例如有个进程,它定期的产 生一个子进程,这个子进程需要做的事情很少,做完它该做的事情之后就退出了,因此这个子进程的生命周期很短,但是,父进程只管生成新的子进程,至于子进程 退出之后的事情,则一概不闻不问,这样,系统运行上一段时间之后,系统中就会存在很多的僵死进程,倘若用ps命令查看的话,就会看到很多状态为Z(defunct)的进程。 严格地来说,僵死进程并不是问题的根源,罪魁祸首是产生出大量僵死进程的那个父进程。因此,当我们寻求如何消灭系统中大量的僵死进程时,答案就是把产生大 量僵死进程的那个元凶枪毙掉(也就是通过kill发送SIGTERM或者SIGKILL信号啦)。枪毙了元凶进程之后,它产生的僵死进程就变成了孤儿进 程,这些孤儿进程会被init进程接管,init进程会wait()这些孤儿进程,释放它们占用的系统进程表中的资源,这样,这些已经僵死的孤儿进程 就能瞑目而去了。

       孤儿进程
       孤儿进程因为在其父进程退出时被init进程所收养,所以init进程会wait()孤儿进程,所有孤儿进程并没有什么危害。

僵尸进程的解决方法:

  • 通过父进程捕获SIGCHLD信号,在信号处理函数调用wait()函数或者waitpid()函数处理僵尸进程。

代码如下:

#include <stdio.h>
#include <unistd.h>
#include <errno.h>
#include <stdlib.h>
#include <signal.h>

static void sig_child(int signo);

int main()
{
    pid_t pid;
    //创建捕捉子进程退出信号
    signal(SIGCHLD,sig_child);
    pid = fork();
    if (pid < 0)
    {
        perror("fork error:");
        exit(1);
    }
    else if (pid == 0)
    {
        printf("I am child process,pid id %d.I am exiting.\n",getpid());
        exit(0);
    }
    printf("I am father process.I will sleep two seconds\n");
    //等待子进程先退出
    sleep(2);
    //输出进程信息
    system("ps -o pid,ppid,state,tty,command");
    printf("father process is exiting.\n");
    return 0;
}

static void sig_child(int signo)
{
     pid_t        pid;
     int        stat;
     //处理僵尸进程
     while ((pid = waitpid(-1, &stat, WNOHANG)) >0)
            printf("child %d terminated.\n", pid);
}
  • fork()两次。将子进程变成孤儿进程,从而其父进程变成init进程,通过init进程处理僵尸进程。

代码如下:

#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <errno.h>

int main()
{
    pid_t  pid;
    //创建第一个子进程
    pid = fork();
    if (pid < 0)
    {
        perror("fork error:");
        exit(1);
    }
    //第一个子进程
    else if (pid == 0)
    {
        //子进程再创建子进程
        printf("I am the first child process.pid:%d\tppid:%d\n",getpid(),getppid());
        pid = fork();
        if (pid < 0)
        {
            perror("fork error:");
            exit(1);
        }
        //第一个子进程退出
        else if (pid >0)
        {
            printf("first procee is exited.\n");//孙子进程称为孤儿进程
            exit(0);
        }
        //第二个子进程
        //睡眠3s保证第一个子进程退出,这样第二个子进程的父亲就是init进程里
        sleep(3);
        printf("I am the second child process.pid: %d\tppid:%d\n",getpid(),getppid());
        exit(0);
    }
    //父进程处理第一个子进程退出
    if (waitpid(pid, NULL, 0) != pid)
    {
        perror("waitepid error:");
        exit(1);
    }
    exit(0);
    return 0;
}
守护进程:

       守护进程(daemon),是一种运行在后台 的特殊进程,它独立与控制终端 ,并 周期性地执行某项任务或等待处理某些发生的事件。 。守护进程可以由一个普通的进程按照守护进程的特性改造而来。基本的流程如下:

在这里插入图片描述

1)首先应该屏蔽一些有关控制终端操作的信息,防止在守护进程还没有正常运行起来前受到控制终端的干扰退出或挂起。
代码如下:

#ifdef SIGTTOU
    signal(SIGTTOU, SIG_IGN);//忽略后台进程写控制终端信号
#endif
#ifdef SIGTTIN
    signal(SIGTTIN, SIG_IGN);//忽略后台进程读控制终端信号
#endif
#ifdef SIGTSTP
    signal(SIGTSTP, SIG_IGN);//忽略终端挂起
#endif

2) 前台转后台:首先在普通进程中调用fork函数之后,使父进程终止,让子进程继续执行,此时子进程称为孤儿进程,由于子进程是父进程的完全copy,而子进程又在后台运行,完成“脱壳”。

if (0 > (pid = fork())) exit(-1);

3)脱离控制终端,登录会话和进程组。如果想要普通进程脱离控制终端,登录会话和进程组,不受他们影响,可以使用setsid()设置新会话的首进程,使其与原来的登录会话和进程组自动分离。
代码如下:

if (-1 == setsid()) exit(0);

setsid()函数说明如下:

#include <sys/types.h>
#include <unistd.h>

pid_t setsid(void);

返回值:成功返回进程组ID,出错返回-1;

函数说明:如果,调用setsid的进程不是一个进程组的组长,此函数创建一个新的会话期。结果为

  • 此进程变成新会话的会话首进程,此时,此进程是该新会话组中的唯一进程。

  • 此进程变成一个新进程组的组长进程。新进程组ID是该调用进程的进程ID

  • 此进程没有控制终端,如果在调用setsid前,该进程有控制终端,那么与该终端的联系被解除。

4)经过前面的是3个步骤,该子进程已经脱离了控制终端,原来的登录会话和进程,似乎已经完成从普通进程到守护进程的转换,但是,对于某些系统,如SVR4,当会话首进程打开一个尚未与任何会话相关联的终端设备,此设备会自动作为控制终端分配给该会话。所以,我们需要采用不再让该子进程称为会话首进程的方式来禁止进程重新打开关联的控制终端。具体方法为再次调用fork函数,该fork函数执行后,子进程结束,那么孙子进程就不再是会话首进程,避免会话再次被关联到控制终端。当需要注意的是,当会话首进程退出的时候可能会向其所在的会话的所有进程发送SIGHUP信号,而SIGHUP信号的默认处理函数是结束进程,为了防止孙子进程意外结束,所以要忽略SIGHUP信号。

signal(SIGHUP,SIG_IGN);
if (0 != fork()) exit(0);
  1. 最后一步就是改变工作目录到根目录。因为进程活动时,其工作目录所在的文件系统不能被卸下,一般需要将工作目录改变到根目录。
if (0 != chdir("/")) exit(0);

6)重设文件创建掩模 (一些进程不用)
进程从创建它的父进程那里继承了文件创建掩模。它可能修改守护进程所创建的文件的存取位。为防止这一点,将文件创建掩模清除:umask(0);

测试代码如下:

#include <unistd.h>   
#include <signal.h>   
#include <fcntl.h>  
#include <sys/syslog.h>  
#include <sys/param.h>   
#include <sys/types.h>   
#include <sys/stat.h>   
#include <stdio.h>  
#include <stdlib.h>  
#include <time.h>  

int init_daemon(void)  
{   
    int pid;   
    int i;  

    // 1)屏蔽一些控制终端操作的信号  
#ifdef SIGTTOU
    signal(SIGTTOU, SIG_IGN);
#endif
#ifdef SIGTTIN
    signal(SIGTTIN, SIG_IGN);
#endif
#ifdef SIGTSTP
    signal(SIGTSTP, SIG_IGN);
#endif

    // 2)在后台运行  
    if( (pid=fork())<0 ) exit(0); 
    if(pid>0){ 
        exit(0);  
    }  

    // 3)脱离控制终端、登录会话和进程组  
    if (-1 == setsid()) exit(0);  


    // 4)禁止进程重新打开控制终端  
    signal(SIGHUP, SIG_IGN);
    if( pid=fork() ){ // 父进程  
        exit(0);      // 结束第一子进程,第二子进程继续(第二子进程不再是会话组长)   
    }else if(pid< 0){ // 出错  
        perror("fork");  
        exit(EXIT_FAILURE);  
    }    

    // 5)改变当前工作目录  
    if (0 != chdir("/")) exit(0);   

    // 6)重设文件创建掩模  
    umask(0);    

    return 0;   
}   

int main(int argc, char *argv[])   
{  
    init_daemon();  

    while(1);  

    return 0;  
}  

结果运行:

在这里插入图片描述

发布了97 篇原创文章 · 获赞 222 · 访问量 4万+

猜你喜欢

转载自blog.csdn.net/qq_32642107/article/details/103056121