为什么 vim 成为孤儿进程时会崩溃?

Why does vim crash when it becomes an orphan process?

#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <sys/wait.h>

int main()
{
    int pid = fork();
    
    if (pid) {
        sleep(5);
        // wait(NULL); // works fine when waited for it.
    } else {
        execlp("vim", "vim", (char *)NULL);
    }
}

当我 运行 这段代码时,vim 运行s 通常会在 5 秒后崩溃(即当其 parent 退出时)。当我等待它时(即不让它成为孤儿进程),代码工作得很好。

为什么成为孤儿进程在这里成为一个问题?它是 vim 特有的东西吗?

为什么 vim 甚至可以看到这个东西?我以为只有 parent 知道它的 children 什么时候死。但是在这里,我看到 child 以某种方式注意到它在被采用时会发生一些事情并以某种方式崩溃。 children 进程在 parent 也死掉时会收到通知吗?

当我运行这个代码时,我在崩溃后得到这个输出:

Vim: Error reading input, exiting...
Vim: preserving files...
Vim: Finished.

这实际上是因为 shell 正在执行分叉 Vim!

的二进制文件

当shell运行前台命令时,它会创建一个新的进程组,并使其成为附加到shell的终端的前台进程组。在bash 5.0中,您可以在give_terminal_to(), which uses tcsetpgrp()中找到转移此职责的代码以设置前台进程组。

需要正确设置一个终端的前台进程组,这样前台的程序运行才能得到终端的信号(比如Ctrl+C发送中断信号,Ctrl+ Z 发送终端停止信号以暂停进程)并以 full-screen 程序(例如 Vim 通常执行的方式更改终端设置。 (前台进程组的主题有点超出这个问题的范围,只是在这里提到它,因为它参与了响应。)

当shell执行的进程(更准确的说是管道)终止时,shell会收回前台进程组,使用相同的give_terminal_to()代码调用它与 shell 的进程组。

这通常没问题,因为在执行的管道完成时,该进程组上通常没有剩余进程,或者如果有的话,它们通常不会保留在终端上(例如,如果您从 shell 启动后台守护程序,该守护程序通常会关闭 stdin/stdout/stderr 流以放弃对终端的访问。)

但是您提出的设置并非如此,其中 Vim 仍然附加到终端和前台进程组的一部分。当 parent 进程退出时,shell 假定管道已完成,它将前台进程组设置回自身,从 [=65= 所在的前前台进程组中“窃取”它] 是。因此,下次 Vim 尝试从终端读取时,读取将失败并且 Vim 将退出并显示您报告的消息。

您自己了解 parent 处理退出确实 不会 影响 Vim 的一种方法是 运行 它通过 strace。例如,使用以下命令(假设 ./vim-launcher 是您的二进制文件):

$ strace -f -o /tmp/vim-launcher.strace ./vim-launcher

由于strace是运行跟随分叉的-f选项,它也会在启动时开始追踪Vim。 shell 将执行 strace(而不是 vim-launcher),因此它的前台管道只会在 strace 停止 运行 时结束。并且 strace 不会停止 运行 直到 Vim 退出。 Vim 将在 5 秒后正常工作,即使它已被重新parented 初始化。

过去也有一个 fghack tool,守护进程的一部分,它完成了相同的阻塞任务,直到所有分叉的 children 退出。它将通过创建一个新管道并让它生成的进程继承该管道来实现这一点,以一种将被所有其他分叉 children 自动继承的方式。这样,它可能会阻塞,直到该管道文件描述符的所有副本都被关闭,这通常只发生在所有进程退出时(除非后台进程不顾一切地关闭所有继承的文件描述符,但这实际上是在说明它们不他们不想被跟踪,到那时他们很可能已经放弃了对终端的访问。)