230

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


关于stackoverflow的一些类似问题:


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

4

24 回答 24

198

当父母死亡时,孩子可以通过在系统调用中SIGHUP指定选项来要求内核传递(或其他信号),如下所示:PR_SET_PDEATHSIGprctl()

prctl(PR_SET_PDEATHSIG, SIGHUP);

详情请参阅man 2 prctl

编辑:这是仅限 Linux

于 2008-11-12T16:12:14.440 回答
72

我正在尝试解决同样的问题,并且由于我的程序必须在 OS X 上运行,因此仅限 Linux 的解决方案对我不起作用。

我与此页面上的其他人得出了相同的结论——当父母去世时,没有一种与 POSIX 兼容的方式来通知孩子。所以我拼凑了下一个最好的事情——让孩子投票。

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

这不是很好,但它很有效,而且比本页其他地方建议的 TCP 套接字/锁定文件轮询解决方案更容易。

于 2010-01-10T01:18:08.007 回答
33

过去,我通过在“子”中运行“原始”代码和在“父”中运行“生成”代码来实现这一点(即:您在之后颠倒了测试的通常意义fork())。然后在“衍生”代码中捕获 SIGCHLD ......

在你的情况下可能是不可能的,但是当它起作用时很可爱。

于 2008-11-12T19:58:48.147 回答
30

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

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

close(pipes[0]); /* Close the reader end in the parent */

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

当父母死亡时——不管是什么原因——它都会关闭管道的末端。子 shell 将从读取中获取 EOF 并继续终止后台子进程。

于 2010-10-29T17:38:01.133 回答
29

在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 ...

但是,依靠该系统进程存在init并具有 PID 1 是不可移植的。POSIX.1-2008 规定

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

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

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

于 2016-04-29T18:32:29.930 回答
15

为了完整起见。在 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);
}
于 2011-06-26T15:38:14.707 回答
11

子进程是否有通往/来自父进程的管道?如果是这样,您将在写入时收到 SIGPIPE,或在读取时收到 EOF - 可以检测到这些条件。

于 2008-11-12T16:15:49.597 回答
11

受此处另一个答案的启发,我提出了以下全 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);
}

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

  • 如果你故意杀死中间进程,那么当父母死亡时,孩子不会被杀死。
  • 如果子进程在父进程之前退出,那么中间进程将尝试杀死原来的子进程 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)
于 2014-05-01T02:25:57.170 回答
9

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

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

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

扩展类比——唯一的其他标准方法是让孩子在发现自己不再有父母时自杀。

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

于 2008-11-12T15:45:14.133 回答
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)
    ;

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

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

于 2013-05-21T00:49:22.470 回答
6

这个解决方案对我有用:

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

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

于 2017-03-21T10:32:29.283 回答
5

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

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

于 2008-11-12T16:42:06.813 回答
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);
    }
}

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

与使用SIGPIPEhandler 的方法相比,这种方法不需要尝试write()调用。

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

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

也可以看看:

  • unix(7)Linux 手册页,用于Linux 上unix(4)的 FreeBSD ,,,。poll(2)socketpair(2)socket(7)
于 2013-03-13T04:55:00.473 回答
3

我认为一种快速而肮脏的方法是在孩子和父母之间创建一个管道。当父母退出时,孩子将收到一个 SIGPIPE。

于 2010-12-09T08:25:14.643 回答
2

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

不幸的是,为了创建一个新的 PID 命名空间,你必须拥有CAP_SYS_ADMIN. 但是,这种方法非常有效,并且除了父级的初始启动之外,不需要对父级或子级进行真正的更改。

请参阅clone(2)pid_namespaces(7)unshare(2)

于 2017-12-06T18:13:45.817 回答
1

POSIXexit()_exit()_Exit()函数定义为:

  • 如果进程是控制进程,则应向属于调用进程的控制终端的前台进程组中的每个进程发送 SIGHUP 信号。

因此,如果您将父进程安排为其进程组的控制进程,则子进程应在父进程退出时收到 SIGHUP 信号。我不确定当父母崩溃时会发生这种情况,但我认为确实如此。当然,对于非崩溃案例,它应该可以正常工作。

请注意,您可能必须阅读大量精美的印刷品 - 包括基本定义(定义)部分,以及和和的系统服务信息exit()-setsid()setpgrp()获得完整的图片。(我也会!)

于 2008-11-12T20:54:28.270 回答
1

如果您向 pid 0 发送信号,例如使用

kill(0, 2); /* SIGINT */

该信号被发送到整个进程组,从而有效地杀死了孩子。

您可以使用以下方法轻松测试它:

(cat && kill 0) | python

如果然后按 ^D,您将看到文本"Terminated"表明 Python 解释器确实已被杀死,而不是因为标准输入被关闭而退出。

于 2011-12-05T21:31:47.627 回答
1

