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


stackoverflow 上的一些类似问题:


stackoverflow 上的一些类似问题 视窗:

有帮助吗?

解决方案

孩子可以要求内核交付 SIGHUP (或其他信号)当父母通过指定选项死亡时 PR_SET_PDEATHSIGprctl() 像这样的系统调用:

prctl(PR_SET_PDEATHSIG, SIGHUP);

man 2 prctl 了解详情。

编辑:这仅适用于 Linux

其他提示

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

我和本页上的其他人得出了相同的结论——当父母去世时,没有一种与 POSIX 兼容的方式来通知孩子。所以我想出了第二个最好的办法——进行儿童民意调查。

当父进程终止(由于任何原因)时,子进程的父进程将成为进程 1。如果子进程只是定期轮询,它可以检查其父进程是否为 1。如果是的话,孩子应该退出。

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

我过去通过在“子”中运行“原始”代码和在“父”中运行“生成”代码(即:你颠倒了测试的通常意义 fork())。然后在“生成的”代码中捕获 SIGCHLD...

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

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

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 */

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

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

为了完整性。在 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);
}

子进程是否有往返于父进程的管道?如果是这样,写入时您会收到 SIGPIPE,读取时您会收到 EOF - 这些情况都可以被检测到。

在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,并在 fork 之后在子进程中测试它 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 - 它是所有进程的祖先。

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

受到另一个答案的启发,我想出了以下全 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)

我不认为可以保证仅使用标准 POSIX 调用。就像现实生活一样,孩子一旦诞生,就有了自己的生命。

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

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

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

有一种仅适用于 Linux 的方法 prctl(2) - 查看其他答案。

正如其他人指出的那样,依赖父进程 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)
    ;

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

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

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

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

这个解决方案对我有用:

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

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

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

一些海报已经提到了管道和 kqueue. 。事实上你也可以创建一对连接的 Unix 域套接字socketpair() 称呼。套接字类型应该是 SOCK_STREAM.

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

#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,否则它将直接退出。无论哪种情况,您都应该看到消息“child:家长挂了电话”。

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

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

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

也可以看看:

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

在下面 POSIX, , 这 exit(), _exit()_Exit() 函数定义为:

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

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

请注意,您可能需要阅读大量细则 - 包括基本定义(Definitions)部分,以及系统服务信息 exit()setsid()setpgrp() - 获得完整的图片。(我也会!)

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

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

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

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

(cat && kill 0) | python

如果您随后按 ^D,您将看到文本 "Terminated" 这表明 Python 解释器确实已被终止,而不是因为 stdin 被关闭而退出。

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

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

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

编辑130118:

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

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

如果父母去世,孤儿的 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

我找到了两个解决方案,但都不完美。

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 生成带有附加控制终端的进程。
当您通过任何方式杀死当前进程时,甚至杀死-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(.....);
    */

我通过滥用终端控制和会话,设法用 3 个进程制作了一个可移植的、非轮询的解决方案。这是精神自慰,但是有效。

诀窍是:

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

那样:

  • 如果进程 A 死亡:进程 B 收到 SIGPIPE 并终止
  • 如果进程 B 死亡:进程 A 的 wait() 返回并死亡,进程 C 收到 SIGHUP (因为当附加终端的会话的会话领导者死亡时,前台进程组中的所有进程都会收到 SIGHUP)
  • 如果进程 C 死亡:进程 B 收到 SIGCHLD 并终止,因此进程 A 终止

缺点:

  • 进程C无法处理SIGHUP
  • 进程C将在不同的会话中运行
  • 进程 C 不能使用会话/进程组 API,因为它会破坏脆弱的设置
  • 为每个此类操作创建一个终端并不是最好的主意

尽管已经过去了 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();
}

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

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

  • 首先,我们有可能改变 init 任何用户进程的进程,那么我们如何确保init进程始终是所有孤儿进程的父进程呢?嗯,在 exit 系统调用代码有一个显式检查来查看执行调用的进程是否是 init 进程(pid 等于 1 的进程),如果是这种情况,内核会发生恐慌(它应该无法再维护进程层次结构)所以不允许 init 进程执行 exit(2) 称呼。
  • 其次,上面公开的基本测试中存在竞争条件。历史上假设初始化进程的 id 是 1, ,但这不是 POSIX 方法所保证的,它声明(如其他响应中公开的)仅系统的进程 ID 被保留用于该目的。几乎没有 posix 实现可以做到这一点,并且您可以假设在原始的 unix 派生系统中具有 1 作为响应 getppid(2) 系统调用足以假设该进程是孤立进程。另一种检查方法是制作一个 getppid(2) 就在分叉之后,将该值与新调用的结果进行比较。这根本不适用于所有情况,因为两个调用都不是原子的,并且父进程可能会在 fork(2) 在第一个之前 getppid(2) 系统调用。过程parent id only changes once, when its parent does an退出(2)call, so this should be enough to check if the获取ppid(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 进程)

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

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

克隆(2), pid_命名空间(7), , 和 取消分享(2).

许可以下: CC-BY-SA归因
不隶属于 StackOverflow
scroll top