(一)什么是僵尸进程
在一个程序调用了exit
系统调用时内核会释放该进程的存储区,打开的文件等,但内核回味每个终止的子程序保存一定的信息量其中包含该进程的进程ID,进程的终止状态,以及该进程使用的CPU时间,这类信息父进程可通过wait
和waitpid
函数获得。
任何一个子进程(init除外),在exit
后不会立刻消失,二十六下一个僵尸进程的数据结构等待其父进程处理,而当进程退出时内核会给父进程发送SIGCHLD信号,父进程可以选择忽略此信号或提供一个该信号到来时即将执行的函数。
而如果一个父进程尚未对其调用wait
进行善后处理的进程被称为僵尸进程(Zombie),ps命令所打印的该进程状态为Z。
(二)僵尸进程目的
设置僵死状态的目的是维护子进程的信息(进程的进程ID,进程的终止状态,以及该进程使用的CPU时间),以便父进程在以后某个时候获取。如果一个进程终止,该进程有子进程处于僵尸状态,则该进程所有僵尸子进程会变成孤儿进程,Linux内核中所有的子进程在编程孤儿进程后会被init进程“领养”,initial进程会清理他们(init 调用wait
去除他们的僵尸状态)。
(三)僵尸进程的处理
我们可通过top命令来统计有多少僵尸进程:
Tasks: 262 total, 1 running, 261 sleeping, 0 stopped, 0 zombie
且在S状态栏中如果状态为Z就是僵尸进程,
或者通过ps和grep命令来寻找僵尸进程:
ps -A -ostat,ppid,pid,cmd | grep -e '^[Zz]'
// -A 参数列出所有进程
// -o 自定义输出字段 我们设定显示字段为 stat(状态), ppid(进程父id), pid(进程id),cmd(命令)这四个参数
//因为状态为 z或者Z的进程为僵尸进程,所以我们使用grep抓取stat状态为zZ进程
运行结果如下:
Z 12334 12339 /path/cmd
我们可以知道该进程的进程ID为12339 通过kill
命令来杀死该进程,通过ps命令我们可知道僵尸进程是否被清理如果没被清理可通过处理kill
它父进程的进程ID 12334 来尝试解决问题。
1、wait()函数
#include <sys/types.h>
#include <sys/wait.h> //头文件
pid_t wait(int *status); //函数原型
如果父进程调用wait()
函数就会立刻阻塞,由wait
分析子进程是否退出,如果有这样一个僵尸子进程出现wait
会收集子进程的信息,并把它销毁后返回,如果没有找到则会一直阻塞直到找到后返回,函数调用成功会返回被收集子进程的进程ID,如果该进程没有子进程则wait
返回值为-1,同时errno值为ECHILD。
参数status是一个指向int型的指针,保存收集僵尸进程的状态信息,如果不在意该僵尸进程状态,只想销毁,那么可以设置为NULL:
pid = wait(NULL);
状态信息允许父进程判定子进程的退出状态,即从子进程的main函数返回值或子进程中exit语句中的退出码。可通过头文件#include <sys/wait.h>
中的宏判断子进程的退出情况:
2、waitpid()函数
#include <sys/types.h>
#include <sys/wait.h> //头文件
pid_t waitpid(pid_t pid, int *status, int options); //函数原型
参数解释:
当pid == -1时阻塞等待任意子进程;当pid > 0时等待子进程ID与pid相等的子进程;当pid == 0时等待与调用者进程同在一组的进程;当 pid <-1时 等待其组ID等于pid绝对值的任意子进程。
status:如果不是空,会把状态信息写到它指向的位置,与wait一样
options:允许改变waitpid的行为,最有用的一个选项是WNOHANG(还有WUNTRACED和WCONTINUED),它的作用是防止waitpid把调用者的执行挂起。
返回值:如果成功返回等待子进程的ID,失败返回-1
3、wait与waitpid区别
- 在一个子进程终止前, wait 使其调用者阻塞,而waitpid 有一选择项,可使调用者不阻塞。
- waitpid并不等待第一个终止的子进程—它有若干个选择项,可以控制它所等待的特定进程。
- 实际上wait函数是waitpid函数的一个特例。waitpid(-1, &status, 0);
(四)僵尸进程的避免
- 父进程调用wait/waitpid等函数等待子进程结束,如果尚无子进程退出wait会导致父进程阻塞。waitpid可以通过传递WNOHANG使父进程不阻塞立即返回。
- 如果不想让父进程挂起,可以在父进程中加入一条语句:signal(SIGCHLD,SIG_IGN);表示父进程忽略SIGCHLD信号,由内核回收,该信号是子进程退出的时候向父进程发送的。
- 如果父进程很忙可以用signal注册信号处理函数,在信号处理函数调用wait/waitpid等待子进程退出。
在并发服务器常用第二种方式,忽略子进程的退出,让内核将僵尸子进程交给init进程处理,当有大量僵尸进程待处理的时候会占用系统资源。