linux子进程知道父进程退出的解决方案

在实际开发中难免会处理进程间的关系,最常见的是父子进程的相互监督。父进程等待子进程,或者自进程知道父进程运行是否结束,以方便释放资源。

一、关于进程

进程是操作系统进行资源分配和调度的基本单位。linux系统使用fork创建进程,进程pid 0是swapper进程,进程pid 1是init进程,init进程是所有普通用户进程的父进程。
fork在 文件中定义如下:
pid_t fork(void);
当fork成功时会返回两次,一次返回给父进程,一次返回给子进程。
如果fork调用成功,返回pid > 0 给父进程,返回pid == 0给子进程。
如果fork调用失败,返回pid == -1给父进程,并且设置errno。

二、父进程与子进程的关系

通过fork后,为什么父进程返回pid > 0,而子进程返回pid == 0呢?这是因为父进程无法知道自己有多少子进程,子进程创建后就独立于父进程了,无法知道子进程的id。子进程能够通过getppid获取父进程的pid。当父进程比子进程提前退出时,子进程会成为孤儿进程,系统会将重置孤儿进程的父进程到init进程ppid == 1。如果子进程提前退出并且父进程没有使用wait函数监听处理结束的子进程,那么该子进程会成为僵尸进程,即系统认为该进程存在,它占用的进程信息如进程pid等,会导致系统无法重复利用该pid号,可以这么理解僵尸进程就是名存实亡的进程,系统认为进程还存在,而子进程已经完成他的运行任务了。

三、简单的进程例程

simple_process.cpp
#include 
#include 
#include 
#include 
#include 
#include 
#include 

int main(int argc, char *argv[]) {
    printf("start program %s, pid: %ld, ppid: %ld \n", argv[0], getpid(), getppid());
    pid_t pid = fork();
    if (-1 == pid) {
        printf("fork process failed. errno: %u, error: %s\n", errno, strerror(errno));
        exit(-1);
    }   
    if (pid > 0) { // parent 
        printf("parent precess\n");
        sleep(1);
/** 标记一、wait 等待子进程结束*/
//      int status;
//      wait(&status);
    } else { // child 
        printf("child process, pid: %ld, ppid: %ld\n", getpid(), getppid());
        for (int i = 0; i < 5; i++) {
            printf("child sleep %ds\n", i); 
            sleep(1);
        }   
        printf("##child process, pid: %ld, ppid: %ld\n", getpid(), getppid());
    }   
    return 0;
}
编译运行
$ g++ simple_process.cpp
$ ./a.out
start program ./a.out, pid: 17164, ppid: 8564
parent precess
child process, pid: 17165, ppid: 17164
child sleep 0s
[sunny@icentos process-wait]$ child sleep 1s
child sleep 2s
child sleep 3s
child sleep 4s
##child process, pid: 17165, ppid: 1
可以看到,当父进程提前结束时,子进程的父进臣变化为init进程,ppid == 1。

四、实现父进程监听子进程运行结束

如果我们把“标志一”的wait代码删除注释,
即 使用下面代码
     /** 标记一、wait 等待子进程结束 */
     int status;
     wait(&status);
编译
$ g++ simple_process.cpp
$ ./a.out
start program ./a.out, pid: 28989, ppid: 8564
parent precess
child process, pid: 28990, ppid: 28989
child sleep 0s
child sleep 1s
child sleep 2s
child sleep 3s
child sleep 4s
##child process, pid: 28990, ppid: 28989
因为wait函数的作用是等待子进程结束,并获取子进程结束的状态,所以父进程wait等待子进程结束后才继续运行退出,故自进程的ppid一直是原来fork父进程的pid。因为wait函数是阻塞的,所以在实际开发中,需要监听子进程退出,可以新建线程来监听,并通过回调函数处理。

五、实现子进程监听父进程运行结束

