4

给定一个 Linux 系统,在 Haskell GHCi 8.8.3 中,我可以运行 Docker 命令:

System.Process> withCreateProcess (shell "docker run -it alpine sh -c \"echo hello\""){create_group=False} $ \_ _ _ pid -> waitForProcess pid
hello
ExitSuccess

但是,当我切换到create_group=True进程挂起。的效果是在 childparentcreate_group中调用set_pgidwith 。为什么这种变化会导致挂起?这是 Docker 中的错误吗?System.Process 中的错误?还是不幸但必要的互动?0pid

4

1 回答 1

6

这不是 Haskell 中的错误或 Docker 中的错误,而只是进程组的工作方式。考虑这个 C 程序:

#include <sys/types.h>
#include <stdio.h>
#include <unistd.h>

int main(void) {
    if(setpgid(0, 0)) {
        perror("setpgid");
        return 1;
    }
    execlp("docker", "docker", "run", "-it", "alpine", "echo", "hello", (char*)NULL);
    perror("execlp");
    return 1;
}

如果你编译它并./a.out直接从你的交互式 shell 运行,它会像你期望的那样打印“hello”。这并不奇怪,因为 shell 已经将它放在自己的进程组中,所以它setpgid是无操作的。如果您使用派生子运行它的中间程序运行它(sh -c ./a.out, \time ./a.out- 注意反斜杠strace ./a.out等),那么setpgid它将把它放在一个新的进程组中,它会像在 Haskell 中一样挂起。

glibc 手册中的“作业控制信号”中解释了挂起的原因:

宏:int SIGTTIN

进程在作为后台作业运行时无法从用户终端读取。当后台作业中的任何进程尝试从终端读取时,作业中的所有进程都会收到一个SIGTTIN信号。此信号的默认操作是停止进程。有关它如何与终端驱动程序交互的更多信息,请参阅访问终端

宏:int SIGTTOU

这类似于SIGTTIN,但在后台作业中的进程尝试写入终端或设置其模式时生成。同样,默认操作是停止进程。如果设置了输出模式,SIGTTOU则仅为尝试写入终端而生成;TOSTOP请参阅输出模式

当你docker run -it做某事时,即使容器内的命令没有,Docker 也会尝试从标准输入读取。由于您刚刚创建了一个新的进程组,并且您没有将其设置为在前台,因此它被视为后台作业。因此,Docker 被 停止SIGTTIN,这导致它看起来挂起。

以下是解决此问题的选项列表:

  1. 将进程的标准输入重定向到 TTY 以外的其他地方
  2. 使用signalorsigaction使进程忽略SIGTTIN信号
  3. 用于sigprocmask阻止进程接收SIGTTIN信号
  4. 调用tcsetpgrp(0, getpid())以使您的新进程组成为前台进程组(注意:这是最复杂的,因为它本身会导致SIGTTOU,因此无论如何您都必须至少暂时忽略该信号)

选项 2 和 3 也仅在程序实际上不需要标准输入时才有效,Docker 就是这种情况。当SIGTTIN不停止该过程时,从标准输入读取仍然会失败EIO,因此如果确实有您想要读取的数据,那么您需要使用选项 4(并记住在孩子退出后将其设置回来)。

如果您已TOSTOP设置(这不是默认设置),那么您必须SIGTTOU为标准输出和标准错误重复修复或(选项 4 除外,它根本不需要重复)。

于 2020-05-19T00:05:02.920 回答