如果它与其他任何人相关,当我在 C++ 的分叉子进程中生成 JVM 实例时,我能让 JVM 实例在父进程完成后正确终止的唯一方法是执行以下操作。如果这不是最好的方法,希望有人可以在评论中提供反馈。

1) 在启动 Java 应用程序之前,按照建议调用prctl(PR_SET_PDEATHSIG, SIGHUP)分叉子进程execv,并且

2) 向 Java 应用程序添加一个关闭钩子,直到其父 PID 等于 1 为止,然后执行一个 hard Runtime.getRuntime().halt(0). 轮询是通过启动一个运行ps命令的单独 shell 来完成的(请参阅:如何在 Java 中找到我的 PID 或在 Linux 上的 JRuby?)。

编辑 130118:

这似乎不是一个可靠的解决方案。我仍然在努力理解正在发生的细微差别,但在屏幕/SSH 会话中运行这些应用程序时,我有时仍然会遇到孤立的 JVM 进程。

我没有在 Java 应用程序中轮询 PPID,而是让关闭挂钩执行清理,然后像上面那样硬停止。waitpid然后,当需要终止所有内容时,我确保在生成的子进程上调用C++ 父应用程序。这似乎是一个更强大的解决方案,因为子进程确保它终止,而父进程使用现有引用来确保其子进程终止。将此与之前的解决方案进行比较,后者让父进程随时终止,并让子进程在终止之前尝试确定他们是否已成为孤儿。

于 2013-01-08T08:22:13.193 回答
0

我找到了 2 个解决方案,都不是完美的。

1.当收到SIGTERM信号时,通过kill(-pid)杀死所有的孩子。
显然,这个解决方案不能处理“kill -9”,但它确实适用于大多数情况并且非常简单,因为它不需要记住所有子进程。


    var childProc = require('child_process').spawn('tail', ['-f', '/dev/null'], {stdio:'ignore'});

    var counter=0;
    setInterval(function(){
      console.log('c  '+(++counter));
    },1000);

    if (process.platform.slice(0,3) != 'win') {
      function killMeAndChildren() {
        /*
        * On Linux/Unix(Include Mac OS X), kill (-pid) will kill process group, usually
        * the process itself and children.
        * On Windows, an JOB object has been applied to current process and children,
        * so all children will be terminated if current process dies by anyway.
        */
        console.log('kill process group');
        process.kill(-process.pid, 'SIGKILL');
      }

      /*
      * When you use "kill pid_of_this_process", this callback will be called
      */
      process.on('SIGTERM', function(err){
        console.log('SIGTERM');
        killMeAndChildren();
      });
    }

同样,如果您在某处调用 process.exit,您可以像上面那样安装“退出”处理程序。注意:Ctrl+C 和突然崩溃已经被操作系统自动处理为杀死进程组,这里不再赘述。

2.使用chjj/pty.js来生成带有控制终端的进程。
当您以任何方式杀死当前进程甚至 kill -9 时,所有子进程也将被自动杀死(由操作系统?)。我猜是因为当前进程持有终端的另一端,所以如果当前进程死亡,子进程将获得 SIGPIPE 所以死亡。


    var pty = require('pty.js');

    //var term =
    pty.spawn('any_child_process', [/*any arguments*/], {
      name: 'xterm-color',
      cols: 80,
      rows: 30,
      cwd: process.cwd(),
      env: process.env
    });
    /*optionally you can install data handler
    term.on('data', function(data) {
      process.stdout.write(data);
    });
    term.write(.....);
    */

于 2013-11-26T09:18:11.983 回答
0

通过滥用终端控制和会话,我设法用 3 个进程创建了一个可移植的、非轮询解决方案。

诀窍是:

  • 进程 A 启动
  • 进程 A 创建一个管道 P(并且从不从中读取)
  • 进程 A 派生到进程 B
  • 进程 B 创建一个新会话
  • 进程 B 为该新会话分配一个虚拟终端
  • 进程 B 安装 SIGCHLD 处理程序以在子退出时终止
  • 进程 B 设置一个 SIGPIPE 处理程序
  • 进程 B 分叉到进程 C
  • 进程 C 做它需要的任何事情(例如 exec()s 未修改的二进制文件或运行任何逻辑)
  • 进程 B 写入管道 P(并以这种方式阻塞)
  • 进程A在进程B上等待()并在它死亡时退出

那样:

  • 如果进程 A 死亡:进程 B 获得 SIGPIPE 并死亡
  • 如果进程 B 死了:进程 A 的 wait() 返回并死了,进程 C 得到一个 SIGHUP(因为当带有终端的会话的会话负责人死了,前台进程组中的所有进程都会得到一个 SIGHUP)
  • 如果进程 C 死了:进程 B 得到一个 SIGCHLD 并死了,所以进程 A 死了

