stackoverflow上的一些类似问题:
- (早期提问)当父进程退出时如何使子进程退出?
- (稍后提问)使用fork()创建的子进程是否在父进程被杀死时自动退出?
stackoverflow上一些关于Windows系统的类似问题:
通过在 prctl()
系统调用中指定选项 PR_SET_PDEATHSIG
,子进程可以要求内核在父进程死亡时发送 SIGHUP
(或其他信号),例如:
prctl(PR_SET_PDEATHSIG, SIGHUP);
详情请参见 man 2 prctl
。
注:此功能仅适用于 Linux。
我也在解决相同的问题,由于我的程序必须在OS X上运行,所以Linux-only的解决方案对我没有用。
我得出了与此页面上的其他人相同的结论--没有一种符合POSIX标准的方法可以在父进程死亡时通知子进程。因此,我采用了次优解--让子进程进行轮询。
当父进程(无论何种原因)死亡时,子进程的父进程变为进程1。如果子进程简单地定期轮询,它可以检查其父进程是否为1。如果是,则子进程应该退出。
虽然这并不完美,但它能够正常工作,并且比此页面上其他人提出的TCP套接字/锁文件轮询解决方案更容易实现。
gettpid()
函数并不会返回1,而是会返回该zone调度程序(进程zsched
)的进程ID。 - Patrick Schlüter在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,并在子进程中使用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)
(在这种情况下内核会崩溃)。 - Luis Coloradoexecve
。Bash 在这里并不特别。 - maxschlepzigfork()
之后反转了通常的测试意义)。然后在“派生”的代码中捕获SIGCHLD信号……在你的情况下可能不可行,但当它起作用时很可爱。如果您无法修改子进程,可以尝试以下操作:
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 并继续杀死后台子进程。
read -u
标志从特定的文件描述符中读取,避免使用dup2
和接管stdin。我还在子进程中添加了一个setpgid(0, 0)
来防止在终端中按下^C时退出。 - Greg Hewgilldup2()
调用的参数顺序错误。如果您想使用 pipes[0]
作为标准输入,您需要写成 dup2(pipes[0], 0)
而不是 dup2(0, pipes[0])
。它是 dup2(oldfd, newfd)
,其中该调用关闭了先前打开的 newfd
。 - maxschlepzigdup2()
后,您还应该关闭 pipes[0]
。 - Jonathan Leffler为了完整性,如果在 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);
}
kqueue
正如 @neoneye 的答案中所述可以正常工作。 - Jonas Due VesterhedenCFRelease(source);
之后不是应该加上 CFRunLoopRun();
吗?类似于 https://developer.apple.com/documentation/corefoundation/cffiledescriptor-ru3#2556086,它使用了 CFRunLoopRunInMode(kCFRunLoopDefaultMode, 20.0, false);
此外,我没有观察到来自 https://dev59.com/JnVC5IYBdhLWcg3wdw-5#1h8moYgBc1ULPQZF2DDz 的崩溃行为。 - timotheecour受到这里另一个答案的启发,我想出了以下全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)
子进程是否有与父进程的管道连接?如果是,当写入时您将收到SIGPIPE信号,或者读取时会得到EOF - 这些条件可以被检测到。
我不认为只使用标准的POSIX调用可以保证这一点。就像现实生活一样,一旦一个子进程被启动,它就有了自己的生命。
父进程是可能会捕获大部分可能的终止事件,并尝试在那时杀死子进程,但总会有一些无法捕获。
例如,没有进程可以捕获SIGKILL
。当内核处理此信号时,它将完全不通知指定的进程而将其杀死。
扩展这个比喻 - 唯一的其他标准方法是当子进程发现自己没有父进程时自杀。
还有一种Linux专有方式可以使用prctl(2)
来实现 - 参见其他答案。
这是针对一种只有在父进程存在时才有意义的工作类型进程的情况。
prctl()
。顺便说一下,Maxim提供的答案是错误的。 - maxschlepzigman prctl
表示:将调用进程的父进程死亡信号设置为arg2(在1..maxsig范围内的信号值或0以清除)。这是调用进程在其父进程死亡时将收到的信号。该值在fork(2)的子进程中被清除,并且在执行set-user-ID或set-group-ID二进制文件时(自Linux 2.4.36 / 2.6.23开始)也会被清除。 - qrdl