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

zxlwwiss  于 2022-09-21  发布在  Unix
关注(0)|答案(23)|浏览(683)

假设我有一个进程,它正好产生一个子进程。现在,当父进程出于任何原因(正常或异常,通过KILL、^C、Assert Failure或任何其他原因)退出时,我希望子进程死亡。如何正确地做到这一点呢?

有关堆栈溢出的一些类似问题:

关于Windows堆栈溢出的一些类似问题:

camsedfj

camsedfj16#

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

父进程**有可能捕获大多数可能的终止事件,并试图在该点上杀伤子进程,但总有一些无法捕获。

例如,任何进程都无法捕获SIGKILL。当内核处理此信号时,它将终止指定的进程,而不通知该进程。

扩大这个类比--唯一的另一种标准做法是,当孩子发现自己不再有父母时,就会自杀。

对于prctl(2),有一种仅适用于Linux的方法--请参阅其他答案。

5sxhfpxr

5sxhfpxr17#

受这里另一个答案的启发,我想出了以下All-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);
}

此方法有两个小注意事项:

  • 如果你故意杀死中间进程,那么当父母死亡时,孩子就不会被杀死。
  • 如果子进程在父进程之前退出,则中间进程将尝试终止原始子进程ID,该进程现在可能引用不同的进程。(可以在中间进程中使用更多代码来修复此问题。)

顺便说一句,我使用的实际代码是用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)
vwhgwdsa

vwhgwdsa18#

子进程是否有进出父进程的管道?如果是这样,则在写入时会收到SIGPIPE,或者在读取时会收到EOF--这些情况都可以检测到。

n3h0vuf2

n3h0vuf219#

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

hof1towb20#

如果无法修改子进程,可以尝试执行类似以下操作:

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 */

这将在启用作业控制的情况下从外壳进程中运行子进程。子进程是在后台产生的。外壳程序等待换行符(或EOF),然后杀死孩子。

当父进程死亡时--无论是什么原因--它将关闭其管道的末端。子外壳将从读取中获得EOF,并继续终止后台的子进程。

xxslljrj

xxslljrj21#

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

如果我们确定负责采用所有orphans的系统进程的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并且具有PID1是不可移植的。POSIX.1-2008规定:

调用进程的所有子进程和僵尸进程的父进程ID应设置为实现定义的系统进程的进程ID。也就是说,这些进程将由一个特殊的系统进程继承。

传统上,采用所有孤儿的系统进程是PID1,即init,它是所有进程的祖先。

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

nr7wwzry

nr7wwzry22#

过去,我通过在“子”中运行“原始”代码和在“父”中运行“衍生”代码(即:在fork()之后颠倒通常的测试含义)来实现这一点。然后在“衍生”代码中诱捕SIGCHLD..。

在你的情况下可能是不可能的,但当它起作用时很可爱。

falq053o

falq053o23#

我也在努力解决同样的问题,因为我的程序必须在OS X上运行,所以只支持Linux的解决方案不适用于我。

我得出了与本页面上其他人相同的结论--当父母去世时,没有一种与POSIX兼容的方式来通知孩子。所以我做了一件仅次于最好的事情--让孩子们投票。

当父进程死亡时(无论出于何种原因),子进程的父进程将成为进程1。如果子进程只是定期轮询,它可以检查其父进程是否为1。如果是,则子进程应该退出。

这不是很好,但它很有效,而且比本页面其他地方建议的TCP套接字/锁文件轮询解决方案更容易。

相关问题