操作系统面经-孤儿进程和僵尸进程

本文介绍了孤儿进程(父进程结束而未等待子进程)、僵尸进程(子进程退出但父进程未清理)和守护进程(后台运行不受终端影响)的概念,以及它们的产生机制和如何通过信号处理等方式进行管理。
摘要由CSDN通过智能技术生成

字节实习生带你进大厂,私信我领取资料

孤儿进程

  • 孤儿进程是一个比父进程存活时间更长的进程
  • 孤立进程被init所采用
  • Init等待被收养的子进程终止
  • 采用孤儿进程后,getppid()返回init的PID;通常下init的PID为1
  • 在使用upstart作为init system的系统上,或者在某些配置中使用systemd的系统上,情况是不同的

父进程如果不等待子进程退出,在子进程之前就结束了自己的“生命”此时的子进程叫做孤儿进程。====爹没了
Linux避免系统存在过多的孤儿进程,init进程收留孤儿进程,变成孤儿进程的父进程。====init养父

实例:以下是一个孤儿进程的示例程序,在此程序中,让父进程先退出,然后子进程再次打印自己的父进程号:

#include<sys/wait.h>
#include<stdio.h>
#include<unistd.h>
#include<stdlib.h>

int main(){
    pid_t fpid;
    fpid = fork();

    if(fpid == -1){
        printf("fork error.\n");
        exit(1);
    }
    else if(fpid == 0)  //child process
    {
        printf("I'm child process, child pid = %d, parent pid = %d\n",getpid(),getppid());
        sleep(5);   //睡眠5s,保证父进程退出
        printf("I'm sleep. child pid = %d, parent pid = %d\n",getpid(),getppid());
        printf("child process is done.\n");
    }
    else{
        printf("I'm parent process.\n");
        sleep(1);  //睡眠1s
        printf("parent process is done.\n");
    }
    return 0;
}

输出结果:
注意:getpid函数可以获得当前进程的pid,getppid函数可以获得当前进程的父进程号。

说明:
首先打印子进程和父进程的ID,后来父进程提前终结,子进程成为孤儿进程,打印子进程和init父进程ID。

僵尸进程

创建子进程后,子进程退出状态不被收集,变成僵尸进程。爹不要它了
除非爹死后变孤儿进程,然后被init养父接收。如果父进程是死循环,那么该僵尸进程就变成游魂野鬼消耗空间。

  • 假设子进程在父进程等待它之前终止
  • 父进程必须仍能收集状态
  • 子进程变成僵尸进程
  • 大多数流程资源都是循环利用的
  • 保留一个进程槽位:PID、状态和资源使用统计
  • 当父节点执行“wait”操作时,僵尸将被移除

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

注意:
僵尸进程还会消耗一定的系统资源,并且还保留一些概要信息供父进程查询子进程的状态可以提供父进程想要的信息。一旦父进程得到想要的信息,僵尸进程就会结束。

僵尸进程怎样产生的:
一个进程在调用exit命令结束自己的生命的时候,其实它并没有真正的被销毁,而是留下一个称为僵尸进程(Zombie)的数据结构(系统调用 exit,它的作用是使进程退出,但也仅仅限于将一个正常的进程变成一个僵尸进程,并不能将其完全销毁)。
在Linux进程的状态中,僵尸进程是非常特殊的一种,它已经放弃了几乎所有内存空间,没有任何可执行代码,也不能被调度,仅仅在进程列表中保留一个位置,记载该进程的退出状态等信息供其他进程收集,除此之外,僵尸进程不再占有任何内存空间。它需要它的父进程来为它收尸,如果他的父进程没安装 SIGCHLD信号处理函数调用wait或waitpid()等待子进程结束,又没有显式忽略该信号,那么它就一直保持僵尸状态,如果这时父进程结束了, 那么init进程自动会接手这个子进程,为它收尸,它还是能被清除的。但是如果父进程是一个循环,不会结束,那么子进程就会一直保持僵尸状态,这就是 为什么系统中有时会有很多的僵尸进程。

怎么查看僵尸进程

利用命令:ps,可以看到有标记为Z的进程就是僵尸进程。

怎么清除僵尸进程

方法一: 改写父进程,在子进程死后要为它收尸。
具体做法是接管SIGCHLD信号。子进程死后,会发送SIGCHLD信号给父进程,父进程收到此信号后,执行waitpid()函数为子进程收尸。这是基于这样的原理:就算父进程没有调用 wait,内核也会向它发送SIGCHLD消息,尽管对的默认处理是忽略,如果想响应这个消息,可以设置一个处理函数。

