假设我有一个进程,它恰好产生了一个子进程。现在,当父进程出于某种原因(正常或异常,通过kill,^ C,断言失败或其他任何原因)退出时,我希望子进程死亡。如何正确地做到这一点?
一些关于stackoverflow的类似问题:
- (如前所述)当父进程退出时,如何导致子进程退出?
- (稍后询问)在杀死父进程时,是否会自动杀死使用fork()创建的子进程?
有关Windows的 stackoverflow的一些类似问题:
假设我有一个进程,它恰好产生了一个子进程。现在,当父进程出于某种原因(正常或异常,通过kill,^ C,断言失败或其他任何原因)退出时,我希望子进程死亡。如何正确地做到这一点?
一些关于stackoverflow的类似问题:
有关Windows的 stackoverflow的一些类似问题:
Answers:
子进程可以SIGHUP
通过PR_SET_PDEATHSIG
在prctl()
syscall中指定选项来要求父进程去世时内核发出(或其他信号),如下所示:
prctl(PR_SET_PDEATHSIG, SIGHUP);
有关man 2 prctl
详细信息,请参见。
编辑:这是仅Linux
prctl()
无竞争条件的方式使用。顺便说一句,Maxim链接的答案不正确。
man prctl
说:将调用进程的父进程终止信号设置为arg2(信号值在1..maxsig范围内,或在0之前清除)。这是调用进程在其父进程死亡时将获得的信号。执行set-user-ID或set-group-ID二进制文件时,将为fork(2)和(自Linux 2.4.36 / 2.6.23起)的子代清除此值。
我正在尝试解决相同的问题,并且由于我的程序必须在OS X上运行,因此仅Linux解决方案对我不起作用。
我和本页上的其他人得出的结论是一样的-没有一种与POSIX兼容的方式可以在父母去世时通知孩子。因此,我想出了第二好的方法-对孩子进行投票。
当父进程终止(出于任何原因)时,子进程的父进程将变为进程1。如果子进程只是定期轮询,则可以检查其父进程是否为1。如果是,则子进程应退出。
这不是很好,但是它可以工作,并且比本页其他地方建议的TCP套接字/锁文件轮询解决方案更容易。
gettpid()
则不会变为1,而是获得pid
区域调度程序(进程zsched
)的。
如果您无法修改子进程,则可以尝试以下操作:
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),然后杀死该子进程。
当父级死亡时,无论是什么原因,它将关闭管道的末端。子外壳将从读取的内容中获取EOF,然后继续杀死后台的子进程。
dup2
通过使用read -u
标志从特定文件描述符中读取来避免和接管stdin 。我还添加了一个setpgid(0, 0)
子项,以防止在终端中按^ C时退出子项。
dup2()
调用的参数顺序错误。如果要pipes[0]
用作stdin,则必须编写dup2(pipes[0], 0)
而不是dup2(0, pipes[0])
。dup2(oldfd, newfd)
呼叫将在此关闭先前打开的newfd。
在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 ...
请注意,将父进程ID存储在fork之前,然后在子进程中对其进行测试,这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-它是所有进程的祖先。
在像Linux或FreeBSD这样的现代系统上,另一个进程可能扮演着这个角色。例如,在Linux上,一个进程可以调用prctl(PR_SET_CHILD_SUBREAPER, 1)
以将其自身建立为继承其任何后代的所有孤儿的系统进程(请参见Fedora 25上的示例)。
init(8)
流程...。您唯一可以假设的是,当父流程死亡时,其父ID将会改变。这实际上在流程的生命周期中发生一次。。。。只有一个主要例外,它是针对init(8)
儿童的,但您却init(8)
从未受到这种保护exit(2)
(在这种情况下,内核恐慌)
为了完整起见。在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);
}
NSTask
或posix生成。startTask
在这里查看我代码中的函数:github.com/neoneye/newton-commander-browse/blob/master/Classes/…–
受到另一个答案的启发,我提出了以下全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)
正如其他人指出的那样,在父级退出时依靠父级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)
;
如果您不想全速轮询,请根据需要添加微睡眠。
对于我来说,此选项似乎比使用管道或依赖信号更简单。
getpid()
在调用之前在父级中完成fork()
。如果父母在此之前去世,则该子女不存在。可能发生的情况是孩子在父母身边住了一段时间。
一些海报已经提到了烟斗和烟斗kqueue
。实际上,您还可以通过调用创建一对连接的Unix域套接字socketpair()
。插座类型应为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,否则它就会退出。无论哪种情况,您都应该看到消息“孩子:父母挂了电话”。
与使用SIGPIPE
处理程序的方法相比,此方法不需要尝试write()
调用。
此方法也是对称的,即进程可以使用同一通道来监视彼此的存在。
此解决方案仅调用POSIX函数。我在Linux和FreeBSD上尝试过。我认为它应该可以在其他Unix上运行,但是我还没有真正测试过。
也可以看看:
unix(7)
Linux的手册页,unix(4)
为FreeBSD, ,,poll(2)
在Linux上。socketpair(2)
socket(7)
在POSIX下exit()
,_exit()
和_Exit()
函数定义为:
因此,如果您将父流程安排为其流程组的控制流程,则在父流程退出时,子流程应该收到SIGHUP信号。我不确定当父母崩溃时会发生这种情况,但我认为确实如此。当然,对于非崩溃情况,它应该可以正常工作。
请注意,您可能需要阅读了不少精美的印刷品-包括基本定义(定义)部分,以及系统服务信息exit()
,并setsid()
和setpgrp()
-来获得完整的信息。(我也会!)
如果您将信号发送到pid 0,例如
kill(0, 2); /* SIGINT */
该信号被发送到整个过程组,从而有效地杀死了孩子。
您可以使用以下类似方法轻松对其进行测试:
(cat && kill 0) | python
如果再按^ D键,您会看到该文本"Terminated"
表明Python解释器确实已被杀死,而不是因为关闭了stdin而退出了。
(echo -e "print(2+2)\n" & kill 0) | sh -c "python -"
快乐地打印4而不是终止
如果与其他任何人都相关,当我从C ++派生分叉的子进程中的JVM实例时,使父实例完成后才能正确终止JVM实例的唯一方法是执行以下操作。如果这不是最好的方法,希望有人可以在评论中提供反馈。
1)prctl(PR_SET_PDEATHSIG, SIGHUP)
根据建议调用分叉的子进程,然后通过execv
和启动Java应用程序。
2)向Java应用程序添加一个关闭挂钩,该轮询将轮询直到其父PID等于1,然后执行hard Runtime.getRuntime().halt(0)
。通过启动运行ps
命令的单独外壳程序来完成轮询(请参阅:如何在Java或Linux上的JRuby中找到我的PID?)。
编辑130118:
看来这不是一个可靠的解决方案。我仍在努力了解正在发生的细微差别,但在屏幕/ SSH会话中运行这些应用程序时,有时仍会遇到孤立的JVM进程。
我没有在Java应用程序中轮询PPID,而是简单地让shutdown钩子执行了清除操作,然后如上所述进行了硬停止。然后,waitpid
当需要终止所有操作时,我确保在生成的子进程上的C ++父应用程序中调用。这似乎是一个更可靠的解决方案,因为子进程确保其终止,而父进程使用现有引用来确保其子进程终止。将此与以前的解决方案进行比较,前一个解决方案让父进程在需要时终止,并让子进程尝试在终止之前弄清是否是孤立的。
PID equals 1
等待无效。新的父代可以是其他一些PID。您应该检查它是否从原始父对象(在fork()之前的getpid())更改为新父对象(在fork()之前被调用的子代中的getppid()不等于getpid())。
特定于Linux的另一种方法是在新的PID名称空间中创建父对象。然后,它将在该名称空间中成为PID 1,并且退出时,其所有子级将立即被杀死SIGKILL
。
不幸的是,要创建一个新的PID名称空间,您必须具有CAP_SYS_ADMIN
。但是,此方法非常有效,除了最初启动父级外,不需要对父级或子级进行任何实际更改。
如果父母去世,则孤儿的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
我发现2个解决方案,都不是完美的。
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,则可以像上面那样安装“退出”处理程序。注意:操作系统已自动处理Ctrl + C和突然崩溃,以杀死进程组,因此此处不再赘述。
2.使用chjj / pty.js生成带有连接的控制终端的进程。
当您以任何方式杀死当前进程甚至杀死-9时,所有子进程也将被自动杀死(通过OS?)。我猜是因为当前进程位于终端的另一端,所以如果当前进程死亡,则子进程将获得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(.....);
*/
通过滥用终端控制和会话,我设法通过3个进程来制作一个可移植的非轮询解决方案。这是精神上的手淫,但有效。
诀窍是:
那样:
缺点:
尽管已经过去了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)
调用时(通过进程系统调用或通过外部任务发送信号等),内核将该进程的所有子进程重新分配初始化进程的ID作为其父进程ID。这将导致最简单的测试和最可移植的方式来了解进程是否已成为孤儿。只需检查getppid(2)
系统调用的结果,以及它是否是init(2)
这种方法产生了两个问题,这些问题可能会导致问题:
init
流程更改为任何用户流程,那么我们如何确保init流程始终是所有孤立流程的父级?好吧,在exit
系统调用代码中,有一个显式检查,以查看执行调用的进程是否为init进程(pid等于1的进程),如果是这种情况,则内核恐慌(应该不再能够维护)流程层次结构),因此init流程不允许进行exit(2)
调用。1
,但这并不是POSIX方法所保证的,它声明(如在其他响应中所公开的)仅为此目的保留了系统的进程ID。几乎没有posix实现可以做到这一点,并且您可以假设在原始的unix派生系统中,具有1
作为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
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)`,但您可以安全地假设这些进程也没有父进程(除非在系统中替换了init进程)我已经将使用环境的父pid传递给孩子,然后定期检查孩子是否存在/ proc / $ ppid。