关于僵尸进程和孤儿进程


基本来自:
linux下的僵尸进程处理SIGCHLD信号 - Jessica程序猿 - 博客园
linux 僵死进程及其处理方法_大树叶 技术专栏-CSDN博客
十二、僵死进程 && 进程替换 - 灰信网(软件开发博客聚合)
《UNIX环境高级编程》

仅做个人学习记录

什么是僵尸进程?

一般情况下,程序调用exit或者return之后,它的绝大多数内存和相关的资源已经被内核释放掉,但是在进程表中这个进程项(entry)还保留着(进程ID,退出状态,占用的资源,执行的cpu时间等等),你可能会问,为什么这么麻烦,直接释放完资源不就行了吗?这是因为有时它的父进程想了解它的退出状态(父进程调用函数wait或者waitpid就可以获取子进程终止信息)。在子进程退出但还未被其父进程“收尸”之前,该子进程就是僵死进程,或者僵尸进程。

如果父进程在子进程之前终止,那么子进程将被init进程(ID为1)收养,这个时候init就是这个子进程的父进程。一个由init进程收养的子进程终止的时候,会怎么样呢?init进程被编写成无论何时只要有子进程终止,就会调用wait函数获得其终止状态。

来自《UNIX环境高级编程》:

一个已经终止、但是其父进程尚未对其进行善后处理(获取终止进程的有关信息、释放它仍占用的资源)的进程被称为僵死进程。

所以一旦出现父进程长期运行,又没有显式调用wait或者waitpid,同时也没有处理SIGCHLD信号,这个时候init进程就没有办法来替子进程收尸,这个时候,子进程就真的成了“僵尸”了。

僵尸进程例子,子进程调用exit退出后,父进程长期运行,有没有显式调用wait或者waitpid,init进程就没有办法来替子进程收尸:

#include <unistd.h>
#include <iostream>
using namespace std;

int main(int argc, char **argv){
    pid_t chldpid = 0;
    int i;
    cout << "main enter" << endl;
    for (i = 0; i < 10; i++){//fork会有两个返回值,具体可以看下一个问题
        if ((chldpid = fork()) == 0){//子进程
            cout << "child exit" << endl;
            exit(0);
        }
        else    cout << "child process generted = " << chldpid << endl;//父进程
    }
    i = 100;
    while (i > 0)   sleep(10);
    return 0;
}

僵死进程与孤儿进程的区别?

就是爸爸(父进程)和儿子(子进程)谁先死的问题!

  • 如果儿子还在世,爸爸去世了,儿子就成孤儿了,这个时候儿子就会被init进程收养,换句话说,init进程充当了儿子的爸爸,所以等到儿子去世的时候,就由init进程来为其收尸。

  • 如果爸爸还活着,儿子死了,这个时候如果爸爸不给儿子收尸,那么儿子就会变成僵尸进程。

孤儿进程例子:

#include <unistd.h>
#include <iostream>
using namespace std;

int main() {
	pid_t pid;
	pid=fork();
	//如果子进程创建失败,直接返回,并打印fork函数错误
	if(pid==-1) {
        cout << "fork error!!!" << endl;
		return 0;
	}
	if(pid==0) {//子进程进入循环
        cout << "子进程id: " << getpid() << endl;
        cout << "父进程id: " << getppid() << endl;
        int i = 10;
		while(i--) {
			sleep(1);
		}
        cout << getppid() << endl;
	}
	if(pid>0) {//父进程直接死亡
		return 0;
	}
	return 0;
}

也可以使用ps -ef命令查看进程id以及对应的父进程id。
在这里插入图片描述
可以看到在过了一段时间之后父进程变为了1588号Systemd进程(ps -ef可以查看进程属性)。也就是说子进程变成孤儿进程之后被别人收养了,但是却不是被init进程(1号进程)收养。原因是:为什么孤儿进程没有被init收养_IceberGu的博客-CSDN博客其实我们是在图形界面的伪终端上运行程序的,并不是真正的终端。具体可以看链接。

僵死进程的危害

1、僵死进程的PID还占据着,意味着海量的子进程会占据满进程表项,会使后来的进程无法fork。

2、僵死进程的内核栈无法被释放掉(1K 或者 2K大小),为啥会留着它的内核栈,因为在栈的最低端,有着thread_info结构,它包含着 struct_task 结构,这里面包含着一些退出信息。

如何避免僵尸进程?

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进程去处理,省去了大量僵尸进程占用系统资源。

僵尸进程处理办法

当一个进程正常或者异常终止时,内核就向其父进程发送SIGCHLD信号。因为子进程终止是一个异步事件(可以在父进程运行的任何时候发生),所以这种信号也是内核向父进程发的异步通知。