方法二:
把父进程杀掉。父进程死后,僵尸进程成为"孤儿进程",过继给进程init,init始终会负责清理僵尸进程。它产生的所有僵尸进程也跟着消失。
注:僵尸进程将会导致资源浪费,而孤儿则不会。

实例1:以下是一个僵尸进程的示例程序,在此程序中,子进程先退出,父进程不调用wait()或waitpid()清理子进程信息。

#include<sys/wait.h>
#include<stdio.h>
#include<unistd.h>
#include<stdlib.h>

int main(){
    pid_t fpid;
    fpid = fork();

    if(fpid == -1){
        printf("fork error.\n");
        exit(1);
    }
    else if(fpid == 0)  //child process
    {
        printf("I'm child process. pid = %d\n",getpid());
        exit(0);            //#退出进程,变成僵尸进程
    }
    else{
        printf("I'm parent process. I will sleep two seconds\n");
        sleep(2);
        system("ps -opid,ppid,state,tty,command");
        printf("father process is exiting.\n");
    }
    return 0;
}

说明:子进程变成了僵尸进程

实例2:父进程循环创建子进程,子进程退出,造成多个僵尸进程。

#include<sys/wait.h>
#include<stdio.h>
#include<unistd.h>
#include<stdlib.h>

void cream_many_zombie(void){
    pid_t pid;
    while(1){
        pid = fork();
        if(pid < 0){
            perror("fork error.\n");
            exit(1);
        }
        else if(pid == 0){
            printf("I am a child. pid = %d\nI am is existing.\n",getpid());
            exit(0);   //子进程退出,变成僵尸进程
        }
        else{
            printf("---------------------------\n");
            printf("I am parent process.\n");
            system("ps -opid,ppid,state,tty,command");
            printf("---------------------------\n");
            sleep(4);
            continue;
        }
    }
    return;
}

int main(){
    cream_many_zombie();
    return 0;
}

僵尸进程解决办法

  1. 通过信号机制:

子进程退出时向父进程发送SIGCHILD信号,父进程处理SIGCHILD信号。在信号处理函数中调用wait进行处理僵尸进程。测试程序如下所示:

image.png


当子进程停止或继续时也会生成SIGCHLD
为了防止这种情况,在使用sigaction()建立SIGCHLD处理程序时,在sa_flags中指定SA_NOCLDSTOP

#include<sys/wait.h>
#include<stdio.h>
#include<unistd.h>
#include<stdlib.h>
#include<signal.h>

static void signale_handler(int signo){
    pid_t pid;
    int stat;
    //处理僵尸进程
    while((pid = waitpid(-1,&stat,WNOHANG)) > 0){
        printf("child %d terminated.\n",pid);
    }
}

void sigchld_zombie(void){
    pid_t pid;
    // signal(SIGCHLD, signale_handler);
    struct sigaction sa;
    sa.sa_handler = signale_handler;
    sa.sa_flags = SA_NOCLDSTOP ;
    sigemptyset(&sa.sa_mask);
    if(sigaction(SIGCHLD,&sa,NULL) == -1){
        perror("sigaction");
    }

    pid = fork();
    if(pid == -1){
        printf("fork error.\n");
        exit(1);
    }
    else if(pid == 0)  //child process
    {
        printf("I am child process,pid = %d.\nI am exiting.\n",getpid());
        exit(0);
    }
    else{
        sleep(2);
        //输出进程信息
        system("ps -opid,ppid,state,tty,command");
        printf("father process isexiting.\n");
    }
    return;
}

int main(){
    sigchld_zombie();
    return 0;
}

2. 两次fork()

实现思路:将子进程成为孤儿进程,从而其的父进程变为init进程,通过init进程可以处理僵尸进程。

  1. 在子进程中再创建一个子进程,相当于第一个子进程就是第二个子进程的父进程.
  2. 当sleep(3)后,要确保第一个进程(第二个进程的父进程)退出,那么第二个进程就变成了孤儿进程
  3. 然后init进程会过来接手处理。
  4. 所有到最后,第一个进程的Pid就变成了init的pid。
#include<sys/wait.h>
#include<stdio.h>
#include<unistd.h>
#include<stdlib.h>
#include<signal.h>

