父母退出后如何让孩subprocess死亡?

假设我有一个产生一个subprocess的进程。 现在,当父进程出于任何原因(通常或exception,通过kill,^ C,声明失败或其他),父进程退出,我希望subprocess死亡。 如何正确地做到这一点?


一些类似的问题在stackoverflow:

  • (之前问过) 当父进程执行时,如何导致subprocess退出?
  • (稍后问) 是否使用fork()创build的subprocess在父进程被终止时自动终止?

Windows上的一些类似的问题在stackoverflow:

  • 如何在Windows中自动销毁subprocess?
  • 当父进程被终止时,终止subprocess

子系统可以通过在prctl()系统调用中指定选项PR_SET_PDEATHSIG来请求内核在父PR_SET_PDEATHSIG中传递SIGHUP (或其他信号),如下所示:

prctl(PR_SET_PDEATHSIG, SIGHUP);

详情请参阅man 2 prctl

编辑:这是仅限Linux

我正在尝试解决同样的问题,由于我的程序必须在OS X上运行,所以仅限于Linux的解决scheme并不适用于我。

我在这个页面上得出了与其他人相同的结论 – 当父母死亡时,没有POSIX兼容的方式通知孩子。 所以我扼杀了下一个最好的事情 – 让孩子进行民意测验。

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

这不是很好,但它工作,比在这个页面其他地方build议的TCP套接字/锁文件轮询解决scheme更容易。

通过在“子”和“父”中运行“原始”代码(即:在fork()之后反转通常的testing意义),我已经实现了这一点。 然后在“生成的”代码中捕获SIGCHLD …

可能不可能在你的情况下,但可爱的时候它的工作。

如果您无法修改subprocess,则可以尝试如下所示:

 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进程中运行subprocess。 subprocess在后台产生。 shell等待换行(或EOF)然后杀死孩子。

当父母死亡时 – 不pipe是什么原因 – 它将closurespipe道的末端。 子shell将从读取中获得EOF并继续杀死背景subprocess。

为了完整起见。 在Mac OS X上,您可以使用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); } 

受到另一个答案的启发,我想出了以下全POSIX解决scheme。 总的想法是在父母和孩子之间创造一个中间过程,这个过程有一个目的:注意父母死亡时,明确杀死孩子。

当子代码不能被修改时,这种types的解决scheme是有用的。

 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) 

subprocess是否有一个pipe道进出父进程? 如果是这样的话,如果你正在写一个SIGPIPE,或者在阅读的时候收到EOF,这些条件可能被检测到。

在Linux下,你可以在孩子中安装父母死亡信号,例如:

 #include <sys/prctl.h> #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 ... 

请注意,将父进程id存储在fork之前,并在prctl()后面的子节点中进行testing,可以消除prctl()和调用subprocess的退出之间的争用情况。

还要注意的是,小孩的父母死亡信号在新创build的孩子中被清除。 它不受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 ... 

尽pipe如此,依赖于该系统进程正在init并使PID 1不可移植。 POSIX.1-2008规定 :

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

传统上,采用所有孤儿的系统过程是PID 1,即init – 这是所有过程的祖先。

在像Linux或者FreeBSD这样的现代系统中,另一个进程可能有这个angular色。 例如,在Linux上,进程可以调用prctl(PR_SET_CHILD_SUBREAPER, 1)来将自己build立为inheritance其所有后代的所有孤儿的系统进程(参考Fedora 25的例子 )。

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

父进程有可能捕获大多数可能的终止事件,并试图在那个时候终止subprocess,但总有一些无法被捕获。

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

为了扩大这个比喻 – 唯一的另一个标准的做法是当孩子发现自己不再有父母时自杀。

有一个Linux的方式与prctl(2) – 看到其他答案。

安装一个陷阱处理程序来捕获SIGINT,如果它仍然存在,它会终止你的subprocess,尽pipe其他的海报是正确的,它不会捕获SIGKILL。

打开一个具有独占访问权限的.lock文件,并让子进行轮询,试图打开它 – 如果打开成功,subprocess应该退出

正如其他人所指出的那样,当父母退出时,依靠父母亲成为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) ; 

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

这个选项似乎比使用pipe道或依靠信号更简单。

我认为一个快速和肮脏的方法是在孩子和父母之间创build一个pipe道。 当父母离开时,孩子将会收到SIGPIPE。

这个解决scheme为我工作:

  • 将stdinpipe道传递给子对象 – 您不必将任何数据写入stream。
  • 孩子无限期地从标准input读取,直到EOF。 EOF表示父母已经离开了。
  • 当父母离开时,这是一种万无一失的便携式方法。 即使父母崩溃,操作系统也会closurespipe道。

这是一个工人型的过程,当父母还活着时,其存在才有意义。

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

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

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

请注意,您可能需要阅读相当多的精美内容 – 包括基本定义(定义)部分以及exit()setsid()setpgrp()的系统服务信息以获取完整的图片。 (我也会!)

如果你发送一个信号到PID 0,例如

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

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

你可以用下面的东西来testing它:

 (cat && kill 0) | python 

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

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

1)在通过execv启动Java应用程序之前,根据build议execv分叉subprocess的prctl(PR_SET_PDEATHSIG, SIGHUP)

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

编辑130118:

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

