按照正常的逻辑,应该讲讲 vfork 的(专为 exec 而定制)。不过鉴于 vfork 现在已经很少使用了,而且现在的 fork 也完全可以替代 vfork,所以讲 vfork 有点重复的意思。当然了不排除面试或者考试会有人问到 vfork,这里稍微提两笔。vfork 采用了类似读时共享的机制,但是其不保证写时复制,它产生的子进程和父进程共享进程空间,所以,如果在 vfork 后没有使用 exec 或者 _exit 函数,其行为将是未定义的。
好了,点到为止,而且实际开发中,也不希望使用 vfork(如果你对vfork感兴趣,请自行 man 或者查阅 《apue》)。下面来造僵尸吧 ^_^。
1. 僵尸进程
如果你对父子进程还不理解,赶紧先跳到前面的章节。
“如果子进程运行结束了而父进程还没有,就会产生僵尸进程。”
这很容易理解,比如你 fork 了一个子进程后,父进程由于耗时任务还没结束,子进程却未老先衰,父进程还对它视而不见,它就死不瞑目。
1.1 造 5 个僵小鱼
下面的代码演示了僵尸进程的制造方法。其原理很简单,就是保证“白发人送黑发人”这一条件就OK。
- 代码
// mywait.c
#include <unistd.h>
#include <stdio.h>
#include <string.h>
int main() {
printf("before fork\n");
pid_t pid, n = 5;
// 父进程生出 5 个子进程
while(n--) {
pid = fork();
if (pid == 0) break;
else if (pid < 0) {
perror("fork");
return 1;
}
}
// 子进程打印一句话就死了。
if (pid == 0) {
printf("hello, I'm child %d; my father is %d\n", getpid(), getppid());
return 0;
}
// 父进程永远在这打印。
while(1) {
sleep(3);
printf("hello, I'm father %d\n", getpid());
}
return 0;
}
- 编译
$ gcc mywait.c -o mywait
- 运行
$ ./mywait
- 结果
before fork
hello, I'm child 7923; my father is 7918
hello, I'm child 7922; my father is 7918
hello, I'm child 7921; my father is 7918
hello, I'm child 7920; my father is 7918
hello, I'm child 7919; my father is 7918
hello, I'm father 7918
hello, I'm father 7918
hello, I'm father 7918
hello, I'm father 7918
……
1.2 查看僵尸
再开启一个终端,使用 ps -af
命令,结果如下:
UID PID PPID C STIME TTY TIME CMD
……
allen 7918 4565 0 14:19 pts/1 00:00:00 ./mywait
allen 7919 7918 0 14:19 pts/1 00:00:00 [mywait] <defunct>
allen 7920 7918 0 14:19 pts/1 00:00:00 [mywait] <defunct>
allen 7921 7918 0 14:19 pts/1 00:00:00 [mywait] <defunct>
allen 7922 7918 0 14:19 pts/1 00:00:00 [mywait] <defunct>
allen 7923 7918 0 14:19 pts/1 00:00:00 [mywait] <defunct>
allen 7953 7143 0 14:23 pts/2 00:00:00 ps -af
可以看到,这里有 5 个进程,名字被加了方括号,后面还跟着 <defunct>
字样。可是子进程明明已经结束了呢?为什么还死不瞑目?
实际上,子进程在死的时候,通知了它父亲:父亲我要死了,快来给我收尸吧!(发送 SIGCHILD信号给父进程)
可是前面的代码父亲除了一直在喊:我是父亲 7918, 我是父亲 7918,……好像没有干其它任何事情。子进程未等到父亲的回复,所以在那死不瞑目。除非子进程的父亲也死了,这时候会有 init 进程来替代原来的父亲替这些子进程收尸。
2. wait 函数——让逝者安息
弄明白了僵尸进程的由来,我们就能想出对策来清理这些僵尸进程。有人说,不清理不行吗?这样说吧,如果僵尸进程的数量非常少,其实对系统造不成什么威胁,如果僵尸进程越来越多,最后就会造成资源耗尽。所以,代码的健壮性很重要!!!特别是服务器开发领域中,通常一运行都上数月甚至好几年,如果代码不健壮,就会造成系统中的僵尸进程越来越多,最后瘫痪(虽然重启也可以解决,但是这治标不治本)。
前面讲到,子进程在死的时候会通知父亲(给父进程发送 SIGCHILD 信号,信号的概念后面才会讲到,这里只要知道就行了),所以父进程只要妥尚处理好子进程的通知就行了,需要用到的函数就是 wait.
wait 函数原型如下:
// 参数保存子进程退出通知码,返回 -1 表示没有子进程或者错误。否则返回子进程的进程 id 号。
pid_t wait(int *status);
前面的代码改成如下的样子:
- 代码
// wipeoutzombie.c
#include <unistd.h>
#include <stdio.h>
#include <string.h>
#include <sys/wait.h>
#include <sys/types.h>
int main() {
printf("before fork\n");
pid_t pid, n = 5;
while(n--) {
pid = fork();
if (pid == 0) break;
else if (pid < 0) {
perror("fork");
return 1;
}
}
if (pid == 0) {
printf("hello, I'm child %d; my father is %d\n", getpid(), getppid());
return 0;
}
while(1) {
sleep(3);
pid = wait(NULL); // 忽略子进程通知码
if (pid == -1) {
perror("wait");
sleep(10);
printf("I'm father %d; I have wiped out all zombies\n", getpid());
return 1;
}
printf("Hello, I'm father %d; child %d exit\n", getpid(), pid);
}
return 0;
}
重新编译运行后结果如下:
before fork
hello, I'm child 8092; my father is 8087
hello, I'm child 8091; my father is 8087
hello, I'm child 8090; my father is 8087
hello, I'm child 8089; my father is 8087
hello, I'm child 8088; my father is 8087
Hello, I'm father 8087; child 8088 exit
Hello, I'm father 8087; child 8089 exit
Hello, I'm father 8087; child 8090 exit
Hello, I'm father 8087; child 8091 exit
Hello, I'm father 8087; child 8092 exit
wait: No child processes
I'm father 8087; I have wiped out all zombies
3. 总结
- 理解什么是僵尸进程
- 知道子进程退出时会给父进程发送 SIGCHILD 信号
- 学会使用 wait 函数来清理僵尸进程
本篇只是初步认识僵尸进程和简单的处理方法,但是这远远不能满足日常需求。后面的文章将会更加深入。