void doble_fork_zombie(void){
    pid_t pid;
    //创建第一个子进程
    pid = fork();
    if (pid < 0)
    {
        perror("fork error:");
        exit(1);
    }
    //第一个子进程
    else if (pid == 0)
    {
        //子进程再创建子进程
        printf("\nI am the first child. pid:%d\tppid:%d\n",getpid(),getppid());
        pid = fork();
        if (pid < 0)
        {
            perror("fork error:");
            exit(1);
        }
        //第一个子进程退出
        else if (pid > 0)
        {
            printf("first process is exited.\n");
            exit(0);
        }
        //第二个子进程
        //睡眠3s保证第一个子进程退出,这样第二个子进程的父亲就是init进程
        sleep(3);
        printf("I am the second child. pid: %d\tppid:%d\n",getpid(),getppid());
        system("ps -opid,ppid,state,tty,command");
        exit(0);
    }
    //父进程处理第一个子进程退出
    if (waitpid(pid, NULL, 0) != pid)
    {
        perror("waitepid error:");
        exit(1);
    }
    exit(0);
}

int main(){
    doble_fork_zombie();
    return 0;
}

说明:父进程变成了init进程。

守护进程

守护进程(Daemon)就是在后台运行,不与任何终端关联的进程,通常情况下守护进程在系统启动时就在运行,它们以root用户或者其他特殊用户(apache和postfix)运行,并能处理一些系统级的任务。习惯上守护进程的名字通常以d结尾(sshd),但这些不是必须的。随系统启动, 其父进程 (ppid) 通常是init 进程。====后台小天使

守护进程是脱离于终端并且在后台运行的进程。守护进程脱离于终端,是为了避免进程在执行过程中的信息在任何终端上显示,并且进程也不会被任何终端所产生的终端信息所打断。

守护进程,也就是通常说的Daemon进程,是Linux中的后台服务进程。它是一个生存期较长的进程,通常独立于控制终端并且周期性地执行某种任务或等待处理某些发生的事件。守护进程常常在系统引导装入时启动,在系统关闭时终止。Linux系统有很多守护进程,大多数服务都是通过守护进程实现的,同时,守护进程还能完成许多系统任务,例如,作业规划进程crond、打印进程lqd等(这里的结尾字母d就是Daemon的意思)。

由于在Linux中,每一个系统与用户进行交流的界面称为终端,每一个从此终端开始运行的进程都会依附于这个终端,这个终端就称为这些进程的控制终端,当控制终端被关闭时,相应的进程都会自动关闭。但是守护进程却能够突破这种限制,它从被执行开始运转,直到整个系统关闭时才退出。如果想让某个进程不因为用户或终端或其他的变化而受到影响,那么就必须把这个进程变成一个守护进程。

下面介绍一下创建守护进程的步骤:

  1. 调用fork(),创建新进程,它会是将来的守护进程.
  2. 在父进程中调用exit,保证子进程不是进程组长
  3. 调用setsid()创建新的会话区,子进程成为会话首进程
  4. 将当前目录改成根目录(如果把当前目录作为守护进程的目录,当前目录不能被卸载他作为守护进程的工作目录),关闭文件描述符
  5. 忽略信号,进行后台服务逻辑处理

代码演示:

#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <syslog.h>

int main(void) {
  pid_t pid;

  // Fork 父进程
  pid = fork();

  if (pid < 0) {
    exit(EXIT_FAILURE);
  }

  // 父进程退出,子进程独立运行
  if (pid > 0) {
    exit(EXIT_SUCCESS);
  }

  // 创建新会话,设置子进程为首领进程
  if (setsid() < 0) {
    exit(EXIT_FAILURE);
  }

  // 改变工作目录
  chdir("/");

  // 重设文件权限掩码
  umask(0);

  // 关闭文件描述符
  for (int i=sysconf(_SC_OPEN_MAX); i>=0; i--) {
    close (i);
  }
  // 打开日志文件
  openlog ("testdaemon", LOG_PID, LOG_DAEMON);

  // 进入无限循环
  while (1) {
    syslog (LOG_INFO, "Daemon running");
    sleep(30);
  }
  closelog();

  return EXIT_SUCCESS;
}

总结

  1. 孤儿进程:父进程先结束【爹先挂了】,init养父(进程号为1) 收养,并被重新设置为其子进程
  2. 僵尸进程:子进程终止,但父进程没有使用wait或waitpid收集其资源【爹不管】
  3. 守护进程:在后台运行,不与任何终端关联的进程【后台天使】

三者的区别:

  • 孤儿进程父进程先结束,守护进程不依赖父进程
  • 僵尸进程已终止但留存PCB,守护进程是持续运行
  • 守护进程脱离终端后台运行,其他两种与终端无关
  • 20
    点赞
  • 21
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值