runc 的输入输出

概述

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。如下图所示:

https://www.myway5.com/wp-content/uploads/2023/03/Snipaste_2023-03-08_13-01-18.png

不过我们也可以发现,这里的输出格式似乎和在终端里 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 内核模拟出来的。

https://www.myway5.com/wp-content/uploads/2023/03/Snipaste_2023-03-08_13-18-38.png

不过在 GUI 场景中,我们打开的终端并不是使用 tty,而是 pseudo-TTY(下文称之为 pty)。pty 也称为伪终端,也就是说它并不是真实的 tty 设备。引入了 pty 之后,终端的数量就不会再有限制了。我们可以打开任意多的 GUI 终端程序,每打开一个 GUI 终端,就会在 /dev/pts/ 下生成一个数字的文件,这个文件就是 pty slave,另外全局还有共用的一个 pty master,位于 /dev/ptmx。

  1. 通过 GUI 终端,我们可以输入字符,输入的字符会发到 pty master 上,pty master 文件描述符,发送到对应的 pty slave。
  2. pty slave 将输入发送给终端上的 shell 程序,比如 bash。
  3. bash 上执行的任何命令,都会将输出发送给 bash,bash 再发送给 pty slave,之后再经过 tty driver, pty master 到 gui 终端上显示。

pty 的工作原理如下图:

https://www.myway5.com/wp-content/uploads/2023/03/Snipaste_2023-03-08_13-28-22.png

这时候我们在回到 -t 参数,这里其实就是为容器内的 sh 进程分配了一个 pty slave。当 sh 感知到自己被分配了 pty slave 后,它会采用不同的工作模式。比如会在终端上输出提示符,对输出的格式进行调整,像 bash 之类的还是加上颜色等等。

Untitled

# 下面这段程序会输出很多行,也说明了在 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

https://www.myway5.com/wp-content/uploads/2023/03/Snipaste_2023-03-08_13-44-19.png

  1. 上层的管控程序,比如 runc-shim 会创建 unix socket
  2. runc 运行在容器内,接管容器的 stdio。然后通过 unix socket 将 pty master 和 fd 发送给上层管控程序。
  3. 将自己的 stdio 通过 pty slave 输入输出。

passthrough && detached

https://www.myway5.com/wp-content/uploads/2023/03/Snipaste_2023-03-08_14-02-40.png

passthrough && foreground

https://www.myway5.com/wp-content/uploads/2023/03/Snipaste_2023-03-08_14-03-28.png

terminal && foreground

https://www.myway5.com/wp-content/uploads/2023/03/Snipaste_2023-03-08_14-03-22.png

参考资料

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

发表回复

您的邮箱地址不会被公开。 必填项已用 * 标注

此站点使用Akismet来减少垃圾评论。了解我们如何处理您的评论数据