Linux 多任务编程——特殊进程:僵尸进程、孤儿进程、守护进程

僵尸进程(Zombie Process)
进程已运行结束,但进程的占用的资源未被回收,这样的进程称为僵尸进程。

在每个进程退出的时候,内核释放该进程所有的资源、包括打开的文件、占用的内存等。 但是仍然为其保留一定的信息,这些信息主要主要指进程控制块的信息(包括进程号、退出状态、运行时间等)。直到父进程通过 wait() 或 waitpid() 来获取其状态并释放(具体用法,请看《等待进程结束》)。 这样就会导致一个问题,如果进程不调用wait() 或 waitpid() 的话, 那么保留的那段信息就不会释放,其进程号就会一直被占用,但是系统所能使用的进程号是有限的,如果大量的产生僵死进程,将因为没有可用的进程号而导致系统不能产生新的进程.此即为僵尸进程的危害,应当避免。

子进程已运行结束,父进程未调用 wait() 或 waitpid() 函数回收子进程的资源是子进程变为僵尸进程的原因。

僵尸进程测试程序如下:


#include <stdio.h>
#include <unistd.h>
#include <errno.h>
#include <stdlib.h>
 
int main(int argc, char *argv[])
{
    pid_t pid;
    pid = fork();    //创建进程
    
    if( pid < 0 ){ // 出错
        perror("fork error:");
        exit(1);
    }else if( 0 == pid ){ // 子进程
    
        printf("I am child process.I am exiting.\n");
        printf("[son id]: %d\n", getpid() );
        
        exit(0);
    }else if( pid > 0){ // 父进程
        // 父进程没有调用 wati() 或 watipid() 
        sleep(1); // 保证子进程先运行
        printf("I am father process.I will sleep two seconds\n");
        printf("[father id]: %d\n", getpid() );
        
        while(1); // 不让父进程退出
    }
    
    return 0;
}

我们在一个终端运行以上程序:


在终端敲:ps -ef | grep defunct ,后面尖括号里是 defunct 的都是僵尸进程。

我们另启一个终端,查看进程的状态,有哪些是僵尸进程:

或者:

如何避免僵尸进程?
1)最简单的方法,父进程通过 wait() 和 waitpid() 等函数等待子进程结束,但是,这会导致父进程挂起。具体用法,请看《进程的控制:结束进程、等待进程结束》

2)如果父进程要处理的事情很多,不能够挂起,通过 signal()  函数人为处理信号 SIGCHLD , 只要有子进程退出自动调用指定好的回调函数,因为子进程结束后, 父进程会收到该信号 SIGCHLD ,可以在其回调函数里调用 wait() 或 waitpid() 回收。关于信号的更详细用法,请看《信号中断处理》。

测试代码如下:


#include <stdio.h>
#include <unistd.h>
#include <errno.h>
#include <stdlib.h>
#include <signal.h>
 
void sig_child(int signo)
{
    pid_t  pid;  
     
    //处理僵尸进程, -1 代表等待任意一个子进程, WNOHANG代表不阻塞
    while( (pid = waitpid(-1, NULL, WNOHANG)) > 0 ){
        printf("child %d terminated.\n", pid);
    }
}
 
int main()
{
    pid_t pid;
    
    // 创建捕捉子进程退出信号
    // 只要子进程退出,触发SIGCHLD,自动调用sig_child()
    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);
        
    }else if(pid > 0){ // 父进程
        sleep(2);    // 保证子进程先运行
        printf("I am father, i am exited\n\n");
        system("ps -ef | grep defunct"); // 查看有没有僵尸进程
    
    }
    
    return 0;
}

运行结果如下:

3)如果父进程不关心子进程什么时候结束,那么可以用signal(SIGCHLD, SIG_IGN)通知内核,自己对子进程的结束不感兴趣,父进程忽略此信号,那么子进程结束后,内核会回收, 并不再给父进程发送信号。关于信号的更详细用法,请看《信号中断处理》。

测试代码如下:


#include <stdio.h>
#include <unistd.h>
#include <errno.h>
#include <stdlib.h>
#include <signal.h>
 
int main()
{
    pid_t pid;
    
    // 忽略子进程退出信号的信号
    // 那么子进程结束后,内核会回收, 并不再给父进程发送信号
    signal(SIGCHLD, SIG_IGN);
    
    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);
        
    }else if(pid > 0){ // 父进程
        sleep(2);    // 保证子进程先运行
        printf("I am father, i am exited\n\n");
        system("ps -ef | grep defunct"); // 查看有没有僵尸进程
    
    }
    
    return 0;
}


运行结果如下:

