这不是 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
,这导致它看起来挂起。
以下是解决此问题的选项列表:
- 将进程的标准输入重定向到 TTY 以外的其他地方
- 使用
signal
orsigaction
使进程忽略SIGTTIN
信号
- 用于
sigprocmask
阻止进程接收SIGTTIN
信号
- 调用
tcsetpgrp(0, getpid())
以使您的新进程组成为前台进程组(注意:这是最复杂的,因为它本身会导致SIGTTOU
,因此无论如何您都必须至少暂时忽略该信号)
选项 2 和 3 也仅在程序实际上不需要标准输入时才有效,Docker 就是这种情况。当SIGTTIN
不停止该过程时,从标准输入读取仍然会失败EIO
,因此如果确实有您想要读取的数据,那么您需要使用选项 4(并记住在孩子退出后将其设置回来)。
如果您已TOSTOP
设置(这不是默认设置),那么您必须SIGTTOU
为标准输出和标准错误重复修复或(选项 4 除外,它根本不需要重复)。