仅做个人学习记录
一般情况下,程序调用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的博客-博客其实我们是在图形界面的伪终端上运行程序的,并不是真正的终端。具体可以看链接。
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,就立即阻塞自己,由wait自动分析是否当前进程的某个子进程已经退出,如果让它找到了这样一个已经变成僵尸的子进程,wait就会收集这个子进程的信息,并把它彻底销毁后返回;如果没有找到这样一个子进程,wait就会一直阻塞在这里,直到有一个出现为止。
参数:status用来保存被收集进程退出时的一些状态,它是一个指向int类型的指针。但如果我们对这个子进程是如何死掉的毫不在意,只想把这个僵尸进程消灭掉,(事实上绝大多数情况下,我们都会这样想),我们就可以设定这个参数为NULL,就象下面这样:
pid = wait(NULL);如果成功,wait会返回被收集的子进程的进程ID,如果调用进程没有子进程,调用就会失败,此时wait返回-1,同时errno被置为ECHILD。
wait系统调用会使父进程暂停执行,直到它的一个子进程结束为止。返回的是子进程的PID,它通常是结束的子进程状态信息允许父进程判定子进程的退出状态,即从子进程的main函数返回的值或子进程中exit语句的退出码。如果status不是一个空指针,状态信息将被写入它指向的位置可以等待特定的子进程终止再返回。
#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的绝对值的任一子进程
来自: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); }运行结果: 以上代码都是可以运行的。