Linux系统中,系统提供一种机制,可以让父进程得以了解子进程的当前状态,当一个进程结束时,内核会释放该进程申请的所有资源,如占用的内存,打开的文件等,但进程自身的一些注册信息仍会被保留,如进程ID,退出状态,运行时间等等,当父进程使用wait或waitpid获取了子进程状态时,这些信息才会被完全释放掉,而直到这时,该进程的信息才被完全清除掉。进程退出,但其注册信息未被父进程获取时,我们就称为僵尸状态,其进程也变成了僵尸进程。
僵尸进程对系统的危害,不仅仅是一部分内存无法释放,要知道,Linux系统是限制打开的总进程数的,大量僵尸进程不断吞噬可用进程空间,最终系统将无法运行新进程,因此,如何防止僵尸进程的出现,是我们必须要了解的。
在此需要先说明一下Linux系统中init进程的作用,当一个父进程先于子进程退出时,子进程的信息将会被转交到init进程下做统一管理,init进程的作用,相当于就是不断地获取子进程的注册信息,以完成清理注册信息的目的,这个进程在我们清理僵尸进程的关键。
针对僵尸进程的出现条件,我们使用C语言进行场景出现。
zombie1.c #include <stdio.h> #include <stdlib.h> int main(void) { pid_t pid; pid = fork(); if (pid < 0) { perror("fork error!\n"); exit(1); } else if (pid == 0) { exit(0); } sleep(10); exit(0); }
将此文件进行编绎并在后台执行,通过查询进程状态,我们来观察僵尸进程的存在
# gcc -o zombie1 zombie1.c # ./zombie1 & # ps aux | grep zombie1 (上一命令结束后立即执行) root 1383 0.0 0.0 4164 344 pts/0 S 17:22 0:00 ./zombie1 root 1384 0.0 0.0 0 0 pts/0 Z 17:22 0:00 [zombie1] <defunct> root 1386 0.0 0.0 112660 964 pts/0 S+ 17:22 0:00 grep --color=auto zombie1 # ps aux | grep zombie1 (等待10秒以后执行) root 1388 0.0 0.0 112660 964 pts/0 S+ 17:22 0:00 grep --color=auto zombie1 [1]+ Done ./zombie1
从以上执行结果可以看出,第一次获取进程状态时,因为子进程已经结束,而父进程尚未执行wait操作,导致子进程的注册信息没有清除,因此该子进程显示为僵尸进行(状态为Z),第二次获取进程状态时,父进程已经执行了wait操作,子进程的信息已经消失,不再做为僵尸进程存在。
在我们进程开发过程中,可能会出现父进程长时间不消亡,而子进程已结束的情况,这时,避免子进程成为僵尸进程,一般有2种办法。
1.应用SIGCHILD信号进行处理
zombie2.c #include <stdio.h> #include <stdlib.h> #include <signal.h> static void clear_child(int); int main(void) { pid_t pid; signal(SIGCHLD, clear_child); pid = fork(); if (pid < 0) { perror("fork error!\n"); exit(1); } else if (pid == 0) { exit(0); } sleep(2); // 当子进程消亡时,SIGCHLD信号将唤醒父进程,从而结束此sleep sleep(10); exit(0); } static void clear_child(int signo) { wait(NULL); }
将此文件进行编绎并在后台执行,通过查询进程状态,我们来观察僵尸进程的存在
# gcc -o zombie2 zombie2.c # ./zombie2 & # ps aux | grep zombie2 (上一命令结束后立即执行) root 1839 0.0 0.0 4164 348 pts/0 S 19:14 0:00 ./zombie2 root 1842 0.0 0.0 112660 964 pts/0 R+ 19:14 0:00 grep --color=auto zombie2 # ps aux | grep zombie2 (等待12秒以后执行) root 1844 0.0 0.0 112660 964 pts/0 S+ 19:14 0:00 grep --color=auto zombie2 [1]+ Done ./zombie2
从以上执行结果可以看出,第一次获取进程状态时,因为子进程已经结束,而父进程因为SIGCHILD信号触发的缘故,从2秒休眠中唤醒,执行了wait操作,因此僵尸进程已经清除,只余下主进程。第二次获取进程状态时,父进程的第2个10秒休眠已经结束,全部相关进程已经清除。
2.fork两次,消除僵尸进程
zombie3.c #include <stdio.h> #include <stdlib.h> int main(void) { pid_t pid; pid = fork(); if (pid < 0) { perror("fork error!\n"); exit(1); } else if (pid == 0) { pid = fork(); if (pid < 0) { perror("fork error!\n"); exit(1); } else if (pid == 0) { sleep(5); exit(0); } exit(0); } waitpid(pid, NULL, 0); sleep(10); // 睡眠期间,两个子进程均已消亡 exit(0); }
将此文件进行编绎并在后台执行,通过查询进程状态,我们来观察僵尸进程的存在
# gcc -o zombie3 zombie3.c # ./zombie3 & # ps aux | grep zombie3 (上一命令结束后立即执行) root 1861 0.0 0.0 4164 344 pts/0 S 19:24 0:00 ./zombie3 root 1863 0.0 0.0 4164 84 pts/0 S 19:24 0:00 ./zombie3 root 1865 0.0 0.0 112660 968 pts/0 R+ 19:24 0:00 grep --color=auto zombie3 # ps aux | grep zombie3 (等待7秒以后执行) root 1861 0.0 0.0 4164 344 pts/0 S 19:24 0:00 ./zombie3 root 1869 0.0 0.0 112660 968 pts/0 R+ 19:24 0:00 grep --color=auto zombie3 # ps aux | grep zombie3 (等待10秒以后执行) root 1875 0.0 0.0 112660 964 pts/0 R+ 19:24 0:00 grep --color=auto zombie3 [1]+ Done ./zombie3
从以上执行结果可以看出,第一次获取进程状态时,因为子进程已经结束,而父进程也执行了wait操作,因此子进程已经清除不可见,只剩下父进程和孙进程,第二次获取进程状态时,孙进程已经消亡,只剩下了父进程。第三次获取进程状态时,全部相关进程都已经清除。