概述
runc 是一个基于 OCI 规范实现的程序。在基于 containerd 的容器技术栈上,runc 是一个非常重要但又容易忽略的组件。当执行 docker
, nerdctl
, ctr
等命令时,其实底层都要调用 runc 去运行容器的进程。不过这篇文章仅会涉及到 runc 的输入输出。
因为在日常的开发中,大多数开发者接触到的都是 docker,因此下面会以 docker 为例,结合底层的 runc 来说明容器进程的输入输出。
标准输入输出
在 linux 上,所有的进程都会打开三个文件描述符:
- 0: stdin 标准输入
- 1: stdout 标准输出
- 2: stderr 标准错误输出
在容器技术上,同样离不开这三个基本概念。在使用 docker run/exec 时,其实就是在 linux cgroup 和 namespace 下运行了一个普通的进程而已。所以这样的进程同样会有输入输出,当我们使用 docker run nginx
的时候,nginx 的日志就会打印到终端上,其实就是将 nginx 进程的标准输入输出通过各种手段输出到终端上了。
-i 参数
docker 提供了 -i
参数,来运行交互式的命令,文档上的说明是:
-i, --interactive Keep STDIN open even if not attached
也就是说即使没有使用 docker attach,也会保持 STDIN 打开。这样我们在终端的 STDIN 就会定向到容器内 sh 进程的 STDIN,这样就可以在终端上输入命令行了。
比如 docker run -i nginx sh
。这里就是使用 nginx 镜像启动容器,并运行了 sh 命令,然后终端的 bash 的 STDIN 定向到容器内的 sh。如下图所示:
不过我们也可以发现,这里的输出格式似乎和在终端里 ls
的输出不同。这里就要引入一个新的知识点:tty 与 pty 以及我们的 -t 参数。
-t 参数
官方文档上的说明是:
-t, --tty Allocate a pseudo-TTY
tty 得名于早期的电传打字机(Teletype),随着技术的发展,tty 的概念变得更宽泛,它不再指打字机这样的物理设备,很多时候指的是 linux 内核中的 tty 驱动。当我们使用无 GUI 的 linux 时,比如 server 版本的 ubuntu,我们使用 ctrl+alt+f1~f6
就可以在 tty1~6 之间进行切换。这里的 tty 就是由 linux 内核模拟出来的。
不过在 GUI 场景中,我们打开的终端并不是使用 tty,而是 pseudo-TTY(下文称之为 pty)。pty 也称为伪终端,也就是说它并不是真实的 tty 设备。引入了 pty 之后,终端的数量就不会再有限制了。我们可以打开任意多的 GUI 终端程序,每打开一个 GUI 终端,就会在 /dev/pts/
下生成一个数字的文件,这个文件就是 pty slave,另外全局还有共用的一个 pty master,位于 /dev/ptmx。
- 通过 GUI 终端,我们可以输入字符,输入的字符会发到 pty master 上,pty master 文件描述符,发送到对应的 pty slave。
- pty slave 将输入发送给终端上的 shell 程序,比如 bash。
- bash 上执行的任何命令,都会将输出发送给 bash,bash 再发送给 pty slave,之后再经过 tty driver, pty master 到 gui 终端上显示。
pty 的工作原理如下图:
这时候我们在回到 -t 参数,这里其实就是为容器内的 sh 进程分配了一个 pty slave。当 sh 感知到自己被分配了 pty slave 后,它会采用不同的工作模式。比如会在终端上输出提示符,对输出的格式进行调整,像 bash 之类的还是加上颜色等等。
# 下面这段程序会输出很多行,也说明了在 sh 里执行 ls 时,原本的输出是很多行的。
root@5f96c3475e20:/# ls | while read line; do echo $line;done
bin
boot
dev
docker-entrypoint.d
docker-entrypoint.sh
etc
home
lib
lib64
media
mnt
opt
proc
root
run
sbin
srv
sys
tmp
usr
var
回到 runc
上述提到 stdio,pty 等概念,很好的解决了运行容器时的输入输出的处理。而这些能力也是 runc 本身就提供的,docker 是在其之上进行了封装。
runc 的输入输出处理有四种模式,由 -d(detach),-t 进行组合得到。
terminal && detached
- 上层的管控程序,比如 runc-shim 会创建 unix socket
- runc 运行在容器内,接管容器的 stdio。然后通过 unix socket 将 pty master 和 fd 发送给上层管控程序。
- 将自己的 stdio 通过 pty slave 输入输出。
passthrough && detached
passthrough && foreground
terminal && foreground
参考资料
http://www.wowotech.net/tty_framework/tty_concept.html
http://www.wowotech.net/tty_framework/tty_architecture.html
http://www.wowotech.net/tty_framework/application_view.html
https://blog.51cto.com/u_14592069/5824829
https://dev.to/napicella/linux-terminals-tty-pty-and-shell-192e
https://github.com/opencontainers/runc/blob/main/docs/terminals.md