而不是在Java应用程序中对PPID进行轮询,而只是简单地让shutdown钩子执行清除操作,然后像上面那样硬停止。 然后,我确保在生成的subprocess的C ++父应用程序中调用waitpid来终止所有的事情。 这似乎是一个更健壮的解决scheme,因为subprocess确保它终止,而父进程使用现有的引用来确保subprocess终止。 把这个和以前的解决scheme比较一下,只要父母的程序满意,他们就会终止这个解决scheme,让孩子们在终止之前试着弄清楚他们是否孤儿。

一些海报已经提到了pipe道和kqueue 。 实际上,你也可以通过socketpair()调用创build一对连接的Unix域套接字 。 套接字types应该是SOCK_STREAM

让我们假设你有两个套接字文件描述符fd1,fd2。 现在fork()创buildsubprocess,它将inheritancefds。 在父母你closuresfd2和在孩子你closuresfd1。 现在每个进程都可以poll()剩余的打开的fd在自己的结束POLLIN事件。 只要每一方在正常的生命周期内没有明确地close()它的fd,你可以相当肯定一个POLLHUP标志应该指示另一个终止(不pipe干净与否)。 一旦通知此事件,孩子可以决定做什么(例如死亡)。

 #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); } } 

您可以尝试编译上面的概念certificate代码,并在像./a.out &这样的terminal中运行它。 你有大约100秒的时间去尝试通过各种信号来杀死父PID,否则就会退出。 无论哪种情况,您都应该看到消息“孩子:父母挂断”。

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

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

该解决scheme只调用POSIX函数。 我在Linux和FreeBSD上试了这个。 我认为它应该在其他Unix上工作,但我还没有真正testing过。

也可以看看:

  • Linux手册页的unix(7) ,Linux上的unix(4)poll(2)socketpair(2)socket(7)

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

我尝试使用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(); } 

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

我find了两个解决scheme,都不完美。

1.收到SIGTERM信号后杀死所有的孩子(-pid)。
很显然,这个解决scheme不能处理“kill -9”,但是对于大多数情况来说它是可行的,而且非常简单,因为它不需要记住所有的subprocess。

 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和突然崩溃已被OS自动处理来终止进程组,所以不在这里。

2.使用chjj / pty.js通过附加的控制terminal产生你的进程。
当你杀死当前进程甚至kill -9时,所有的subprocess也会被自动杀死(通过OS?)。 我想这是因为当前进程持有terminal的另一端,所以如果当前进程死亡,subprocess将得到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(.....); */ 

我设法通过滥用terminal控制和会话来实现一个具有3个进程的可移植的非轮询解决scheme。 这是心理手淫,但工程。

诀窍是:

  • 进程A开始
  • 进程A创build一个pipe道P(并且从不读取)
  • 过程A分叉到过程B中
  • 过程B创build一个新的会话
  • 进程B为该新会话分配一个虚拟terminal
  • 进程B在小孩退出时安装SIGCHLD处理程序
  • 进程B设置一个SIGPIPE处理程序
  • 进程B进入进程C
  • 进程C可以执行任何需要的操作(例如,exec()是未修改的二进制文件或运行任何逻辑)
  • 进程B写入pipe道P(并以此方式阻塞)
  • 处理进程B上的wait(),并在处理完成时退出

那样:

  • 如果进程A死亡:进程B得到一个SIGPIPE并死亡
  • 如果进程B死亡:进程A的wait()返回并死亡,进程C得到一个SIGHUP(因为当一个terminal连接的会话的领导者死亡时,前台进程组中的所有进程都得到SIGHUP)
  • 如果进程C死亡:进程B得到一个SIGCHLD并死亡,所以进程A死亡

不足之处:

  • 进程C不能处理SIGHUP
  • 进程C将在不同的会话中运行
  • 进程C不能使用会话/进程组API,因为它会破坏脆弱的设置
  • 为每一个这样的操作创build一个terminal并不是有史以来最好的想法

历史上,从UNIX v7开始,进程系统通过检查进程的父进程ID来检测进程的单进程。 正如我所说,历史上, init(8)系统进程是一个特殊的进程,只有一个原因:它不能死亡。 它不能死因为内核algorithm处理分配一个新的父进程id,取决于这个事实。 当进程执行其exit(2)调用(通过进程系统调用或通过外部任务向其发送信号等)时,内核将该进程的所有subprocess重新分配init进程的id作为其父进程id 。 这导致了最简单的testing,并且最容易知道一个进程是否孤儿。 只要检查getppid(2)系统调用的结果,并且如果它是init(2)进程的进程标识,那么进程在系统调用之前得到孤儿。

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

  • first, we have the possibility of changing the init process to any user process, so How can we assure that the init process will always be parent of all orphan processes? Well, in the exit system call code there's a explicit check to see if the process executing the call is the init process (the process with pid equal to 1) and if that's the case, the kernel panics (It should not be able anymore to maintain the process hierarchy) so it is not permitted for the init process to do an exit(2) call.
  • second, there's a race condition in the basic test exposed above. Init process' id is assumed historically to be 1 , but that's not warranted by the POSIX approach, that states (as exposed in other response) that only a system's process id is reserved for that purpose. Almost no posix implementation does this, and you can assume in original unix derived systems that having 1 as response of getppid(2) system call is enough to assume the process is orphan. Another way to check is to make a getppid(2) just after the fork and compare that value with the result of a new call. This simply doesn't work in all cases, as both call are not atomic together, and the parent process can die after the fork(2) and before the first getppid(2) system call. The process parent id only changes once, when its parent does an exit(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)`, but you can assume safely these processes as having no parent either (except when you substitute in a system the init process)