父母退出后如何使子进程死亡?

Paw*_*dan 199 c unix linux fork process

假设我有一个只产生一个子进程的进程.现在,当父进程因任何原因(正常或异常,通过kill,^ C,断言失败或其他任何原因)退出时,我希望子进程死掉.如何正确地做到这一点?


stackoverflow上的一些类似问题:


关于Windows的 stackoverflow的一些类似问题:

qrd*_*rdl 180

SIGHUP通过PR_SET_PDEATHSIGprctl()syscall中指定选项,子进程可以让内核传递(或其他信号),如下所示:

prctl(PR_SET_PDEATHSIG, SIGHUP);

详情man 2 prctl请见.

编辑:这只是Linux

  • 把答案称为穷人并不是很好 - 即使它没有解决竞争条件.关于如何在无竞争条件下使用`prctl()`,请参阅[我的回答](http://stackoverflow.com/a/36945270/427158).顺便说一下,Maxim的答案是不正确的. (14认同)
  • 这是一个糟糕的解决方案,因为父母可能已经去世了。比赛条件。正确的解决方案:http://stackoverflow.com/a/17589555/412080 (5认同)
  • 这只是一个错误的anser.它将在调用fork的线程死亡时将信号发送到子进程,而不是在父进程终止时. (4认同)
  • @Lothar看到某种证据会很高兴.`男人prctl`说:设置调用进程的父进程死亡信号到arg2的(无论是在范围1..maxsig的信号值,或者0来清除).这是调用进程在其父级死亡时将获得的信号.执行set-user-ID或set-group-ID二进制文件时,将为fork(2)的子项和(自Linux 2.4.36/2.6.23)清除此值. (2认同)

Sch*_*hof 66

我正在尝试解决同样的问题,因为我的程序必须在OS X上运行,所以Linux专用解决方案对我不起作用.

我得出了与此页面上其他人相同的结论 - 当父母去世时,没有与POSIX兼容的方式通知孩子.因此,我完成了下一个最好的事情 - 让孩子进行民意调查.

当父进程死亡(由于任何原因)时,子进程的父进程变为进程1.如果子进程只是定期轮询,则可以检查其父进程是否为1.如果是,则该进程应退出.

这不是很好,但它可以工作,并且比本页其他地方建议的TCP套接字/锁定文件轮询解决方案更容易.

  • 只是为了获取信息,在Solaris上,如果你在一个区域,`gettpid()`不会变为1,而是获得区域调度程序的`pid`(进程`zsched`). (10认同)
  • 优秀的解决方 继续调用getppid()直到它返回1然后退出.这很好,我现在也用它.一个非pollig解决方案会很好.谢谢Schof. (6认同)
  • 如果有人想知道,在Android系统中,当父母去世时,pid似乎是0(进程系统pid)而不是1. (4认同)
  • 要在fork()之前有一个更强大且独立于平台的方法,只需要getpid(),如果来自child的getppid()不同,则退出. (2认同)
  • 如果您不控制子进程,则此方法不起作用。例如,我正在处理包装find(1)的命令,并且我想确保如果包装由于某种原因死亡,则查找被杀死。 (2认同)

dmc*_*kee 34

我在过去通过运行"child"中的"原始"代码和"parent"中的"衍生"代码(即:你反转了通常的测试意识fork())来实现了这一点.然后在"生成"代码中捕获SIGCHLD ...

在你的情况下可能不可能,但它可行时很可爱.

  • 在父进程中完成工作的巨大问题是您正在更改父进程。如果服务器必须“永远”运行,那么这不是一个选择。 (2认同)

小智 29

如果您无法修改子进程,可以尝试以下操作:

int pipes[2];
pipe(pipes)
if (fork() == 0) {
    close(pipes[1]); /* Close the writer end in the child*/
    dup2(0, pipes[0]); /* Use reader end as stdin */
    exec("sh -c 'set -o monitor; child_process & read dummy; kill %1'")
}

close(pipes[0]); /* Close the reader end in the parent */
Run Code Online (Sandbox Code Playgroud)

这将在启用了作业控制的shell进程中运行子进程.子进程在后台生成.shell等待换行符(或EOF)然后杀死孩子.

当父母去世时 - 无论是什么原因 - 它将关闭管道的末端.子shell将从读取中获得EOF并继续杀死后台子进程.

  • 很好,但是有五个系统调用,并且产生了十行代码让我对这段代码性能有点怀疑. (2认同)

neo*_*eye 14

为了完整起见.在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);
}
Run Code Online (Sandbox Code Playgroud)


max*_*zig 12

在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 ...
Run Code Online (Sandbox Code Playgroud)

请注意,在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 ...
Run Code Online (Sandbox Code Playgroud)

init然而,依赖于系统进程并且具有PID 1是不可移植的.POSIX.1-2008规定:

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

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

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

  • 不幸的是,如果一个子进程从一个线程分叉,然后该线程退出,子进程将获得 SIGTERM。 (2认同)

Mar*_*rkR 11

子进程是否有来自父进程的管道?如果是这样,如果写入,您将收到一个SIGPIPE,或者在阅读时获得EOF - 可以检测到这些条件.


Gre*_*ill 10

受到另一个答案的启发,我想出了以下全POSIX解决方案.一般的想法是在父级和子级之间创建一个中间过程,其目的有一个:当父级死亡时注意,并明确地杀死子级.

当无法修改子代码时,此类解决方案很有用.

