僵尸进程
文章目录
1. 概念
僵尸进程指的是处于僵尸态的进程,这种进程无法进行调度,但其所占用的系统资源并未被释放。僵尸态是进程生命周期的必经阶段,是无法避免的,但为了节约系统资源,应尽快清理腾出僵尸态进程所占用的内存资源。
僵尸进程
2. 产生的原因
当一个程序的代码流程从main函数返回后,进程就结束了,但此时不能立即退出,因为还需要向其父进程汇报执行的结果和死亡的原因,又因为已无法被调度,因此进程只能以一种被动的姿态躺倒,等待其创建者(父进程)前来获取其执行结果和死亡原因。
以下代码可以查看处于僵尸态的进程:
// zombie.c
int main()
{
// 子进程退出(变僵尸)
if(fork() == 0)
return 0;
// 父进程不退出
pause();
return 0;
}
执行上述代码,并使用ps命令查看进程状态:
gec@ubuntu:~$ ./zombie
gec@ubuntu:~$ ps ajx
... ...
8390 8422 8422 8422 pts/2 8439 Ss 1000 0:00 bash
8422 8439 8439 8422 pts/2 8439 S+ 1000 0:00 ./zombie
8439 8440 8439 8422 pts/2 8439 Z+ 1000 0:00 [zombie] <defunct>
8406 8441 8441 8406 pts/1 8441 R+ 1000 0:00 ps ajx
gec@ubuntu:~$ ps ajx
由上述结果可见,zombie父进程处于睡眠状态[S+],而其子进程[zombie]处于僵尸态[Z+]。
3. 释放僵尸进程
上述实验中,如果父进程一直对其子进程不管不顾,那么其子进程的确会长期处于僵尸态,浪费系统资源。僵尸进程只有当以下情形之一发生时,才会释放其资源:
- 父进程对其调用 wait()/waitpid()。
- 父进程退出,被孤儿进程组收养。
3.1 方法一:父进程直接退出
这应该不算什么方法,父进程的退出只是便于让系统中的孤儿进程组收养其孤儿进程,进而在该孤儿进程编程僵尸后由系统负责回收并释放其系统资源。
3.2 方法二:子进程等待父进程对其执行wait()/waitpid()
这是最浅显的做法,wait()/waitpid()函数有如下三个功效:
- 释放对应僵尸子进程的系统资源
- 获取对应僵尸子进程的退出状态
- 阻塞父进程(可选)
上述功效中的第一项即可满足我们目前的需求,比如在如上代码中,只要父进程代码做如下修改即可避免僵尸的产生:
// no-zombie.c
int main()
{
// 子进程退出(变僵尸)
if(fork() == 0)
return 0;
// 父进程调用wait()释放子进程资源
wait(NULL);
pause();
return 0;
}
此办法看似简单,但并不实用,因为一般而言父进程很难预知子进程退出的时机,当父进程执行wait()/waitpid()的时候,要么进入漫长的等待,要么子进程尚未变僵尸,因此让父进程去时刻主动关注子进程的状态和回收资源是不切实际的,而且父进程的后代进程可能不止一个,大家任务各有长短,父进程本身也可能处于循环任务之中,因此该办法仅供参考。
3.3 方法三:子进程主动告知父进程前来收尸
这是最常见的做法,让变成僵尸的子进程主动通知其父进程,既省却了父进程监控子进程的麻烦,又可以让子进程的僵尸状态尽可能保证在最短时间内处理。
具体而言,子进程在进入僵尸态时,会自动向父进程发送信号SIGCHILD,而父进程可以利用异步信号响应函数来及时处理这些僵尸子进程。参考代码如下:
void cleanup(int sig)
{
// 僵尸子进程会被自动清除
wait(NULL);
}
int main()
{
// 在产生子进程之前,准备好处理它们的SIGCHILD信号
signal(SIGCHLD, cleanup);
// 子进程退出,成为僵尸进程
if(fork() == 0)
return 0;
// 父进程干自己的活,无需关注子进程
while(1)
pause();
return 0;
}
注意,由于pause()函数会在收到信号后退出,为了能查看父进程在清除子进程资源后仍在运行,上述代码中使用了循环来执行pause()函数。