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

假设我有一个进程,它只生成一个子进程。现在,当父进程由于某种原因(正常或不正常,通过kill, ^C,断言失败或其他任何原因)退出时,我希望子进程死亡。如何正确地做到这一点?


关于stackoverflow的一些类似问题:


窗户在stackoverflow上的一些类似问题:

203483 次浏览

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

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

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

扩展一下类比——唯一的另一种标准方式是,当孩子发现自己不再有父母时自杀。

有一种linux特有的方法可以用prctl(2)来实现——参见其他答案。

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

prctl(PR_SET_PDEATHSIG, SIGHUP);

详见man 2 prctl

编辑:这是linux专用的

子进程是否有连接父进程的管道?如果是这样,那么写入时会收到SIGPIPE,读取时会收到EOF——这些情况都可以检测到。

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

以独占访问的方式打开一个.lockfile,并让子进程尝试打开它——如果打开成功,子进程应该退出

我在过去已经通过在“子”中运行“原始”代码和在“父”中运行“衍生”代码来实现这一点(也就是说:你在fork()之后反转通常意义上的测试)。然后在“衍生”代码中捕获SIGCHLD…

对你来说可能不行,但管用的时候很可爱。

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

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

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

请注意,你可能必须阅读相当多的小字-包括基本定义(定义)部分,以及exit()setsid()setpgrp()的系统服务信息-才能获得完整的图片。(我也是!)

我正在尝试解决同样的问题,但由于我的程序必须运行在OS X上,所以只运行linux的解决方案对我不起作用。

我得到了与本页其他人相同的结论——当父母去世时,没有一种与posix兼容的方式来通知孩子。所以我想出了次好的办法——让孩子投票。

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

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

如果你无法修改子进程,你可以尝试以下方法:

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将从read中获得一个EOF,并继续杀死后台的子进程。

我认为一种快速而又不那么简单的方法是在子节点和父节点之间创建一个管道。当父节点退出时,子节点将收到一个SIGPIPE。

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

如果你发送一个信号到pid 0,使用for实例

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

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

你可以很容易地测试它,比如:

(cat && kill 0) | python

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

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

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

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

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

130118年编辑:

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

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

一些海报已经提到了管道和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 = %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上工作,但我还没有真正测试过。

参见:

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

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

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

在我看来,这个选项比使用管道或依赖于信号更简单。

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

< p > 1.当收到SIGTERM信号时,通过Kill (-pid)杀死所有子结点。 < br > 显然,这个解决方案不能处理“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和突然崩溃已经被操作系统自动处理了,用来杀死进程组,这里不再讲 < p > 2.使用chjj/pty.js生成附加控制终端的进程。 < br > 当你以任何方式甚至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(.....);
*/


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

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

如果我们确定负责采用所有孤儿的系统进程具有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 -它是所有进程的祖先。

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

通过滥用终端控制和会话,我设法用3个进程实现了一个可移植的、非轮询的解决方案。

诀窍在于:

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

这种方式:

  • 如果进程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)系统调用的结果,如果它是init(2)进程的进程id,那么该进程在系统调用之前就孤儿了。

这种方法会产生两个问题:

  • 首先,我们可以将init进程更改为任何用户进程,那么我们如何确保init进程始终是所有孤儿进程的父进程呢?好吧,在exit系统调用代码中有一个显式的检查,以查看执行调用的进程是否是init进程(pid等于1的进程),如果是这种情况,内核会恐慌(它应该不再能够维护进程层次结构),因此不允许init进程执行exit(2)调用。
  • 其次,在上面暴露的基本测试中有一个竞态条件。Init进程的id在历史上被假定为1,但POSIX方法并不保证这一点,它声明(在其他响应中暴露)只有系统的进程id是为该目的保留的。几乎没有posix实现这样做,你可以假设在最初的unix派生系统中,有1作为getppid(2)系统调用的响应就足以假设该进程是孤儿进程。另一种检查方法是在fork之后创建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进程)。

这个解决方案对我很有效:

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

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

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

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

参见复制(2)pid_namespaces (7)(2)共享

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