fork创建进程以后子进程就成为独立的运行单位(进程是操作系统资源分配和调度的基本单位),虽然子进程会继承父进程原来的变量,但是子进程对这些变量的操作不会影响相应父进程变量的值等(这里只是指简单的int,long变量,如果是socket,file等共享资源那么还可能会影响)。子进程已经无法通过普通方式知道父进程的运行状态(正在运行还是运行结束,进程是否存活)。
子进程要知道父进程的状态只能通过进程通讯方法解决,常用的进程间通讯:匿名管道,有名管道,信号量,消息队列,信号,共享内存,套接字。
本文将介绍使用套接字实现父进程结束自动结束子进程的方式,主要运用两个特性。
1)socketpair会产生一个双向的句柄,两个句柄相当于通讯两端,程序能够通过一端写入,然后再另一端句柄读取内容。
2)进程运行退出时系统会自动回收资源,关闭该进程所占有的文件句柄,套接字句柄也是一种句柄。
代码实现
#include 
#include 
#include 
#include 
#include 
#include 
#include 
#include 
#include 

void * thr_child(void *arg) {
    int sock = *((int*)arg);
    char buf[2] = {0};
    ssize_t len = 0;
    while (-1 == (len = read(sock, buf, sizeof(buf))) && EINTR == errno);
    printf("thr_child. pid: %ld, ppid:%ld\n", getpid(), getppid());
    exit(-1 == len ? -1 : 0);
}

int main(int argc, char *argv[]) {
    printf("start program: %s, pid: %ld, ppid: %ld\n", argv[0], getpid(), getppid());
    int fd[2] = {0};
    if (-1 == socketpair(PF_LOCAL, SOCK_STREAM, 0, fd)) {
        perror("socketpair failed\n");
        exit(-1);
    }

    pid_t pid = fork();
    if (-1 == pid) {
        perror("fork failed\n");
        exit(-1);
    }
    if (0 == pid) { // child
        printf("child process, pid: %ld, ppid: %ld\n", getpid(), getppid());
        close(fd[0]);
        int sock = fd[1];

        pthread_t pid;
        if (pthread_create(&pid, NULL, thr_child, (void *)&sock)) {
            perror("child. create thread failed\n");
            exit(-1);
        }

        for (int i = 0; i < 20; i++) {
            sleep(1);
            printf("child sleep: %ds\n", i + 1);
        }
    } else { // parent
        printf("parent process, pid: %ld, ppid: %ld\n", getpid(), getppid());
        close(fd[1]);
        int sock = fd[0];
        for (int i = 0; i < 5; i++) {
            sleep(1);
            printf("parent sleep: %ds\n", i + 1);
        }
    }

    printf("process %s finish\n", pid > 0 ? "parent" : "child");
    return 0;
}
编译及运行
$ g++ -pthread notify_parent_quit.cpp
$ ./a.out
start program: ./a.out, pid: 31141, ppid: 8564
parent process, pid: 31141, ppid: 8564
child process, pid: 31142, ppid: 31141
parent sleep: 1s
child sleep: 1s
parent sleep: 2s
child sleep: 2s
parent sleep: 3s
child sleep: 3s
parent sleep: 4s
child sleep: 4s
parent sleep: 5s
process parent finish
child sleep: 5s
thr_child. pid: 31142, ppid:1
可以看到,如果使用wait函数,那么父进程会等待子进程sleep 20秒再自行结束。但是因为父进程结束时会关闭套接字句柄sock导致子进程read返回0表示达到文件结尾,因为父进程没有主动close套接字句柄sock,所以只有父进程退出时由操作系统关闭该句柄。上面的notify程序就是运用这点,在父进程结束时通知子进程的。通过线程函数thr_child可以知道,在线程退出前打印ppid时,子进程的父进程ppid == 1,即由于父进程结束导致子进程成为孤儿进程。
总结、socketpair只是其中一种简单的实现方式。这里只是使用简单的例子说明,在实际应用中处理的情况会比较复杂。

你可能感兴趣的:(C,C++语言开发)