父进程退出后如何让子进程死亡?
- 2024-09-30 15:23:00
- admin 原创
- 136
问题描述:
假设我有一个进程,它只生成一个子进程。现在,当父进程因某种原因(正常或异常,通过 kill、^C、断言失败或其他任何原因)退出时,我希望子进程终止。如何正确执行此操作?
stackoverflow 上还有一些类似的问题:
Windows 版stackoverflow 上有一些类似的问题:
解决方案 1:
当父进程死亡时,子进程可以通过在系统调用中指定选项来要求内核传递SIGHUP
(或其他信号),如下所示:PR_SET_PDEATHSIG
`prctl()`
prctl(PR_SET_PDEATHSIG, SIGHUP);
请参阅man 2 prctl
详情。
编辑:这仅适用于 Linux
解决方案 2:
我正在尝试解决同样的问题,由于我的程序必须在 OS X 上运行,所以仅适用于 Linux 的解决方案对我来说不起作用。
我和本页上的其他人得出了相同的结论——没有与 POSIX 兼容的方法可以在父母去世时通知孩子。所以我想出了退而求其次的办法——让孩子进行投票。
当父进程死亡(无论出于何种原因)时,子进程的父进程将成为进程 1。如果子进程只是定期轮询,它可以检查其父进程是否为 1。如果是,子进程应该退出。
这不是很好,但它有效,并且比本页其他地方建议的 TCP 套接字/锁文件轮询解决方案更容易。
解决方案 3:
在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——它是所有进程的祖先。
在Linux或FreeBSD等现代系统上,另一个进程可能具有该角色。例如,在 Linux 上,进程可以调用prctl(PR_SET_CHILD_SUBREAPER, 1)
以将自己确立为系统进程,并继承其任何后代的所有孤儿进程(参见Fedora 25 上的示例)。
解决方案 4:
我过去曾通过在“子进程”中运行“原始”代码并在“父进程”中运行“衍生”代码来实现这一点(即:您在之后反转通常意义上的测试fork()
)。然后在“衍生”代码中捕获 SIGCHLD...
在您的情况下可能不可行,但当它起作用时会很可爱。
解决方案 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,然后继续终止后台子进程。
解决方案 6:
受到此处另一个答案的启发,我想出了以下全 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,而该 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)
解决方案 7:
为了完整性,在 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);
}
解决方案 8:
子进程是否有通向/来自父进程的管道?如果是,则在写入时您会收到 SIGPIPE,在读取时您会收到 EOF - 这些情况可以被检测到。
解决方案 9:
我认为仅使用标准 POSIX 调用无法保证这一点。就像现实生活一样,一旦子进程被生成,它就有了自己的生命。
父进程有可能捕获大多数可能的终止事件,并尝试在此时终止子进程,但总有一些无法捕获的事件。
例如,没有进程可以捕获SIGKILL
。当内核处理此信号时,它将终止指定的进程,而不会向该进程发出任何通知。
扩展这个类比——唯一的其他标准方式是当孩子发现自己不再有父母时自杀。
有一种仅适用于 Linux 的方法可以实现此目的prctl(2)
- 请参阅其他答案。
解决方案 10:
这个解决方案对我有用:
将标准输入管道传递给子进程 - 您不必将任何数据写入流中。
子进程无限期地从 stdin 读取,直到 EOF。EOF 表示父进程已经离开。
这是一种万无一失且可移植的检测父进程是否消失的方法。即使父进程崩溃,操作系统也会关闭管道。
这是一个工作类型的进程,它的存在只有当父进程存活时才有意义。
解决方案 11:
有些发帖者已经提到了管道和。实际上,您也可以通过调用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
", getpid());
sleep(100);
exit(0);
} else { /* child */
close(sv[0]);
fprintf(stderr, "child: pid = %d
", 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
");
exit(0);
}
}
您可以尝试编译上述概念验证代码,并在类似 的终端中运行它./a.out &
。您大约有 100 秒的时间尝试通过各种信号终止父 PID,否则它只会退出。无论哪种情况,您都应该看到消息“子进程:父进程挂断”。
与使用处理程序的方法相比SIGPIPE
,该方法不需要尝试write()
调用。
该方法也是对称的,即进程可以使用相同的通道来监视彼此的存在。
此解决方案仅调用 POSIX 函数。我在 Linux 和 FreeBSD 中尝试过。我认为它应该可以在其他 Unix 上运行,但我还没有真正测试过。
参见:
unix(7)
Linux 手册页,unix(4)
适用于 FreeBSDpoll(2)
、、、在 Linux 上socketpair(2)
。socket(7)
解决方案 12:
安装一个陷阱处理程序来捕获 SIGINT,如果您的子进程仍然存在,它会终止它,尽管其他海报正确地指出它不会捕获 SIGKILL。
打开一个具有独占访问权限的 .lockfile,并让子进程轮询它尝试打开它 - 如果打开成功,子进程应该退出
解决方案 13:
正如其他人指出的那样,依赖父进程 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)
;
如果您不想全速轮询,请根据需要添加微睡眠。
对我来说,这个选项比使用管道或依赖信号更简单。
解决方案 14:
另一种 Linux 特有的方法是,在新的 PID 命名空间中创建父进程。然后它将是该命名空间中的 PID 1,当它退出时,它的所有子进程都将立即被杀死SIGKILL
。
不幸的是,为了创建新的 PID 命名空间,您必须拥有CAP_SYS_ADMIN
。但是,这种方法非常有效,并且除了初始启动父进程之外,不需要对父进程或子进程进行任何实际更改。
参见clone(2)、pid_namespaces(7)和unshare(2)。
解决方案 15:
我认为一个快捷而粗暴的方法是在子进程和父进程之间创建一个管道。当父进程退出时,子进程将收到 SIGPIPE 信号。
解决方案 16:
在POSIX下,exit()
和函数定义为:_exit()
`_Exit()`
如果该进程是控制进程,则应向属于调用进程的控制终端的前台进程组中的每个进程发送SIGHUP信号。
因此,如果您安排父进程作为其进程组的控制进程,则当父进程退出时,子进程应该会收到 SIGHUP 信号。我不确定当父进程崩溃时是否会发生这种情况,但我认为确实如此。当然,对于非崩溃情况,它应该可以正常工作。
请注意,您可能需要阅读大量的细则 - 包括基本定义(定义)部分,以及和的系统服务信息exit()
-setsid()
才能setpgrp()
获得完整的画面。 (我也是!)
解决方案 17:
如果你向 pid 0 发送一个信号,例如使用
kill(0, 2); /* SIGINT */
该信号被发送给整个进程组,从而有效地杀死子进程。
您可以使用以下方法轻松测试它:
(cat && kill 0) | python
如果您随后按下 ^D,您将看到文本"Terminated"
,表明 Python 解释器确实已被终止,而不是因为 stdin 被关闭而退出。
解决方案 18:
如果它与其他人相关,当我从 C++ 派生出 JVM 实例时,在父进程完成后,我唯一能让 JVM 实例正确终止的方法是执行以下操作。如果这不是最好的方法,希望有人可以在评论中提供反馈。
1)prctl(PR_SET_PDEATHSIG, SIGHUP)
在通过 启动 Java 应用程序之前,按照建议调用分叉的子进程execv
,然后
2) 向 Java 应用程序添加一个关闭钩子,该钩子会进行轮询,直到其父 PID 等于 1,然后执行硬操作Runtime.getRuntime().halt(0)
。轮询是通过启动运行ps
命令的单独 shell 来完成的(请参阅:如何在 Java 或 Linux 上的 JRuby 中找到我的 PID?)。
编辑 130118:
看来这不是一个可靠的解决方案。我仍然有点难以理解到底发生了什么,但在屏幕/SSH 会话中运行这些应用程序时,有时我仍然会得到孤立的 JVM 进程。
我没有在 Java 应用程序中轮询 PPID,而是让关闭挂钩执行清理,然后像上面一样硬停止。然后,我确保waitpid
在需要终止所有内容时在生成的子进程上调用 C++ 父应用程序。这似乎是一个更强大的解决方案,因为子进程确保自己终止,而父进程使用现有引用来确保其子进程终止。将其与之前的解决方案进行比较,之前的解决方案是让父进程随时终止,并让子进程在终止之前尝试确定它们是否已成为孤儿进程。
解决方案 19:
从历史上看,从 UNIX v7 开始,进程系统通过检查进程的父 id 来检测进程的孤立性。正如我所说,从历史上看,系统init(8)
进程是一个特殊进程,只有一个原因:它不会死。它不会死,因为处理分配新父进程 id 的内核算法取决于这个事实。当进程执行其exit(2)
调用时(通过进程系统调用或通过外部任务,如向其发送信号等),内核会将此进程的所有子进程重新分配给 init 进程的 id 作为其父进程 id。这导致了最简单的测试,也是最便携的了解进程是否已变为孤儿的方式。只需检查getppid(2)
系统调用的结果,如果它是进程的进程 id init(2)
,则该进程在系统调用之前就已变为孤儿进程。
这种方法会出现两个可能导致问题的问题:
首先,我们可以将
init
进程更改为任何用户进程,那么我们如何确保 init 进程始终是所有孤立进程的父进程呢?好吧,在exit
系统调用代码中有一个明确的检查,以查看执行调用的进程是否是 init 进程(pid 等于 1 的进程),如果是的话,内核就会崩溃(它应该不再能够维护进程层次结构),因此不允许 init 进程进行调用exit(2)
。其次,上面公开的基本测试中存在竞争条件。Init 进程的 ID 在历史上被认为是
1
,但 POSIX 方法并不保证这一点,该方法指出(如其他响应中所公开的)只有系统的进程 ID 是为此目的而保留的。几乎没有 posix 实现会这样做,并且您可以假设在原始 unix 派生系统中,具有系统调用1
的响应getppid(2)
足以假设该进程是孤儿进程。另一种检查方法是getppid(2)
在 fork 之后立即创建一个并将该值与新调用的结果进行比较。这在所有情况下都不起作用,因为两个调用不是原子的,并且父进程可以在fork(2)
第一个getppid(2)
系统调用之后和之前死亡。进程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)`,但您可以安全地假设这些进程也没有父进程(除非您在系统中替换 init 进程)
解决方案 20:
我已经将使用环境的父进程 pid 传递给子进程,然后定期检查子进程中是否存在 /proc/$ppid。
解决方案 21:
我设法通过滥用终端控制和会话来实现具有 3 个进程的可移植、非轮询解决方案。
诀窍是:
进程 A 已启动
进程 A 创建管道 P(但从不从中读取数据)
进程 A 分叉成进程 B
进程 B 创建新的会话
进程 B 为该新会话分配一个虚拟终端
进程 B 安装 SIGCHLD 处理程序,当子进程退出时终止
进程 B 设置 SIGPIPE 处理程序
进程 B 分叉成进程 C
进程 C 执行其需要的操作(例如,执行未修改的二进制文件或运行任何逻辑)
进程 B 写入管道 P(并以此方式阻塞)
进程 A 等待进程 B,并在进程 B 死亡时退出
这样:
如果进程 A 死亡:进程 B 收到 SIGPIPE 信号并死亡
如果进程 B 死亡:进程 A 的 wait() 返回并死亡,进程 C 收到 SIGHUP 信号(因为当连接终端的会话的会话领导者死亡时,前台进程组中的所有进程都会收到 SIGHUP 信号)
如果进程 C 死亡:进程 B 收到 SIGCHLD 信号并死亡,因此进程 A 也死亡
缺点:
进程 C 无法处理 SIGHUP
进程 C 将在不同的会话中运行
进程 C 不能使用会话/进程组 API,因为它会破坏脆弱的设置
为每个这样的操作创建一个终端并不是最好的主意
解决方案 22:
我找到了两个解决方案,但都不完美。
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”处理程序。注意:Ctrl+C 和突然崩溃已被操作系统自动处理以终止进程组,因此这里不再赘述。
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(.....);
*/
解决方案 23:
如果父进程死亡,孤儿进程的 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
解决方案 24:
尽管已经过去了 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();
}
- 2024年20款好用的项目管理软件推荐,项目管理提效的20个工具和技巧
- 2024年开源项目管理软件有哪些?推荐5款好用的项目管理工具
- 项目管理软件有哪些?推荐7款超好用的项目管理工具
- 项目管理软件哪个最好用?盘点推荐5款好用的项目管理工具
- 项目管理软件有哪些最好用?推荐6款好用的项目管理工具
- 项目管理软件有哪些,盘点推荐国内外超好用的7款项目管理工具
- 2024项目管理软件排行榜(10类常用的项目管理工具全推荐)
- 项目管理软件排行榜:2024年项目经理必备5款开源项目管理软件汇总
- 2024年常用的项目管理软件有哪些?推荐这10款国内外好用的项目管理工具
- 项目管理必备:盘点2024年13款好用的项目管理软件