使用 {create_group=True} / set_pgid 生成进程在启动时挂起 Docker

Spawning a process with {create_group=True} / set_pgid hangs when starting Docker

给定 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 时,进程挂起。 create_group的作用是用0 in the child, and pid in the parent调用set_pgid。为什么该更改会导致挂起?这是 Docker 中的错误吗? System.Process 中的错误?还是不幸但必要的互动?

这不是 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;
}

如果您直接从交互式 shell 编译它和 运行 ./a.out,它将按您的预期打印 "hello"。这并不奇怪,因为 shell 已经将它放在自己的进程组中,所以它的 setpgid 是一个 no-op。如果你 运行 它与一个中间程序将 child 分叉到 运行 它(sh -c ./a.out\time ./a.out - 注意反斜杠,strace ./a.out,等),然后 setpgid 将把它放在一个新的进程组中,它会像在 Haskell.

中那样挂起

挂起的原因在"Job Control Signals" in the glibc manual中有解释:

Macro: int SIGTTIN

A process cannot read from the user’s terminal while it is running as a background job. When any process in a background job tries to read from the terminal, all of the processes in the job are sent a SIGTTIN signal. The default action for this signal is to stop the process. For more information about how this interacts with the terminal driver, see Access to the Terminal.

Macro: int SIGTTOU

This is similar to SIGTTIN, but is generated when a process in a background job attempts to write to the terminal or set its modes. Again, the default action is to stop the process. SIGTTOU is only generated for an attempt to write to the terminal if the TOSTOP output mode is set; see Output Modes.

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

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

  1. 将进程的标准输入重定向到 TTY 以外的地方
  2. 使用signal or sigaction使进程忽略SIGTTIN信号
  3. 使用sigprocmask阻止进程接收SIGTTIN信号
  4. 调用<a href="http://man7.org/linux/man-pages/man3/tcsetpgrp.3.html" rel="noreferrer">tcsetpgrp</a>(0, getpid())让你的新进程组成为前台进程组(注意:这是最复杂的,因为它本身会导致 SIGTTOU,因此您至少必须暂时忽略该信号)

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

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