目录
僵尸(Zombie)进程
僵尸进程的产生机制
僵尸进程的危害
僵尸进程的销毁
wait函数
waitpid函数
进程管理在网络编程中十分重要,如果未处理好,将会导致出现“僵尸进程”,进而影响服务器端对进程的管控。
僵尸(Zombie)进程
第一次听到这个名词大家可能会有些陌生,为什么叫“僵尸”进程?事实上,这个词用的很形象也很恰当:当一个进程使用fork创建子进程后,若出现子进程先于父进程结束,并且此时父进程没有对子进程的资源进行释放回收,那么这个子进程将成为一个“僵尸进程”,并始终占据着一个进程号。
僵尸进程的产生机制
通过使用fork函数可以制造一些“僵尸进程”。根据之前的定义,让我们用代码来引出僵尸进程。
zombie.cpp
#include <stdio.h>
#include <unistd.h>
#define CIRCLE 40
int main(int argc, char *argv[])
{
pid_t pid = fork();
if (pid == 0) // 子进程
{
printf("I'm child process\n");
}
else
{
printf("Child Process ID: %d \n", pid);
sleep(CIRCLE); // 休眠20s
}
pid == 0 ? printf("Child process end")
: printf("Parent process end");
return 0;
}
运行结果(注意要编译出来运行哦~一些IDE会默认对僵尸进程进行处理):
通过 ps au 指令,我们可以查看所有当前进程的具体信息。不难发现,ID为10476的进程被标记为僵尸进程(Z+),而经过40秒的等待后,子进程和父进程同时被销毁,僵尸进程消失。
补充:
用终端打开程序时,我们可以用 & 符号将窗口中输入的指令放到后台去运行。
比如我们编译好的zombie程序,输入 ./zombie & 后程序开始执行,继续输入 ps au,可以在同一个终端下查看进程的变化。
僵尸进程的危害
出现少量的僵尸进程并不会对操作系统造成太大影响,但如果数量多了,那么操作系统将可能因为没有可用的进程号(僵尸进程也占用进程号)而导致系统无法创建新的进程。
僵尸进程的销毁
在这之前,我们应晓得如何使程序得到结束,具体方式有以下几种:
- 传递参数并调用exit函数
- main函数中执行return语句并返回值
为了正确销毁子进程,父进程应主动请求获取子进程的返回值。在<sys/wait.h>库中,有2个方法可以提供给我们帮助。
wait函数
#include <sys/wait.h>
pid_t wait(int * statloc);
//成功时退回终止的子进程 ID, 失败时返回-1。
调用此函数时如果已有子进程终止 ,那么子进程终止时传递的返回值(exit函数的参数值、main函数的return返回值)将保存到该函数的参数所指内存空间内。但函数参数指向的单元中还包含其他信息,因此需要通过以下宏进行分离:
- WIFEXITED(整数型变量):子进程正常终止时,返回true
- WEXITSTATUS(整数型变量):获取子进程的的返回值
如何使用呢?请看以下例子:
wait.cpp
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <sys/wait.h>
#define CIRCLE 40
int main(int argc, char *argv[])
{
int status;
pid_t pid = fork(); //这里创建的子进程将在通过return语句终止
if (pid == 0) // 子进程进入
{
return 111;
}
else
{
printf("Child process ID: %d \n", pid);
pid = fork(); //这里创建的进程将通过下面的exit()函数终止
if (pid == 0)
{
exit(222); //exit中所填常量即返回值
}
else
{
printf("Child process ID: %d \n", pid);
//之前终止的子进程信息将保存在status变量中,同时相关子进程被销毁
wait(&status);
//通过WIFEXITED验证子进程是否正常终止。
//如果正常退出,则调用WEXITSTATUS输出子进程的返回值。
if (WIFEXITED(status))
{
printf("Child 1 return: %d \n", WEXITSTATUS(status));
}
//再次调用wait函数和宏,以处理另一个子进程
wait(&status);
if (WIFEXITED(status))
{
printf("Child 2 return: %d \n", WEXITSTATUS(status));
}
sleep(CIRCLE); //让父进程休眠,以便验证查看
}
}
return 0;
}
运行结果:
但是请大家注意,利用wait来销毁僵尸进程时,如果没有己终止的子进程,那么程序将会一直阻塞(Blocking),直到有子进程终止才能继续下一步操作。
waitpid函数
wait函数会引起程序阻塞,那么有没有其他方法能够解决这个问题呢?当然有,那就是wait函数的改进版——waitpid。这个方法能够避免阻塞的同时控制僵尸进程。
#include <sys/wait.h>
pid_t waitpid(pid_t pid , int * statloc , int options);
//成功时返回终止的子进程ID,失败时返回-1
/*参数说明*/
// pid: 等待终止的目标子进程的ID,若传递-1,则与wait函数相同,等待任意子进程终止
// statloc: 反应状态的变量
// options: 传递头文件sys/wait.h中声明的常量WNOHANG(值为1),即使没有终止的子进程也不会进入阻塞状态,而是返回O并退出函数
测试用例:
waitpid.cpp
#include <stdio.h>
#include <unistd.h>
#include <sys/wait.h>
int main(int argc, char *argv[])
{
int status;
pid_t pid = fork();
if (pid == 0)
{
sleep(10); //延迟子进程10s
return 222;
}
else
{
while (!waitpid(-1, &status, WNOHANG)) //若之前没有终止的子进程将返回0结束循环
{
sleep(2);
printf("sleep 2s");
}
if (WIFEXITED(status))
{
printf("Child return %d \n", WEXITSTATUS(status));
}
}
return 0;
}
运行结果:
sleep(2)函数被调用了5次,共计延迟 2*5 =10 s,验证了waitpid在没有发生阻塞的同时销毁了可能出现的僵尸进程。