调用wait和waitpid的进程可能会发生什么?

  • 如果其所有子进程都还在运行,则阻塞;
  • 如果一个子进程已终止,正等待父进程获取其终止状态,则取得该子进程的终止状态立即返回;
  • 如果它没有任何子进程,则立即出错返回;

wait函数

#include <sys/wait.h>
pid_t wait(int *status);

进程一旦调用了wait,就立即阻塞自己,由wait自动分析是否当前进程的某个子进程已经退出,如果让它找到了这样一个已经变成僵尸的子进程,wait就会收集这个子进程的信息,并把它彻底销毁后返回;如果没有找到这样一个子进程,wait就会一直阻塞在这里,直到有一个出现为止。

参数:status用来保存被收集进程退出时的一些状态,它是一个指向int类型的指针。但如果我们对这个子进程是如何死掉的毫不在意,只想把这个僵尸进程消灭掉,(事实上绝大多数情况下,我们都会这样想),我们就可以设定这个参数为NULL,就象下面这样:

pid = wait(NULL);

如果成功,wait会返回被收集的子进程的进程ID,如果调用进程没有子进程,调用就会失败,此时wait返回-1,同时errno被置为ECHILD。

  • wait系统调用会使父进程暂停执行,直到它的一个子进程结束为止。
  • 返回的是子进程的PID,它通常是结束的子进程
  • 状态信息允许父进程判定子进程的退出状态,即从子进程的main函数返回的值或子进程中exit语句的退出码。
  • 如果status不是一个空指针,状态信息将被写入它指向的位置

waitpid函数

可以等待特定的子进程终止再返回。

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

参数:

  • status:如果不是空,会把状态信息写到它指向的位置,与wait一样
  • options:允许改变waitpid的行为,最有用的一个选项是WNOHANG,它的作用是防止waitpid把调用者的执行挂起

返回值:如果成功返回等待子进程的ID,失败返回-1

对于waitpid的p i d参数的解释与其值有关:

  • pid == -1 等待任一子进程。于是在这一功能方面waitpid与wait等效。

  • pid > 0 等待其进程ID与pid相等的子进程。

  • pid == 0 等待其组ID等于调用进程的组ID的任一子进程。换句话说是与调用者进程同在一个组的进程。

  • pid < -1 等待其组ID等于pid的绝对值的任一子进程

wait与waitpid区别:

  • 在一个子进程终止前, wait 使其调用者阻塞,而waitpid 有一选择项,可使调用者不阻塞
  • waitpid并不等待第一个终止的子进程—它有若干个选择项,可以控制它等待特定的进程终止。
  • waitpid支持作业控制。
  • 实际上wait函数是waitpid函数的一个特例:waitpid(-1, &status, 0)

示例

来自:linux下的僵尸进程处理SIGCHLD信号 - Jessica程序猿 - 博客园

如以下代码会创建100个子进程,但是父进程并未等待它们结束,所以在父进程退出前会有100个僵尸进程。

#include <stdio.h>  
#include <unistd.h>  
   
int main() {  
  int i;  
  pid_t pid;  
  for(i=0; i<100; i++) {  
    pid = fork();  
    if(pid == 0)  break;  
  }  
  if(pid>0) {  
    printf("press Enter to exit...");  
    getchar();  
  }  
  return 0;  
}

wait函数:直接在父进程里面进行wait的调用即可,参数为NULL。
来自:十二、僵死进程 && 进程替换 - 灰信网(软件开发博客聚合)

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

int main(){
    pid_t pid=fork();
    assert(pid!=-1);
    if(pid==0){
        printf("i am child\n");
        sleep(10);
        printf("child over\n");
    }
    else{
        pid_t id=wait(NULL);//处理僵死进程
        printf("i am father\n");
        sleep(20);
        printf("father over");
    }
    exit(0);
}

运行结果:
在这里插入图片描述

waitpid函数:

指定处理fork()之后PID为子进程的僵死进程,设置为不阻塞的即options=WNOHANG,那么在父进程中调用waitpid(pid,NULL,WNOHANG)即可。

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

int main(){
    pid_t pid=fork();
    assert(pid!=-1);
    if(pid==0){
        printf("i am child\n");
        sleep(5);
        printf("child over\n");
    }
    else{
        pid_t id;
        do//父进程循环检测僵死进程,并不会阻塞在这不动
        {
            id=waitpid(pid,NULL,WNOHANG);//如果最后一个参数options值为0,那么就会阻塞,和wait一样
            if(id==0){
                printf("child run\n");
                sleep(1);
            }
        }while(id==0);
        printf("i am father\n");
        sleep(10);
        printf("father over");
    }
    exit(0);
}

运行结果:
在这里插入图片描述
以上代码都是可以运行的。

猜你喜欢

转载自blog.csdn.net/qq_32523711/article/details/108954901