基本来自:
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);
}
运行结果:
以上代码都是可以运行的。