4)还有一些技巧,就是 fork()  两次,父进程 fork() 一个子进程,然后继续工作,子进程 fork() 一 个孙进程后退出,那么孙进程被 init 接管,孙进程结束后,init (1 号进程)会回收。不过子进程的回收还要自己做。《UNIX环境高级编程》8.6节说的非常详细。原理是将子进程成为孤儿进程,从而其的父进程变为 init 进程(1 号进程),通过 init 进程(1 号进程)可以处理僵尸进程。更多详情,请看《特殊进程之孤儿进程》。

测试程序如下所示:


#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){ // 子进程
            //睡眠3s保证下面的父进程退出,这样当前子进程的父亲就是 init 进程
            sleep(3);
            printf("I am the second child process.pid: %d\tppid:%d\n",getpid(),getppid());
            exit(0);
            
        }else if (pid >0){ //父进程退出
            printf("first procee is exited.\n");
            exit(0);
        }
        
    }else if(pid > 0){ // 父进程
    
        // 父进程处理第一个子进程退出,回收其资源
        if (waitpid(pid, NULL, 0) != pid){
            perror("waitepid error:");
            exit(1);
        }
        
        exit(0);
    }
    
    return 0;
}

运行结果如下:

孤儿进程(Orphan Process)

父进程运行结束,但子进程还在运行(未运行结束)的子进程就称为孤儿进程(Orphan Process)。孤儿进程最终会被 init 进程(进程号为 1 )所收养,并由 init 进程对它们完成状态收集工作。

孤儿进程是没有父进程的进程,为避免孤儿进程退出时无法释放所占用的资源而变为僵尸进程,进程号为 1 的 init 进程将会接受这些孤儿进程,这一过程也被称为“收养”。init 进程就好像是一个孤儿院,专门负责处理孤儿进程的善后工作。每当出现一个孤儿进程的时候,内核就把孤 儿进程的父进程设置为 init ,而 init 进程会循环地 wait() 它的已经退出的子进程。这样,当一个孤儿进程凄凉地结束了其生命周期的时候,init 进程就会代表党和政府出面处理它的一切善后工作。因此孤儿进程并不会有什么危害。

注意:如果是64位系统,孤儿进程的父进程号并不是 1 号。

孤儿进程的测试例子:

 
  1. #include <stdio.h>

  2. #include <stdlib.h>

  3. #include <unistd.h>

  4. #include <errno.h>

  5.  
  6. int main()

  7. {

  8. pid_t pid;

  9. //创建进程

  10. pid = fork();

  11.  
  12. if (pid < 0){ // 出错

  13. perror("fork error:");

  14. exit(1);

  15. }else if (pid == 0){//子进程

  16. sleep(2); // 保证父进程先结束

  17.  
  18. printf("son proess: [son id] = %d, [son's father id] = %d\n", getpid(), getppid());

  19.  
  20. exit(0);

  21.  
  22. }else if(pid > 0){ // 父进程

  23. printf("father process, i am exited\n");

  24.  
  25. exit(0);

  26. }

  27.  
  28. return 0;

  29. }


运行结果如下:

守护进程(Daemon Process)
守护进程(Daemon Process),也就是通常说的 Daemon 进程(精灵进程),是 Linux 中的后台服务进程。它是一个生存期较长的进程,通常独立于控制终端并且周期性地执行某种任务或等待处理某些发生的事件。

守护进程是个特殊的孤儿进程,这种进程脱离终端,为什么要脱离终端呢?之所以脱离于终端是为了避免进程被任何终端所产生的信息所打断,其在执行过程中的信息也不在任何终端上显示。由于在 Linux 中,每一个系统与用户进行交流的界面称为终端,每一个从此终端开始运行的进程都会依附于这个终端,这个终端就称为这些进程的控制终端,当控制终端被关闭时,相应的进程都会自动关闭。

Linux 的大多数服务器就是用守护进程实现的。比如,Internet 服务器 inetd,Web 服务器 httpd 等。

如何查看守护进程
在终端敲:ps axj

a 表示不仅列当前用户的进程,也列出所有其他用户的进程
x 表示不仅列有控制终端的进程,也列出所有无控制终端的进程
j 表示列出与作业控制相关的信息

从上图可以看出守护进行的一些特点:

守护进程基本上都是以超级用户启动( UID 为 0 )
没有控制终端( TTY 为 ?)
终端进程组 ID 为 -1 ( TPGID 表示终端进程组 ID)


一般情况下,守护进程可以通过以下方式启动:

在系统启动时由启动脚本启动,这些启动脚本通常放在 /etc/rc.d 目录下;
利用 inetd 超级服务器启动,如 telnet 等;
由 cron 定时启动以及在终端用 nohup 启动的进程也是守护进程。

如何编写守护进程?
下面是编写守护进程的基本过程:
1)屏蔽一些控制终端操作的信号

这是为了防止守护进行在没有运行起来前,控制终端受到干扰退出或挂起。关于信号的更详细用法,请看《信号中断处理》。
    signal(SIGTTOU,SIG_IGN); 
    signal(SIGTTIN,SIG_IGN); 
    signal(SIGTSTP,SIG_IGN); 
    signal(SIGHUP ,SIG_IGN);