缺点:

  • 进程 C 无法处理 SIGHUP
  • 进程 C 将在不同的会话中运行
  • 进程 C 不能使用会话/进程组 API,因为它会破坏脆弱的设置
  • 为每一个这样的操作创建一个终端并不是最好的主意
于 2016-08-21T16:22:06.457 回答
0

即使 7 年过去了,我也遇到了这个问题,因为我正在运行 SpringBoot 应用程序,该应用程序需要在开发期间启动 webpack-dev-server 并且需要在后端进程停止时将其终止。

我尝试使用Runtime.getRuntime().addShutdownHook,但它适用于 Windows 10,但不适用于 Windows 7。

我已将其更改为使用等待进程退出或InterruptedException似乎在两个 Windows 版本上都能正常工作的专用线程。

private void startWebpackDevServer() {
    String cmd = isWindows() ? "cmd /c gradlew webPackStart" : "gradlew webPackStart";
    logger.info("webpack dev-server " + cmd);

    Thread thread = new Thread(() -> {

        ProcessBuilder pb = new ProcessBuilder(cmd.split(" "));
        pb.redirectOutput(ProcessBuilder.Redirect.INHERIT);
        pb.redirectError(ProcessBuilder.Redirect.INHERIT);
        pb.directory(new File("."));

        Process process = null;
        try {
            // Start the node process
            process = pb.start();

            // Wait for the node process to quit (blocking)
            process.waitFor();

            // Ensure the node process is killed
            process.destroyForcibly();
            System.setProperty(WEBPACK_SERVER_PROPERTY, "true");
        } catch (InterruptedException | IOException e) {
            // Ensure the node process is killed.
            // InterruptedException is thrown when the main process exit.
            logger.info("killing webpack dev-server", e);
            if (process != null) {
                process.destroyForcibly();
            }
        }

    });

    thread.start();
}
于 2016-10-25T11:24:00.853 回答
0

从历史上看,从 UNIX v7 开始,进程系统通过检查进程的父 ID 来检测进程的孤儿。正如我所说,从历史上看,init(8)系统进程是一个特殊的进程,原因只有一个:它不会死。它不会死,因为处理分配新父进程 id 的内核算法取决于这个事实。当一个进程执行它的exit(2)调用(通过进程系统调用或通过外部任务向它发送信号等)时,内核将这个进程的所有子进程重新分配 init 进程的 id 作为它们的父进程 id。这导致了最简单的测试和最便携的方式来了解进程是否已成为孤儿。只需检查getppid(2)系统调用的结果,如果它是进程 idinit(2)进程然后进程在系统调用之前得到孤立。

这种方法出现了两个可能导致问题的问题:

  • 首先,我们有可能将init进程更改为任何用户进程,那么我们如何确保 init 进程始终是所有孤儿进程的父进程?好吧,在exit系统调用代码中,有一个明确的检查来查看执行调用的进程是否是 init 进程(pid 等于 1 的进程),如果是这种情况,内核就会崩溃(它应该不再能够维护进程层次结构),因此不允许 init 进程进行exit(2)调用。
  • 其次,上面暴露的基本测试中有一个竞争条件。Init process' id 在历史上被假定为1,但这不是 POSIX 方法所保证的,它声明(如在其他响应中公开的那样)仅为该目的保留系统的进程 id。几乎没有 posix 实现这样做,您可以假设在原始的 unix 派生系统中,具有系统调用的1响应getppid(2)足以假设该进程是孤立的。另一种检查方法是getppid(2)在 fork 之后进行 a 并将该值与新调用的结果进行比较。这并不是在所有情况下都有效,因为两个调用都不是原子的,并且父进程可能在第一次系统调用之后fork(2)和之前死亡。getppid(2)进程parent id only changes once, when its parent does an退出(2) call, so this should be enough to check if thegetppid(2)result changed between calls to see that parent process has exit. This test is not valid for the actual children of the init process, because they are always children ofinit(8)`,但是您可以安全地假设这些进程也没有父进程(除非您在系统中替换为 init 进程)
于 2017-03-08T08:51:34.073 回答
0

我已经将使用环境的父 pid 传递给子进程,然后定期检查 /proc/$ppid 是否存在于子进程中。

于 2020-05-19T23:01:57.170 回答
-1

如果 parent 死了,orphans 的 PPID 变为 1 - 你只需要检查你自己的 PPID。在某种程度上,这就是上面提到的轮询。这是外壳片:

check_parent () {
      parent=`ps -f|awk '$2=='$PID'{print $3 }'`
      echo "parent:$parent"
      let parent=$parent+0
      if [[ $parent -eq 1 ]]; then
        echo "parent is dead, exiting"
        exit;
      fi
}


PID=$$
cnt=0
while [[ 1 = 1 ]]; do
  check_parent
  ... something
done
于 2011-12-06T23:17:22.293 回答