引言:前面的系列文章介绍了进程的基础概念和相关常用的 API,本文将介绍两种比较特殊进程:孤儿进程与僵尸进程,进一步加深对进程的了解,避免进程使用过程中的一些坑点。
孤儿进程
什么是孤儿进程?
父进程运行结束,但子进程还在运行(未运行结束),这样的子进程就称为孤儿进程(Orphan Process)
每当出现一个孤儿进程的时候,内核就把孤儿进程的父进程设置为 init ,而 init 进程会循环地 wait()
它的已经退出的子进程。这样,当一个孤儿进程凄凉地结束了其生命周期的时候,init 进程就会代表党和政府出面处理它的一切善后工作。 因此孤儿进程并不会有什么危害。
总之:孤儿进程就是父进程退出了,但子进程还在执行。
示例:
#include <sys/types.h>
#include <unistd.h>
#include <stdio.h>
#include <stdlib.h>
int main()
{
pid_t pid = -1;
// 创建子进程
pid = fork();
if (pid < 0) // 没有创建成功
{
perror("fork");
return 1;
}
// 父进程
if (pid > 0)
{
printf("父进程休息3秒后退出。。。\n");
printf("父进程: pid:%d\n", getpid());
sleep(1);
printf("父进程等太累了,现退出了。。。\n");
exit(0);
}
while (1)
{
printf("子进程不停的工作,子进程:pid:%d,父进程:ppid:%d\n", getpid(), getppid());
sleep(1);
}
return 0;
}
运行结果:
yxm@192:~$ gcc test.c -o test
yxm@192:~$ ./test
父进程休息3秒后退出。。。
父进程: pid:32075
子进程不停的工作,子进程:pid:32076,父进程:ppid:32075
父进程等太累了,现退出了。。。
子进程不停的工作,子进程:pid:32076,父进程:ppid:32075
yxm@192:~$ 子进程不停的工作,子进程:pid:32076,父进程:ppid:1 # 终端可以输入,同时有数据在输出
子进程不停的工作,子进程:pid:32076,父进程:ppid:1
子进程不停的工作,子进程:pid:32076,父进程:ppid:1
一般情况下,运行一个程序时,默认会切换到后台运行,当有输出的时候再切换到前台。
如上面的运行结果所示:创建子进程后,子进程复制了父进程内核部分的某些数据(比如标准输入、标准输出、标准错误),所以父进程和子进程的标准输出都是当前终端。又因为父进程是前台进程,所以会占用当前终端,但是父进程死亡后,终端占用被解除,但是子进程(变成孤儿进程)没有死亡,其标准输出依旧是当前终端。最终形成了,终端可以输入,同时有数据在输出的特殊情况。
【注意】ubuntu 系统中,字节界面中,产生的孤儿进程会被 1 号( 即 init 进程)进程收养,但是在图形界面中孤儿进程会被非1号进程收养。
僵尸进程
僵尸进程介绍
每个进程结束之后, 都会释放自己地址空间中的用户区数据,内核区的 PCB 没有办法自己释放掉(子进程残留资源(PCB)存放于内核中),需要父进程去释放,进程终止时,父进程尚未回收,子进程残留资源(PCB)存放于内核中,变成僵尸(Zombie)进程。
僵尸进程不能被 kill -9 杀死,这样就会导致一个问题,如果父进程不调用 wait()
或 waitpid()
的话,那么保留的那段信息就不会释放,其进程号就会一直被占用,但是系统所能使用的进程号是有限的,如果大量的产生僵尸进程,将因为没有可用的进程号而导致系统不能产生新的进程,此即为僵尸进程的危害,应当避免。
总之:僵尸进程就是子进程结束了,但父进程没有回收其资源。
#include <stdio.h>
#include <string.h>
#include <sys/types.h>
#include <unistd.h>
int main()
{
int i = 0;
pid_t pid = -1;
// 创建子进程
pid = fork();
if (-1 == pid) // 没有创建成功
{
perror("fork");
return 1;
}
// 子进程
if (0 == pid)
{
for (int i = 0; i < 5; i++)
{
printf("子进程做事%d\n", i);
sleep(1);
}
printf("子进程想不开,结束了自己。。。。\n");
exit(0);
}
else if (pid > 0)
{
while(1)
{
printf("父进程休眠了, pid : %d, ppid : %d\n", getpid(), getppid());
sleep(1);
}
}
return 0;
}
yxm@192:~$ gcc test.c -o test
yxm@192:~$ ./test
父进程休眠了, pid : 33087, ppid : 30344
子进程做事0
父进程休眠了, pid : 33087, ppid : 30344
子进程做事1
子进程做事2
父进程休眠了, pid : 33087, ppid : 30344
父进程休眠了, pid : 33087, ppid : 30344
子进程做事3
子进程做事4
父进程休眠了, pid : 33087, ppid : 30344
父进程休眠了, pid : 33087, ppid : 30344
子进程想不开,结束了自己。。。。
父进程休眠了, pid : 33087, ppid : 30344
^C
yxm@192:~$ ps -aux
...
...
yxm 33087 0.0 0.0 4516 756 pts/0 S+ 00:10 0:00 ./test
yxm 33088 0.0 0.0 0 0 pts/0 Z+ 00:10 0:00 [test] <defunct>#僵尸进程
yxm 33125 0.0 0.0 7476 832 ? S 00:10 0:00 sleep 180
yxm 33180 0.0 0.1 37860 3420 pts/1 R+ 00:10 0:00 ps -aux
僵尸进程解决办法
方式一:僵尸进程的产生是因为父进程没有 wait() 子进程。所以如果我们自己写程序的话一定要,最好在父进程中通过 wait()
和 waitpid()
来避免僵尸进程的产生。
方式二:当系统中出现了僵尸进程时,我们是无法通过 kill 命令把它清除掉的。但是我们可以杀死它的父进程,让它变成孤儿进程,并进一步被系统中管理孤儿进程的进程收养并清理。具体步骤如下:
首先,需要确定僵尸进程的相关信息,比如父进程 ppid、僵尸进程的 pid 以及命令行等信息。可以执行如下命令:
ps -e -o stat,ppid,pid,cmd | egrep '^[Zz]'
参数说明:
- -e:参数用于列出所有的进程;
- -o:参数用于设定输出格式,这里只输出进程的stat(状态信息)、ppid(父进程pid)、pid(当前进程的pid),cmd(进程的可执行文件);
- egrep:是linux下的正则表达式工具:
- ‘^’:这是正则表达式,表示第一个字符的位置
- [Zz],表示 z 或者大写的 Z 字母,即表示第一个字符为 Z 或者 z 开头的进程数据,因为僵尸进程的状态信息以 Z 或者 z 字母开头。
然后,可以
kill -9 父进程 pid
。kill 之后,僵尸进程将被 init 进程收养并清理
【补充】现在大多数 linux 系统,会将僵尸进程标识为 defunct,所以也可以通过如下命令来获取僵尸进程信息:
ps -ef | grep "defunct"
总结
孤儿进程与僵尸进程是两种特殊的进程,一种是父进程先退出,子进程变成孤儿,这种进程没有危害;一种是子进程先退出,父进程没有回收资源导致子进程变成僵尸,会占用系统资源。他们都发生过在父子进程之间。