linux下进程控制

我们的一个系统在父进程退出后子进程偶尔出现不能正常的退出问题,这篇文章就是记录解决这个问题的过程。在unix系统上我们通过fork函数产生一个新的进程,这个新产生的进程我们称为子进程,调用fork函数的进程则是父进程。

父进程获取子进程的状态

父进程有时需要获取子进程的状态,这可以实现一些有趣的功能,例如秒起。posix标准里提供了 waitpid函数,通过waitpid父进程可以获取特定pid进程的状态。这个函数的原型如下:

pid_t waitpid(pid_t pid, int *wstatus, int options)
    pid < -1 | 表示wait进程组id是pid绝对值这个组中的所有子进程
    Pid = -1  | 表示wait所有子进程
    Pid = 0  | 表示wait当前进程组中的所有子进程
    Pid  > 0  | 表示wait进程id等于pid的子进程

option参数默认填0就可以了, 如果所有进程都运行,函数默认会处于阻塞状态,如果有进程终止,则会返回终止进程的pid。如果没有任何子进程,该函数就会报错。进程的状态会保存在wstatus参数中,我们通过以下宏来查看

 WIFEXITED(wstatus)  进程正常终止返回真,可以进一步通过 WEXITSTATUS(wstatus)获取推出状态
 WIFSIGNALED(wstatus) 若为异常退出,返回真,这时可以通过 +(wstatus) 进一步获取让进程退出的信号
 WCOREDUMP(wstatus) 判断子进程是否产生了coredump文件。 

通过这个函数我们来看看如何实现简单的进程秒起,下面是示例代码:

    #include <cstdio>
    #include <cstdlib>
    #include <sys/wait.h>
    #include <sys/types.h>
    #include <unistd.h>

    int child_is_dead(){
        int stat;
        while(1) {
            pid_t pid = waitpid(0, &stat, 0);
            if(WIFEXITED(stat)) {
                printf("child :%d is dead, exit(%d) \n", pid, WEXITSTATUS(stat));
                return 1;
            } else if(WIFSIGNALED(stat)) {
                printf("child :%d is dead, recv sign:%d \n", pid, WTERMSIG(stat));
                return 1;
            }
        }
        return 0;
    }

    void start_worker() {
        printf("worker start, pid:%d , ppid:%d , gid:%d \n", getpid(), getppid(), getgid());
        sleep(10);
    }


    int main(int argc, char **argv) {
        while(1) {
            pid_t pid = fork();
            if(pid == 0){
                start_worker();
                return 0;
            } else if(pid > 0) {
                child_is_dead();
            } else {
                perror("fork error");
                break;
            }
        }
        return 0;
    }

运行时结果如下图所示,worker退出时,master进程总能感知到,并且重新拉起worker进程

process_alive

在上面的实现中有一个问题就是父进程异常退出后,子进程变成孤儿进程,不能及时退出。 程序重启后可能会出现多个worker进程,导致服务异常。

子进程如何感知父进程退出?

一种简单的方法是在master进程中捕捉导致进程退出的信号,然后在进程退出时向worker进程发送一个SIGTERM信号, 这种实现在大多数情况下都能很好的工作,但是我们发现当我们用 kill -9 master_pid,来杀死master进程,worker进程并不会退出。 而这里的原因很简单,SIGKILL 是两个不能被捕获的信号之一(另一个是SIGSTOP),系统收到这个信号后,会立即终止该进程。所以上面的处理方法在一些特定情况下会有问题。

另外一种思路,当master进程异常退出,worker进程就会变成孤儿进程,被系统的INIT进程给收养。此时如果我们通过getppid()来查询父进程id,会发现父进程id变成了1. 此时认为当前worker进程已经变成了孤儿进程,需要退出, 这种方法的缺点就是需要轮训父进程的id,效率较低。 类似的,还可以通过一个pipe 实现这样的功能。 首先介绍一下pipe(管道)。 pipe是linux下一种很基础也很古老的IPC形式,它只能用于父子进程或者兄弟进程之间进行通信。 并且只有pipe的读端(fd[0])存在的情况下, 向写端(fd[1])写入数据时才能成功,否则内核会触发SIGPIPE信号,我们可以捕捉SIGPIPE信号。 利用这一特性,我们也能及时的感知父进程的状态。 以下是示例代码,当父进程退出后,write操作会触发SIGPIPE信号,并引起worker终止执行:

    #include <cstdio>
    #include <cstdlib>
    #include <sys/wait.h>
    #include <sys/types.h>
    #include <unistd.h>
    #include <errno.h>
    
    int main(int argc, char **argv) {
        int pipe_fd[2];
        char w_buf[10];
    
        if(pipe(pipe_fd) < 0) {
            perror("pipe create error");
            return -1;
        }
    
        pid_t pid = fork();
        if(pid == 0){
            int ret = write(pipe_fd[1], w_buf, 4); 
            if(ret < 0) {
                perror("write to pipe error");
            }
        } else {
            sleep(1000);
        }
        return 0;
    }

这是一种相对较好,也比较通用的的方法,幸运的是,如果我们的程序之运行在linux平台中,则可以使用linux提供了一个函数prctl,函数原型如下:

   #include <sys/prctl.h>
   int prctl(int option, unsigned long arg2, unsigned long arg3,
             unsigned long arg4, unsigned long arg5);

这个系统调用是操作系统进程的相关参数的,功能受option配置。当 option设置成 PR_SET_PDEATHSIG 时,创建父进程退出时会向子进程发送一个信号,不过如果父进程有多个线程,当创建当前进程的线程退出时,就会触发这个信号。 下面是简化后的代码。

    #include <cstdlib>
    #include <sys/wait.h>
    #include <sys/types.h>
    #include <unistd.h>
    #include <sys/prctl.h>
    
    int main(int argc, char **argv) {
        pid_t pid = fork();
        if(pid == 0){
            prctl(PR_SET_PDEATHSIG, SIGKILL);
        } else {
            sleep(1000);
        }
        return 0;
    }

由于我们的程序只会运行在linux平台下, 最终我们采用这种prctl结束时触发SIGKILL信号来结束子进程,这种方案代码少,也更好维护。

发表于: 2018年12月8日 10时14分