代码之家  ›  专栏  ›  技术社区  ›  Paweł Hajdan

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

  •  190
  • Paweł Hajdan  · 技术社区  · 17 年前

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


    关于stackoverflow的一些类似问题:


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

    23 回复  |  直到 9 年前
        1
  •  197
  •   qrdl    17 年前

    孩子可以要求内核交付 SIGHUP (或其他信号)通过指定选项在父级死亡时 PR_SET_PDEATHSIG prctl() 系统调用如下:

    prctl(PR_SET_PDEATHSIG, SIGHUP);

    man 2 prctl

        2
  •  71
  •   Schof    16 年前

    我正试图解决同样的问题,因为我的程序必须在OSX上运行,所以Linux唯一的解决方案对我不起作用。

    我得出了与本页其他人相同的结论——当父母去世时,没有一种POSIX兼容的方式通知孩子。所以我胡乱想了下一件最好的事情——让孩子投票。

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

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

        3
  •  33
  •   dmckee --- ex-moderator kitten    17 年前

    我在过去通过在“child”中运行“original”代码和在“parent”中运行“spoked”代码实现了这一点(也就是说:您在测试之后反转了通常意义上的测试) fork() ).然后在“繁殖”代码中捕获SIGCHLD。。。

        4
  •  30
  •   Phil Rutschman    5 年前

    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,并继续终止后台子进程。

        5
  •  26
  •   maxschlepzig    7 年前

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

    如果我们确信负责采用所有测试的系统流程 orphans 具有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 specifies :

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

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

    论现代制度 Linux FreeBSD 另一个进程可能具有这种作用。例如,在Linux上,进程可以调用 prctl(PR_SET_CHILD_SUBREAPER, 1) 将自身建立为继承其任何后代的所有孤儿的系统进程(参见 example

        6
  •  15
  •   neoneye    8 年前

    为了完整起见。在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);
    }
    
        7
  •  11
  •   MarkR    17 年前

    子进程与父进程之间是否有管道?如果是这样,您将在写入时收到SIGPIPE,或者在读取时收到EOF-这些情况都可以检测到。

        8
  •  11
  •   Greg Hewgill    12 年前

    受这里另一个答案的启发,我提出了以下所有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);
    }
    

    • 如果您故意杀死中间进程,那么当父进程死亡时,子进程将不会被杀死。

    另外,我使用的实际代码是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)
    
        9
  •  8
  •   Alnitak    17 年前

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

    信息技术 父进程可能捕获大多数可能的终止事件,并尝试在该点终止子进程,但总有一些无法捕获。

    例如,任何进程都无法捕获 SIGKILL . 当内核处理这个信号时,它将终止指定的进程,而不向该进程发出任何通知。

    延伸这个类比——唯一的其他标准方式是,当孩子发现自己不再有父母时自杀。

    prctl(2) -请参阅其他答案。

        10
  •  6
  •   user2168915    13 年前

    正如其他人指出的,当父pid退出时,依赖父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)
        ;
    

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

        11
  •  6
  •   joonas.fi    8 年前

    这个解决方案对我有效:

    • 将stdin管道传递给child-您不必向流中写入任何数据。
    • 这是一种简单方便的方法来检测父母何时离开。即使父级崩溃,操作系统也会关闭管道。

    这是针对工作进程类型的,其存在仅在父进程处于活动状态时才有意义。

        12
  •  5
  •   Ana Betts    17 年前

    安装 陷阱处理程序

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

        13
  •  5
  •   Cong Ma    13 年前

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

    假设您有两个套接字文件描述符fd1、fd2。现在 fork() poll() 剩余的开放fd位于其自身端,用于 POLLIN 事件只要双方不明确表示 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,否则它将简单地退出。无论哪种情况,您都应该看到消息“child:parent挂起”。

    与使用的方法进行了比较 SIGPIPE 处理程序,此方法不需要尝试 write() 呼叫

    对称的 ,即进程可以使用相同的通道来监视彼此的存在。

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

    • unix(7) Linux手册页的, unix(4) 对于FreeBSD, poll(2) socketpair(2) socket(7) 在Linux上。
        14
  •  3
  •   Yefei    15 年前

        15
  •  2
  •   Omnifarious    8 年前

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

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

    看见 clone(2) pid_namespaces(7) unshare(2)

        16
  •  1
  •   Jonathan Leffler    17 年前

    在下面 POSIX 这个 exit() , _exit() _Exit() 功能定义为:

    请注意,您可能需要阅读大量的细节,包括基本定义(Definitions)部分,以及系统服务信息 退出() setsid() setpgrp()

        17
  •  1
  •   Thorbiörn Fritzon    14 年前

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

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

    您可以通过以下方式轻松测试:

    (cat && kill 0) | python
    

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

        18
  •  1
  •   Community Mohan Dere    9 年前

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

    1) 召唤 prctl(PR_SET_PDEATHSIG, SIGHUP) execv

    2) 向Java应用程序添加一个shutdownhook,该应用程序将轮询直到其父PID等于1,然后执行一个硬操作 Runtime.getRuntime().halt(0) ps 命令(请参阅: How do I find my PID in Java or JRuby on Linux? ).

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

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

        19
  •  0
  •   Alexis Wilke    11 年前

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

    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”处理程序。

    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(.....);
        */
    
    
        20
  •  0
  •   Ido Ran    9 年前

    尽管7年过去了,但我在运行SpringBoot应用程序时遇到了这个问题,该应用程序需要在开发过程中启动webpack dev server,并且需要在后端进程停止时终止它。

    我试着用 Runtime.getRuntime().addShutdownHook 但它可以在Windows10上运行,但不能在Windows7上运行。

    我已将其更改为使用一个专用线程,该线程等待进程退出或退出 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();
    }
    
        21
  •  0
  •   Luis Colorado    9 年前

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

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

    • 首先,我们有可能改变现状 init 进程到任何用户进程,那么我们如何确保init进程始终是所有孤立进程的父进程呢?嗯,在 exit 系统调用代码有一个明确的检查,以查看执行调用的进程是否是init进程(pid等于1的进程),如果是这种情况,内核会崩溃(它应该不能再维护进程层次结构),因此不允许init进程执行 出口(2) 呼叫
    • 1 ,但POSIX方法并不保证这一点,即声明(在其他响应中公开)仅为该目的保留系统的进程id。几乎没有posix实现能做到这一点,您可以假设在原始的unix派生系统中 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 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 of init(8)`,但是您可以安全地假设这些进程也没有父进程(除非在系统中替换init进程)
        22
  •  0
  •   Sergei Kirjanov    6 年前

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

        23
  •  0
  •   Joundill    5 年前

    诀窍是:

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

    这样:

    • 如果进程A死亡:进程B获得SIGPIPE并死亡
    • 如果进程B终止:进程A的wait()返回并终止,进程C将获得一个SIGHUP(因为当连接终端的会话的会话头终止时,前台进程组中的所有进程都将获得SIGHUP)
    • 如果进程C死亡:进程B得到一个SIGCHLD并死亡,那么进程a死亡

    • 进程C将在不同的会话中运行
    • 进程C无法使用会话/进程组API,因为它将破坏脆弱的设置
    • 为每一个这样的操作创建一个终端并不是最好的主意
        24
  •  0
  •   mymedia    5 年前

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