贤菜
2022/02/18阅读:61主题:简
聊聊僵尸进程

僵尸进程预告
进程的世界如同电影中的僵尸,进程执行结束后,应该被销毁。但有时候我们的程序变成了僵尸进程--如同僵尸一样,虽然进程没有任何作用,但占用系统资源,给系统带来负担。我们应该掌握正确的方法,消灭僵尸进程。
1 背景
最近在维护分布式任务调度平台--xxl job平台,有业务方反馈线上出现了僵尸进程,为快速定位问题,不得不对相关知识进行复习。通过阅读本文,读者将会了解僵尸进程形成的原因以及解决方案。
2 复现
我们通过如下c语言,创建一个子进程,子进程很快执行结束,父进程sleep 30秒。
#include <stdio.h>
#include <unistd.h>
int main(int argc, char *argv[]) {
pid_t pid = fork();
if(pid == 0) {
printf("child process ID:%d \n", pid);
} else {
printf("child process ID:%d\n", pid);
sleep(30);
}
if (pid == 0) {
printf("end child ID:%d\n", pid);
} else {
printf("end parent process ID:%d\n", pid);
}
return 0;
}
编译后,执行编译代码命令 ./test 效果如下图所示。父进程id为69234,子进程的id为69235,子进程进入状态为Z(Marks a dead process,zombie)
3 原因
是操作系统将进程变为僵尸进程的。父进程在创建子进程后,子进程有两种方式传递值给父进程,即
-
通过return返回值返回 -
通过调用exit函数,exit中传递参数
子进程传值时,这些值会传给操作系统,但操作系统并不会将值主动传递给父进程,而是需要父进程主动通过函数调用来查询,如果父进程不来查询,操作系统将会一直保存该值,并让子进程处于僵尸进程的状态。
4 解决方案
4.1 通过wait函数
演示代码如下,父进程通过调用wait函数,接收子进程one的return 3,再wait接收子进程two的exit 7。
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <sys/wait.h>
int main(int argc, char *argv[]) {
int status;
pid_t pid = fork();
if (pid == 0) {
printf("I am child return 3\n");
return 3;
} else {
printf("child PID: %d\n", pid);
pid = fork();
if (pid == 0) {
printf("I am child exit 7\n");
exit(7);
} else {
printf("child PID:%d\n", pid);
wait(&status);
//WIFEXITED,子进程是否正常终止
if(WIFEXITED(status)) {
//WEXITSTATUS,返回子进程的返回值
printf("child send one: %d\n", WEXITSTATUS(status));
}
wait(&status);
if(WIFEXITED(status)){
printf("child send two: %d\n", WEXITSTATUS(status));
}
sleep(30);
}
}
return 0;
}
编译后,运行代码,没有发现僵尸进程(子进程很快退出,ps进程列表中没有发现子进程pid)。
4.2 通过waitpid函数
通过循环调用waitpid(相比wait,该函数不会阻塞),成功时返回终止的子进程id或0,失败返回-1,其中子进程sleep 30s,父进程每执行一次while循环sleep 3s,代码如下所示
#include <stdio.h>
#include <unistd.h>
#include <sys/wait.h>
int main(int argc, char *argv[]) {
int status;
pid_t pid=fork();
printf("pid:%d\n", pid);
if (pid == 0) {
sleep(30);
return 24;
} else {
int i = 1;
while(!waitpid(-1, &status, WNOHANG)) {
printf("%d time, parent sleep 3 seconds\n", i);
sleep(3);
i++;
}
if (WIFEXITED(status)){
printf("child send %d\n", WEXITSTATUS(status));
}
}
return 0;
}
编译后运行代码,结果如下,未发现僵尸进程。
4.3 利用信号处理
通过上面的代码,无论是wait,还是waitpid,都需要父进程等待,主动问询,但子进程何时终止,父进程并不知道。能不能父进程专注于自己的工作,子进程退出时再通知父进程来处理?求助于操作系统,是可以的。子进程终止时,会产生SIGCHLD信号,如果我们向操作系统注册该信号处理函数,就可以实现。
如下代码注册一个子进程退出时,需要运行的函数child_exit,父进程产生两个子进程,因此child_exit被执行两次。
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <signal.h>
#include <sys/wait.h>
void child_exit(int sig) {
int status;
pid_t pid =waitpid(-1, &status,WNOHANG);
if(WIFEXITED(status)) {
printf("process pid %d exit\n",pid);
printf("child send:%d\n",WEXITSTATUS(status));
}
}
int main(int argc, char *argv[]) {
pid_t pid;
struct sigaction act;
act.sa_handler= child_exit;
sigemptyset(&act.sa_mask);
act.sa_flags = 0;
//这里注册信号,有子进程退出时
//父进程被唤醒
sigaction(SIGCHLD, &act, 0);
pid =fork();
if(pid==0){
printf("child process one sleep 10 seconds, then return 12\n");
sleep(10);
return 12;
} else {
printf("parent, child one process pid:%d\n", pid);
pid = fork();
if (pid == 0) {
printf("child process two sleep 10 seconds, then exit 24\n");
sleep(10);
exit(24);
} else {
//父进程专注于自己的工作
//这里采用sleep
int i;
printf("parent, child two process pid:%d\n",pid);
for (i=0;i<5;i++){
sleep(5);
printf("parent wait for 5 seconds, i is %d\n", i);
}
}
}
return 0;
}
编译后运行效果如下图所示,没有产生僵尸进程。
5 后记
5.1 关于信号处理
信号处理有两个函数signal和sigaction,原因是signal在不同UNIX操作系统中可能存在区别,但sigaction是统一的。
5.2 常见信号
序号 | 信号 | 功能 |
---|---|---|
1 | SIGALRM | 到了通过alarm函数注册的时间 |
2 | SIGINT | 输入CTRL+C(这里明白了为什么有的进程CTRL+C后,进程无法退出) |
3 | SIGCHLD | 子进程终止 |
4 | SIGTERM | 进程终止 |
5 | SIGSTOP | 停止进程 |
5.3 区分孤儿进程
孤儿进程和僵尸进程是不同的,僵尸进程会浪费操作系统资源,但孤儿进程不会,孤儿进程是指父进程退出,由父进程产生的子进程被称为孤儿进程,这些进程将会把init(pid为1)的进程接管。
欢迎关注,我们一起交流
作者介绍