int p[2];
pipe(p);
pid_t child = fork();
if (child == 0) {
    close(p[1]); // close write end of pipe
    setpgid(0, 0); // prevent ^C in parent from stopping this process
    child = fork();
    if (child == 0) {
        close(p[0]); // close read end of pipe (don't need it here)
        exec(...child process here...);
        exit(1);
    }
    read(p[0], 1); // returns when parent exits for any reason
    kill(child, 9);
    exit(1);
}
Run Code Online (Sandbox Code Playgroud)

这种方法有两个小注意事项:

  • 如果你故意杀死中间过程,那么当父母去世时孩子不会被杀死.
  • 如果子进程在父进程之前退出,则中间进程将尝试终止原始子进程pid,现在可以引用不同的进程.(这可以通过中间过程中的更多代码来修复.)

顺便说一句,我使用的实际代码是Python.这是为了完整性:

def run(*args):
    (r, w) = os.pipe()
    child = os.fork()
    if child == 0:
        os.close(w)
        os.setpgid(0, 0)
        child = os.fork()
        if child == 0:
            os.close(r)
            os.execl(args[0], *args)
            os._exit(1)
        os.read(r, 1)
        os.kill(child, 9)
        os._exit(1)
    os.close(r)
Run Code Online (Sandbox Code Playgroud)

  • 我认为你的第二个警告是错误的。子进程的 pid 是属于其父进程的资源,在父进程(中间进程)等待它(或终止并让 init 等待它)之前,它无法被释放/重用。 (3认同)

Aln*_*tak 7

我不相信可以保证只使用标准POSIX调用.就像现实生活一样,一旦孩子产生,它就有自己的生命.

可能的父进程捕捉最有可能终止事件,并试图在该点杀子过程,但总有一些不能被捕获.

例如,没有任何流程可以捕获SIGKILL.当内核处理此信号时,它将终止指定的进程,而不会通知该进程.

扩展类比 - 唯一的另一种标准方法是让孩子在发现自己不再拥有父母时自杀.

有一种仅限Linux的方式prctl(2)- 请参阅其他答案.


小智 6

正如其他人所指出的那样,当父母退出时,依靠父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)
    ;
Run Code Online (Sandbox Code Playgroud)

如果您不想全速轮询,请根据需要添加微睡眠.

这个选项对我来说比使用管道或依赖信号更简单.

  • 全速投票是疯狂的. (2认同)

joo*_*.fi 6

这个解决方案对我有用:

  • 将 stdin 管道传递给 child - 您不必将任何数据写入流中。
  • Child 无限期地从 stdin 读取直到 EOF。EOF 表示父母已经离开。
  • 这是检测父母何时离开的万无一失且便携的方式。即使父级崩溃,操作系统也会关闭管道。

这是一个工人类型的进程,它的存在只有在父进程活着时才有意义。


Ana*_*tts 5

安装一个陷阱处理程序来捕获 SIGINT,如果它仍然活着,它会杀死你的子进程,尽管其他海报是正确的,它不会捕获 SIGKILL。

打开具有独占访问权限的 .lockfile 并让子轮询尝试打开它 - 如果打开成功,子进程应该退出


Con*_* Ma 5

一些海报已经提到了管道和kqueue. 实际上,您还可以通过调用创建一对连接的Unix 域套接字socketpair()。套接字类型应该是SOCK_STREAM.

让我们假设您有两个套接字文件描述符 fd1、fd2。现在fork()创建子进程,它将继承 fds。在父级中关闭 fd2,在子级中关闭 fd1。现在每个进程都可以poll()在自己的一端为POLLIN事件打开剩余的 fd 。只要每一方close()在正常生命周期内没有明确地表明它的 fd,你就可以相当确定一个POLLHUP标志应该表明对方的终止(无论是否干净)。收到此事件的通知后,孩子可以决定做什么(例如死亡)。

#include <unistd.h>
#include <stdlib.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <poll.h>
#include <stdio.h>

int main(int argc, char ** argv)
{
    int sv[2];        /* sv[0] for parent, sv[1] for child */
    socketpair(AF_UNIX, SOCK_STREAM, 0, sv);

    pid_t pid = fork();

    if ( pid > 0 ) {  /* parent */
        close(sv[1]);
        fprintf(stderr, "parent: pid = %d\n", getpid());
        sleep(100);
        exit(0);

    } else {          /* child */
        close(sv[0]);
        fprintf(stderr, "child: pid = %d\n", getpid());

        struct pollfd mon;
        mon.fd = sv[1];
        mon.events = POLLIN;

        poll(&mon, 1, -1);
        if ( mon.revents & POLLHUP )
            fprintf(stderr, "child: parent hung up\n");
        exit(0);
    }
}
Run Code Online (Sandbox Code Playgroud)

您可以尝试编译上面的概念验证代码,然后在像./a.out &. 您有大约 100 秒的时间来尝试通过各种信号杀死父 PID,否则它将直接退出。在任何一种情况下,您都应该看到消息“孩子:父母挂断电话”。

与使用SIGPIPE处理程序的方法相比,此方法不需要尝试write()调用。

这种方法也是对称的,即进程可以使用相同的通道来监视彼此的存在。

此解决方案仅调用 POSIX 函数。我在 Linux 和 FreeBSD 中尝试过这个。我认为它应该适用于其他 Unix,但我还没有真正测试过。

也可以看看:

  • unix(7)的 Linux 手册页,unix(4)用于 FreeBSD poll(2)、、socketpair(2)socket(7)在 Linux 上。