2)在后台运行

这是为避免挂起控制终端将守护进程放入后台执行。方法是在进程中调用 fork() 使父进程终止, 让守护进行在子进程中后台执行。 
    if( pid = fork() ){ // 父进程
        exit(0);         //结束父进程,子进程继续
    }

3)脱离控制终端、登录会话和进程组

有必要先介绍一下 Linux 中的进程与控制终端,登录会话和进程组之间的关系:进程属于一个进程组,进程组号(GID)就是进程组长的进程号(PID)。登录会话可以包含多个进程组。这些进程组共享一个控制终端。这个控制终端通常是创建进程的 shell 登录终端。 控制终端、登录会话和进程组通常是从父进程继承下来的。我们的目的就是要摆脱它们 ,使之不受它们的影响。因此需要调用 setsid() 使子进程成为新的会话组长,示例代码如下:
    setsid();

setsid() 调用成功后,进程成为新的会话组长和新的进程组长,并与原来的登录会话和进程组脱离。由于会话过程对控制终端的独占性,进程同时与控制终端脱离。 

4)禁止进程重新打开控制终端

现在,进程已经成为无终端的会话组长,但它可以重新申请打开一个控制终端。可以通过使进程不再成为会话组长来禁止进程重新打开控制终端,采用的方法是再次创建一个子进程,示例代码如下:
    if( pid=fork() ){ // 父进程
        exit(0);      // 结束第一子进程,第二子进程继续(第二子进程不再是会话组长) 
    }

5)关闭打开的文件描述符

进程从创建它的父进程那里继承了打开的文件描述符。如不关闭,将会浪费系统资源,造成进程所在的文件系统无法卸下以及引起无法预料的错误。按如下方法关闭它们:
    // NOFILE 为 <sys/param.h> 的宏定义
    // NOFILE 为文件描述符最大个数,不同系统有不同限制
    for(i=0; i< NOFILE; ++i){// 关闭打开的文件描述符
        close(i);
    }


6)改变当前工作目录

进程活动时,其工作目录所在的文件系统不能卸下。一般需要将工作目录改变到根目录。对于需要转储核心,写运行日志的进程将工作目录改变到特定目录如 /tmp。示例代码如下:
    chdir("/");

7)重设文件创建掩模

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

8)处理 SIGCHLD 信号

但对于某些进程,特别是服务器进程往往在请求到来时生成子进程处理请求。如果父进程不等待子进程结束,子进程将成为僵尸进程(zombie)从而占用系统资源。如果父进程等待子进程结束,将增加父进程的负担,影响服务器进程的并发性能。在 Linux 下可以简单地将 SIGCHLD 信号的操作设为 SIG_IGN 。关于信号的更详细用法,请看《信号中断处理》。
    signal(SIGCHLD, SIG_IGN);

这样,内核在子进程结束时不会产生僵尸进程。

示例代码如下:
#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)屏蔽一些控制终端操作的信号
    signal(SIGTTOU,SIG_IGN); 
    signal(SIGTTIN,SIG_IGN); 
    signal(SIGTSTP,SIG_IGN); 
    signal(SIGHUP ,SIG_IGN);
 
    // 2)在后台运行
    if( pid=fork() ){ // 父进程
        exit(0); //结束父进程,子进程继续
    }else if(pid< 0){ // 出错
        perror("fork");
        exit(EXIT_FAILURE);
    }
    
    // 3)脱离控制终端、登录会话和进程组
    setsid();  
    
    // 4)禁止进程重新打开控制终端
    if( pid=fork() ){ // 父进程
        exit(0);      // 结束第一子进程,第二子进程继续(第二子进程不再是会话组长) 
    }else if(pid< 0){ // 出错
        perror("fork");
        exit(EXIT_FAILURE);
    }  
    
    // 5)关闭打开的文件描述符
    // NOFILE 为 <sys/param.h> 的宏定义
    // NOFILE 为文件描述符最大个数,不同系统有不同限制
    for(i=0; i< NOFILE; ++i){
        close(i);
    }
    
    // 6)改变当前工作目录
    chdir("/tmp"); 
    
    // 7)重设文件创建掩模
    umask(0);  
    
    // 8)处理 SIGCHLD 信号
    signal(SIGCHLD,SIG_IGN);
    
    return 0; 

 
int main(int argc, char *argv[]) 
{
    init_daemon();
    
    while(1);
 
    return 0;
}

运行结果如下:

--------------------- 
作者:Mike__Jiang 
来源:CSDN 
原文:https://blog.csdn.net/tennysonsky/article/details/45966571 
版权声明:本文为博主原创文章,转载请附上博文链接!

猜你喜欢

转载自blog.csdn.net/Hamlee67/article/details/84108818