假设我有一个进程,它只生成一个子进程。现在,当父进程由于某种原因(正常或不正常,通过kill, ^C,断言失败或其他任何原因)退出时,我希望子进程死亡。如何正确地做到这一点?


关于stackoverflow的一些类似问题:

(前面问过)当父进程退出的时候,我怎么能让子进程退出呢? (稍后问)用fork()创建的子进程会在父进程被杀死时自动被杀死吗?


关于Windows的stackoverflow也有类似的问题:

如何在Windows中自动销毁子进程? 当父进程被杀死时,杀死子进程


当前回答

我认为不可能保证只使用标准POSIX调用。就像现实生活一样,一旦孩子被孕育出来,它就有了自己的生命。

父进程可以捕获大多数可能的终止事件,并尝试在此时终止子进程,但总有一些无法捕获。

例如,没有进程可以捕获SIGKILL。当内核处理这个信号时,它将杀死指定的进程,而不通知该进程。

扩展一下类比——唯一的另一种标准方式是,当孩子发现自己不再有父母时自杀。

使用prctl(2)有一种linux独有的方法——请参阅其他答案。

其他回答

另一种Linux特有的方法是在一个新的PID名称空间中创建父进程。然后它将是该名称空间中的PID 1,当它退出时,它的所有子节点将立即被SIGKILL杀死。

不幸的是,为了创建一个新的PID名称空间,您必须拥有CAP_SYS_ADMIN。但是,这种方法非常有效,在初始启动父进程之后不需要对父进程或子进程进行任何实际更改。

请参见clone(2)、pid_namespaces(7)和unshare(2)。

为了完整起见。在macOS上你可以使用kqueue:

void noteProcDeath(
    CFFileDescriptorRef fdref, 
    CFOptionFlags callBackTypes, 
    void* info) 
{
    // LOG_DEBUG(@"noteProcDeath... ");

    struct kevent kev;
    int fd = CFFileDescriptorGetNativeDescriptor(fdref);
    kevent(fd, NULL, 0, &kev, 1, NULL);
    // take action on death of process here
    unsigned int dead_pid = (unsigned int)kev.ident;

    CFFileDescriptorInvalidate(fdref);
    CFRelease(fdref); // the CFFileDescriptorRef is no longer of any use in this example

    int our_pid = getpid();
    // when our parent dies we die as well.. 
    LOG_INFO(@"exit! parent process (pid %u) died. no need for us (pid %i) to stick around", dead_pid, our_pid);
    exit(EXIT_SUCCESS);
}


void suicide_if_we_become_a_zombie(int parent_pid) {
    // int parent_pid = getppid();
    // int our_pid = getpid();
    // LOG_ERROR(@"suicide_if_we_become_a_zombie(). parent process (pid %u) that we monitor. our pid %i", parent_pid, our_pid);

    int fd = kqueue();
    struct kevent kev;
    EV_SET(&kev, parent_pid, EVFILT_PROC, EV_ADD|EV_ENABLE, NOTE_EXIT, 0, NULL);
    kevent(fd, &kev, 1, NULL, 0, NULL);
    CFFileDescriptorRef fdref = CFFileDescriptorCreate(kCFAllocatorDefault, fd, true, noteProcDeath, NULL);
    CFFileDescriptorEnableCallBacks(fdref, kCFFileDescriptorReadCallBack);
    CFRunLoopSourceRef source = CFFileDescriptorCreateRunLoopSource(kCFAllocatorDefault, fdref, 0);
    CFRunLoopAddSource(CFRunLoopGetMain(), source, kCFRunLoopDefaultMode);
    CFRelease(source);
}

我认为不可能保证只使用标准POSIX调用。就像现实生活一样,一旦孩子被孕育出来,它就有了自己的生命。

父进程可以捕获大多数可能的终止事件,并尝试在此时终止子进程,但总有一些无法捕获。

例如,没有进程可以捕获SIGKILL。当内核处理这个信号时,它将杀死指定的进程,而不通知该进程。

扩展一下类比——唯一的另一种标准方式是,当孩子发现自己不再有父母时自杀。

使用prctl(2)有一种linux独有的方法——请参阅其他答案。

在Linux下,你可以在子进程中安装父进程死亡信号,例如:

#include <sys/prctl.h> // prctl(), PR_SET_PDEATHSIG
#include <signal.h> // signals
#include <unistd.h> // fork()
#include <stdio.h>  // perror()

// ...

pid_t ppid_before_fork = getpid();
pid_t pid = fork();
if (pid == -1) { perror(0); exit(1); }
if (pid) {
    ; // continue parent execution
} else {
    int r = prctl(PR_SET_PDEATHSIG, SIGTERM);
    if (r == -1) { perror(0); exit(1); }
    // test in case the original parent exited just
    // before the prctl() call
    if (getppid() != ppid_before_fork)
        exit(1);
    // continue child execution ...

请注意,在fork之前存储父进程id,并在prctl()之后在子进程中测试它,消除了prctl()和调用子进程的退出之间的竞争条件。

还要注意,子进程的父进程死亡信号在新创建的子进程中被清除。它不受execve()的影响。

如果我们确定负责收养所有孤儿的系统进程PID为1,这个测试就可以简化:

pid_t pid = fork();
if (pid == -1) { perror(0); exit(1); }
if (pid) {
    ; // continue parent execution
} else {
    int r = prctl(PR_SET_PDEATHSIG, SIGTERM);
    if (r == -1) { perror(0); exit(1); }
    // test in case the original parent exited just
    // before the prctl() call
    if (getppid() == 1)
        exit(1);
    // continue child execution ...

但是,依赖于系统进程的初始化和PID 1是不可移植的。posix . 1的授权- 2008指定:

调用进程的所有现有子进程和僵尸进程的父进程ID应设置为实现定义的系统进程的进程ID。也就是说,这些进程应该由一个特殊的系统进程继承。

传统上,采用所有孤儿进程的系统进程是PID 1,即init -它是所有进程的祖先。

在像Linux或FreeBSD这样的现代系统上,另一个进程可能具有这个角色。例如,在Linux上,一个进程可以调用prctl(PR_SET_CHILD_SUBREAPER, 1)来将自己建立为继承其任何后代的所有孤儿的系统进程(参见Fedora 25上的一个例子)。

正如其他人指出的那样,当父进程退出时,依赖父进程pid变成1是不可移植的。不需要等待特定的父进程ID,只需要等待ID发生变化:

pit_t pid = getpid();
switch (fork())
{
    case -1:
    {
        abort(); /* or whatever... */
    }
    default:
    {
        /* parent */
        exit(0);
    }
    case 0:
    {
        /* child */
        /* ... */
    }
}

/* Wait for parent to exit */
while (getppid() != pid)
    ;

如果不想以全速轮询,可以根据需要添加微睡眠。

在我看来,这个选项比使用管道或依赖于信号更简单。