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

zxlwwiss  于 2022-09-21  发布在  Unix
关注(0)|答案(23)|浏览(682)

假设我有一个进程,它正好产生一个子进程。现在,当父进程出于任何原因(正常或异常,通过KILL、^C、Assert Failure或任何其他原因)退出时,我希望子进程死亡。如何正确地做到这一点呢?

有关堆栈溢出的一些类似问题:

关于Windows堆栈溢出的一些类似问题:

ruoxqz4g

ruoxqz4g1#

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

假设您有两个套接字文件描述符fd1、fd2。现在fork()创建子进程,它将继承FDS。在父进程中关闭fd2,在子进程中关闭fd1。现在,对于POLLIN事件,每个进程可以在其自己的一端poll()剩余的开放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 = %dn", getpid());
        sleep(100);
        exit(0);

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

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

        poll(&mon, 1, -1);
        if ( mon.revents & POLLHUP )
            fprintf(stderr, "child: parent hung upn");
        exit(0);
    }
}

您可以尝试编译上面的概念验证代码,并在./a.out &这样的终端上运行它。您有大约100秒的时间来尝试通过各种信号终止父PID,否则它将直接退出。在这两种情况下,您都应该看到消息“CHILD:PARENT HOUND”。

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

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

此解决方案仅调用POSIX函数。我在Linux和FreeBSD上尝试了这一点。我认为它应该可以在其他制服上工作,但我还没有真正测试过。

另见:

  • Linux手册页的unix(7)、适用于FreeBSD的unix(4)、Linux上的poll(2)socketpair(2)socket(7)
6rvt4ljy

6rvt4ljy2#

通过滥用终端控制和会话,我设法用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,因为它会破坏脆弱的设置
  • 为每一次这样的操作创建一个航站楼并不是最好的想法
wsxa1bj1

wsxa1bj13#

我将使用环境的父Pid传递给子进程,然后定期检查子进程是否存在/proc/$PPID。

vsikbqxv

vsikbqxv4#

从历史上看,从Unix v7开始,进程系统通过检查进程的父ID来检测进程的孤立性。正如我所说的,从历史上看,init(8)系统进程是一个特殊的进程,原因只有一个:它不会消亡。它不会死,因为处理分配新的父进程ID的核心算法依赖于这一事实。当进程执行其exit(2)调用时(通过进程系统调用或通过向其发送信号等的外部任务),内核将该进程的所有子进程重新分配初始进程的ID作为其父进程ID。这导致了最简单、最可移植的知道进程是否已成为孤立进程的方法。只需检查getppid(2)系统调用的结果,如果它是init(2)进程的进程ID,则该进程在系统调用之前成为孤立进程。

这种方法会产生两个可能导致问题的问题:

  • 首先,我们有可能将init进程更改为任何用户进程,那么如何确保init进程始终是所有孤立进程的父进程?在exit系统调用代码中,有一个显式检查来查看执行调用的进程是否是init进程(id等于1的进程),如果是这样的话,内核死机(它应该不再能够维护进程层次结构),因此不允许init进程执行exit(2)调用。
  • 第二,上面曝光的基本测试中存在竞争条件。Init进程的id在历史上被假定为1,但POSIX方法并不保证这一点,它声明(在其他响应中暴露的)只有系统的进程id被保留用于该目的。几乎没有POSIX实现可以做到这一点,并且您可以假设在原始的Unix派生系统中,拥有1作为getppid(2)系统调用的响应就足以假定该进程是孤立的。另一种检查方法是在分叉之后创建一个getppid(2),并将该值与新调用的结果进行比较。这并不是在所有情况下都有效,因为两个调用并不是原子的,父进程可能在fork(2)之后和第一个getppid(2)系统调用之前死亡。Processparent id only changes once, when its parent does anexit(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进程)
tgabmvqs

tgabmvqs5#

尽管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();
}
rnmwe5a2

rnmwe5a26#

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

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,您可以像上面那样安装‘Exit’处理程序。注:Ctrl+C和突然崩溃已被操作系统自动处理以杀死进程组,因此此处不再赘述。

2.使用chjj/pty.js派生您的进程,并附加控制终端。

当你用KILL-9杀死当前进程时,所有子进程也会被自动杀死(被OS?)。我猜是因为当前进程持有终端的另一边,所以如果当前进程死了,子进程将得到SIGPIPE So Death。

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(.....);
    */
jucafojl

jucafojl7#

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

1)在通过execv启动Java应用程序之前,按照建议在派生子进程上调用prctl(PR_SET_PDEATHSIG, SIGHUP),以及

2)向Java应用程序添加一个关闭钩子,该钩子轮询直到其父进程ID等于1,然后执行硬Runtime.getRuntime().halt(0)。轮询是通过启动一个单独的外壳来完成的,该外壳运行ps命令(参见:How do I find my PID in Java or JRuby on Linux?)。

编辑130118:

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

我没有在Java应用程序中轮询PPID,而是简单地让Shutdown钩子执行清理,然后如上所述进行硬停止。然后,我确保在该终止一切的时候,在派生的子进程上的C++父应用程序中调用waitpid。这似乎是一个更健壮的解决方案,因为子进程确保它终止,而父进程使用现有引用来确保其子进程终止。将这与前一个解决方案进行比较,前一个解决方案让父进程随时终止,并让子进程尝试在终止之前确定它们是否已成为孤儿。

9fkzdhlc

9fkzdhlc8#

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

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

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

您可以使用如下内容轻松测试它:

(cat && kill 0) | python

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

crcmnpdw

crcmnpdw9#

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

  • 如果进程为控制进程,则应向呼叫进程所属的控制终端前台进程组中的每个进程发送SIGHUP信号。

因此,如果将父进程安排为其进程组的控制进程,则当父进程退出时,子进程应该会收到SIGHUP信号。我不完全确定当父母崩溃时会发生这种情况,但我认为会发生。当然,对于非崩盘情况,它应该运行得很好。

请注意,您可能需要阅读大量详细的印刷品--包括基本定义(Definitions)部分,以及exit()setsid()setpgrp()的系统服务信息--才能全面了解情况。(我也是!)

cgyqldqp

cgyqldqp10#

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

遗憾的是,为了创建新的PID名称空间,您必须拥有CAP_SYS_ADMIN。但是,这种方法是非常有效的,除了父对象的初始启动之外,不需要对父对象或子对象进行真正的更改。

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

sd2nnvve

sd2nnvve11#

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

lx0bsm1f

lx0bsm1f12#

正如其他人所指出的,依赖父进程ID在父进程退出时变为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)
    ;

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

在我看来,这种选择似乎比使用管道或依靠信号更简单。

vnjpjtjt

vnjpjtjt13#

通过在prctl() syscall中指定选项PR_SET_PDEATHSIG,子进程可以在父进程死亡时请求内核发送SIGHUP(或其他信号):

prctl(PR_SET_PDEATHSIG, SIGHUP);

详情见man 2 prctl

编辑:这是仅用于Linux的

avwztpqn

avwztpqn14#

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

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

vbopmzt1

vbopmzt115#

这个解决方案对我很管用:

  • 将stdin管道传递给子对象-您不必向流中写入任何数据。
  • 儿童从标准输入到EOF无限期阅读。EOF发出父母已经离开的信号。
  • 这是一种万无一失的便携方式,可以检测父母何时离开。即使Parent崩溃,OS也会关闭管道。

这是对于工作者类型的进程,其存在只有在父进程活着的情况下才有意义。

相关问题