这不是 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 是一个空操作。如果您使用派生子运行它的中间程序运行它(sh -c ./a.out、\time ./a.out - 注意反斜杠、strace ./a.out 等),那么setpgid 会将其放入一个新的进程组中,它会像在 Haskell 中一样挂起。
挂起的原因在"Job Control Signals" in the glibc manual中有解释:
宏:int SIGTTIN
进程在作为后台作业运行时无法从用户终端读取。当后台作业中的任何进程尝试从终端读取时,作业中的所有进程都会收到SIGTTIN 信号。此信号的默认操作是停止进程。有关它如何与终端驱动程序交互的更多信息,请参阅Access to the Terminal。
宏:int SIGTTOU
这类似于SIGTTIN,但在后台作业中的进程尝试写入终端或设置其模式时生成。同样,默认操作是停止进程。 SIGTTOU 仅在设置了TOSTOP 输出模式时才会生成尝试写入终端;见Output Modes。
当你docker run -it 时,Docker 将尝试从标准输入读取,即使容器内的命令没有。由于您刚刚创建了一个新的进程组,并且您没有将其设置为在前台,因此它被视为后台作业。因此,Docker 被 SIGTTIN 停止,这导致它看起来挂起。
以下是解决此问题的选项列表:
- 将进程的标准输入重定向到 TTY 以外的其他地方
- 使用
signal 或sigaction 使进程忽略SIGTTIN 信号
- 使用
sigprocmask 阻止进程接收SIGTTIN 信号
- 调用
@987654327@(0, getpid()) 使您的新进程组成为前台进程组(注意:这是最复杂的,因为它本身会导致SIGTTOU,所以无论如何您都必须至少暂时忽略该信号)
选项 2 和 3 也仅在程序实际上不需要标准输入时才有效,Docker 就是这种情况。当SIGTTIN 没有停止该过程时,从标准输入读取仍然会失败并显示EIO,因此如果您确实要读取数据,那么您需要使用选项4(并记住一旦孩子将其设置回来退出)。
如果您设置了 TOSTOP(这不是默认设置),那么您必须对 SIGTTOU 或标准输出和标准错误重复修复(选项 4 除外,它不需要完全重复)。