贤菜

V1

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)的进程接管。

欢迎关注,我们一起交流

分类:

后端

标签:

后端

作者介绍

贤菜
V1