父母退出后如何让孩子过程死亡?

时间:2008-11-12 15:37:51

标签: c linux unix process fork

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


stackoverflow上的一些类似问题:


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

24 个答案:

答案 0 :(得分:171)

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

prctl(PR_SET_PDEATHSIG, SIGHUP);

有关详细信息,请参阅man 2 prctl

编辑:这是仅限Linux的

答案 1 :(得分:65)

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

我得出了与此页面上其他人相同的结论 - 当父母去世时,没有与POSIX兼容的方式通知孩子。所以我把下一个最好的东西搞定了 - 让孩子进行民意调查。

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

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

答案 2 :(得分:34)

我过去通过运行“child”中的“原始”代码和“parent”中的“衍生”代码来实现这一目标(即:在{{1}之后反转通常的测试意义})。然后在“生成”代码中捕获SIGCHLD ......

在您的情况下可能无法实现,但在工作时可爱。

答案 3 :(得分: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 */

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

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

答案 4 :(得分: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);
}

答案 5 :(得分:11)

子进程是否有来自父进程的管道?如果是这样的话,你会在写作时收到一个SIGPIPE,或者在阅读时获得EOF - 这些条件都可以被检测到。

答案 6 :(得分:11)

在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()的影响。

如果我们确定负责采用所有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 - 它是所有过程的祖先。

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

答案 7 :(得分: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);
}

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

  • 如果你故意杀死中间过程,那么当父母去世时,孩子不会被杀死。
  • 如果孩子在父母之前退出,那么中间过程将尝试杀死原始子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)

答案 8 :(得分:7)

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

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

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

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

使用prctl(2)只有Linux的方式 - 请参阅其他答案。

答案 9 :(得分: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)
    ;

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

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

答案 10 :(得分:4)

安装陷阱处理程序以捕获SIGINT,如果它仍处于活动状态,它会终止你的子进程,尽管其他海报是正确的,它不会捕获SIGKILL。

打开一个具有独占访问权限的.lockfile并对其进行子项轮询,试图打开它 - 如果打开成功,则子进程应该退出

答案 11 :(得分:4)

这个解决方案对我有用:

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

这是一个工人类型的过程,只有在父母还活着的时候才有意义。

答案 12 :(得分:3)

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

答案 13 :(得分:3)

有些海报已经提到过管道和kqueue。实际上,您还可以通过socketpair()调用创建一对连接的 Unix域套接字。套接字类型应为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,或者只是退出。在任何一种情况下,您都应该看到消息“child:parent hung up”。

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

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

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

另见:

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

答案 14 :(得分:1)

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

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

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

编辑130118:

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

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

答案 15 :(得分:1)

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

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

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

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

(cat && kill 0) | python

如果你按^ D,你会看到文本"Terminated"表示Python解释器确实被杀死了,而不是因为stdin被关闭而退出。

答案 16 :(得分:1)

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

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

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

请注意,您可能需要阅读相当多的精细打印 - 包括基本定义(定义)部分,以及exit()setsid()以及{{1}的系统服务信息} - 获得完整的图片。 (我也是!)

答案 17 :(得分:0)

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

1.收到SIGTERM信号后,杀死所有孩子(-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生成您的流程并附带控制终端。
当你以任何方式杀死当前进程甚至杀死-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(.....);
    */

答案 18 :(得分:0)

如果父母死亡,孤儿的PPID变为1 - 您只需要检查自己的PPID。 在某种程度上,这是民意调查,如上所述。 这是shell的一部分:

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

答案 19 :(得分:0)

我设法通过滥用终端控制和会话来进行3个进程的便携式非轮询解决方案。这是精神手淫,但有效。

诀窍是:

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

那样:

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

缺点:

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

答案 20 :(得分: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();
}

答案 21 :(得分:0)

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

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

  • 首先,我们可以将init进程更改为任何用户进程,那么我们如何确保init进程始终是所有孤立进程的父进程?好吧,在exit系统调用代码中有一个显式检查,看看执行调用的进程是否是init进程(pid等于1的进程),如果是这种情况,内核会发生恐慌(它不应该能够继续维护流程层次结构)因此init进程不允许进行exit(2)调用。
  • 第二,上面暴露的基本测试中存在竞争条件。假设初始进程'id在历史上假定为1,但POSIX方法不保证这一点,它表明(在其他响应中公开)只保留系统的进程id用于此目的。几乎没有posix实现这样做,并且您可以假设在原始的unix派生系统中,1作为getppid(2)系统调用的响应足以假定该过程是孤立的。另一种检查方法是在fork之后生成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 getppid(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 of init(8)`,但你可以安全地假设这些进程没有父进程(除非你替换为系统初始化过程)

答案 22 :(得分:0)

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

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

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

答案 23 :(得分:-1)

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