- 容器运行时
- 云原生
【免费下载链接】docker-ce
:warning: This repository is deprecated and will be archived (Docker CE itself is NOT deprecated) see the https://github.com/docker/docker-ce/blob/master/README.md :warning:
导读:本篇文章以 docker-ce 仓库内 vendored 的github.com/creack/pty(v1.1.11)为研究对象,系统讲解 Go 语言操作 Unix 伪终端(pseudo-terminal,简称 pty)的核心 API、跨平台实现原理与两个完整的实战示例(命令捕获与交互式 Shell)。文章同时结合 docker-ce 的源码与集成测试,展示 pty 包如何支撑docker run -it、docker attach等交互式容器功能,读完你可以直接用 pty 写出自己的终端程序,也能理解 Docker 交互式会话背后的底层机制。
一、什么是伪终端(pty)
伪终端是 Unix 系统提供的一对设备文件:ptmx(伪终端主设备,master)与对应的pts(伪终端从设备,slave)。程序把从设备当作"标准终端"交给子进程(例如bash、grep),而宿主进程通过主设备读写数据,从而可以模拟出一个完整的终端环境:子进程认为自己在跟真实终端对话,能够输出 ANSI 转义序列、响应窗口大小变化(SIGWINCH)、支持行编辑与信号控制。
creack/pty正是把这一套 POSIX 机制封装成简洁 Go API 的库。在 docker-ce 中,它被同时 vendored 在 components/engine/vendor.conf 与 components/cli/vendor.conf,固定版本为v1.1.11(提交2a38352e8b4d7ab6c336eef107e42a55e72e7fbc),主要服务于docker run -it、docker attach、docker exec -it等交互式命令的集成测试与 TTY 模拟场景。
二、安装与依赖
原文档给出的安装方式是基于GOPATH时代的经典命令:
go get github.com/creack/pty从仓库内的 go.mod 可以看出,该模块本身go 1.13+,且不依赖任何第三方包(vendor 目录下仅有自身代码),非常适合直接go get或作为依赖引入。Docker 官方则通过vendor.conf将其固定为精确提交以保证可复现构建。
三、API 总览:一个文件读懂核心接口
包入口定义在 doc.go,公开的核心函数与类型如下:
| API | 作用 |
|---|---|
Open() (pty, tty *os.File, err error) | 打开一对主/从伪终端,返回 ptmx 与 pts 两个文件句柄 |
Start(c *exec.Cmd) (pty *os.File, err error) | 为命令分配伪终端并启动进程(等价于StartWithSize(c, nil)) |
StartWithSize(c *exec.Cmd, sz *Winsize) | 启动前先按Winsize设置终端尺寸 |
StartWithAttrs(c, sz, attrs) | 完全自定义SysProcAttr启动(一般用不到) |
Setsize(t, ws) | 调整终端行列尺寸(内部 ioctlTIOCSWINSZ) |
GetsizeFull(t)/Getsize(t) | 读取终端完整尺寸描述 / 仅行列数 |
InheritSize(pty, tty) | 把 ptmx 的尺寸同步到 pts,用于响应 SIGWINCH |
ErrUnsupported | 在不支持伪终端的平台(如 Windows)上由Open返回 |
其中Winsize结构体定义于 util.go,对应内核的struct winsize:
type Winsize struct { Rows uint16 // ws_row: 行数(以单元格计) Cols uint16 // ws_col: 列数(以单元格计) X uint16 // ws_xpixel: 像素宽度 Y uint16 // ws_ypixel: 像素高度 }Start 系列:从零到会话控制
值得关注的是Start的内部行为,见 run.go:
- 自动把
tty(从设备)同时挂接为子进程的Stdin、Stdout、Stderr(仅当这些字段为 nil 时); - 自动设置
SysProcAttr.Setsid = true,让子进程开启新会话(new session); - 自动设置
SysProcAttr.Setctty = true,让该伪终端成为子进程的控制终端; StartWithSize会在c.Start()之前先调用Setsize完成初始窗口尺寸设定。
而StartWithAttrs是为"不需要控制终端"这类边界场景保留的底层接口,其注释明确提示"一般无需使用"。
四、底层原理:Linux 上的一次Open()完整旅程
在 Linux 上,open()的实现位于 pty_linux.go,步骤清晰:
os.OpenFile("/dev/ptmx", os.O_RDWR, 0)—— 打开伪终端主设备(即 ptmx);- 通过 ioctl
TIOCGPTN查询对应的pts 编号,拼出/dev/pts/<N>路径(ptsname); - 通过 ioctl
TIOCSPTLCK传入 0,解锁该从设备(unlockpt); - 以
os.O_RDWR|syscall.O_NOCTTY打开/dev/pts/<N>,得到从设备句柄; - 返回
(ptmx, pts)一对文件,且出错时自动关闭已打开的 ptmx(defer保证资源不泄漏)。
而 BSD/Darwin 平台则走TIOCPTYGNAME+grantpt(授予访问权限)+unlockpt的流程,见 pty_darwin.go;ioctl 请求码在 ioctl_bsd.go 中按<sys/ioccom.h>的宏规则手工拼装(_IOR/_IOW/_IOWR)。如果目标平台不在支持列表内,pty_unsupported.go 会直接返回ErrUnsupported。整个库通过 build tags 按linux / darwin / freebsd / dragonfly / openbsd / solaris分发平台实现,并配套了ztypes_*.go(由 cgo-godefs 生成)记录各架构的 ioctl 常量类型。
五、示例一:用 pty 捕获交互式命令输出
原文档第一个示例非常精炼:启动一个带颜色的grep,向其中写入多行文本,观察过滤后的结果。完整代码:
package main import ( "github.com/creack/pty" "io" "os" "os/exec" ) func main() { c := exec.Command("grep", "--color=auto", "bar") f, err := pty.Start(c) if err != nil { panic(err) } go func() { f.Write([]byte("foo\n")) f.Write([]byte("bar\n")) f.Write([]byte("baz\n")) f.Write([]byte{4}) // EOT }() io.Copy(os.Stdout, f) }执行流程拆解:
pty.Start(c)完成三件事:打开主/从设备对、把 pts 挂到子进程三个标准流、以新会话+控制终端方式启动grep;- 主 goroutine 通过返回的
f(ptmx)向grep的 stdin 依次写入foo、bar、baz与一个EOT 字节(0x04,即 Ctrl-D)——之所以需要 EOT,是因为此时grep读取的是"终端"而非普通管道,必须靠 EOT 信号告知输入结束; io.Copy(os.Stdout, f)在另一个方向上把grep的(带颜色转义序列的)输出实时复制到程序自己的 stdout。
这里的关键认知是:把伪终端主设备当作普通io.Reader/io.Writer即可,这就是 pty 包 API 设计的精髓。
六、示例二:打造完整交互式 Shell
原文档的第二个示例更贴近真实终端模拟器,它启动一个bash,并打通"本地 stdin → pty → bash → pty → 本地 stdout"的完整回路:
package main import ( "io" "log" "os" "os/exec" "os/signal" "syscall" "github.com/creack/pty" "golang.org/x/crypto/ssh/terminal" ) func test() error { // 创建任意命令。 c := exec.Command("bash") // 以 pty 启动命令。 ptmx, err := pty.Start(c) if err != nil { return err } // 结束时务必关闭 pty。 defer func() { _ = ptmx.Close() }() // Best effort. // 处理 pty 尺寸(窗口大小)。 ch := make(chan os.Signal, 1) signal.Notify(ch, syscall.SIGWINCH) go func() { for range ch { if err := pty.InheritSize(os.Stdin, ptmx); err != nil { log.Printf("error resizing pty: %s", err) } } }() ch <- syscall.SIGWINCH // 初始尺寸同步。 // 将 stdin 置为 raw 模式。 oldState, err := terminal.MakeRaw(int(os.Stdin.Fd())) if err != nil { panic(err) } defer func() { _ = terminal.Restore(int(os.Stdin.Fd()), oldState) }() // Best effort. // 双向复制:stdin → pty,pty → stdout。 go func() { _, _ = io.Copy(ptmx, os.Stdin) }() _, _ = io.Copy(os.Stdout, ptmx) return nil } func main() { if err := test(); err != nil { log.Fatal(err) } }这个示例内含三个必须掌握的终端编程要点:
- SIGWINCH 窗口尺寸联动:用户改变本地终端窗口大小时,内核向进程发送
SIGWINCH信号。代码用signal.Notify捕获它,调用pty.InheritSize(os.Stdin, ptmx)把本地终端(os.Stdin)的尺寸同步给伪终端——其实现(见 util.go)就是"先GetsizeFull再Setsize"。注意启动后要主动补发一次SIGWINCH,让 ptmx 初始尺寸与本地终端一致; - raw 模式:
terminal.MakeRaw关闭本地终端的行缓冲、回显与信号处理,使击键能"原样"穿过 pty 直达 bash;结束后必须terminal.Restore恢复,否则终端会处于"失魂"状态; - 双向 io.Copy:一条 goroutine 把本地键盘输入灌入 ptmx,主流程把 bash 的输出(含提示符、颜色)抄回 stdout,构成完整的交互回路。
七、在 docker-ce 中的真实应用:集成测试与 TTY 模拟
pwd 包在 docker-ce 中并非空转的第三方依赖,而是集成测试基础设施的重要组成:
- docker_cli_attach_unix_test.go:用
pty.Open()拿到主/从设备对,把tty挂给docker attach命令,再通过主设备cpty.Write写入按键。例如TestAttachAfterDetach向 pty 写入字节16与17(即 Ctrl-P/Ctrl-Q,Docker 默认的 detach 键序列),验证docker run -ti后先 detach 再 attach 的完整闭环;TestAttachClosedOnContainerStop则验证容器停止后 attach 会话正常结束(对应 issue #9860); - docker_cli_run_unix_test.go:同样用
pty.Open()模拟交互式docker run -ti,写入hello、Ctrl-P 等按键以验证 TTY 容器行为; - proxy_signal_test.go:e2e 测试用
pty.Start(cmd)启动docker run -i -t --init ... sleep 30,随后syscall.Kill(pid, syscall.SIGTERM),验证 CLI 能把信号代理进容器、容器按预期退出(对应 moby/moby#28872)。
这些用例完整展示了 pty 包的经典玩法:在自动化测试里用程序模拟"人类用户 + 终端"——这正是-it交互式容器无法用普通管道测试的原因。pty 提供的"假装是终端"能力,让 CI 可以对最棘手的交互流程做确定性验证。
八、注意事项与最佳实践
- 必须关闭文件:
Start返回的 ptmx 和内部打开的 tty 都是真实文件描述符,示例中统一使用defer ptmx.Close(),并在出错路径上由库内部负责清理(见 run.go 与 pty_linux.go 的 Best effort 关闭); - EOT 而非 EOF:向伪终端写数据模拟输入结束要用字节
0x04(Ctrl-D),不能依赖普通管道的关闭语义; - 平台限制:
Open()在不支持伪终端的平台上返回ErrUnsupported(doc.go),Windows 上需要winpty之类的替代方案; - 配合 raw mode 使用:若要实现"所见即所得"的交互终端,务必先
MakeRaw再复制数据,结束时恢复终端状态; - 尺寸同步不可省:忽略 SIGWINCH 会导致子进程(如
vim、top)渲染错位。
九、小结
creack/pty用不到十个公开函数就把 Unix 伪终端的全部能力——打开设备对、挂载标准流、会话控制、尺寸管理——封装成了符合 Go 习惯的io.Reader/io.Writer接口。从底层看,它是TIOCGPTN/TIOCSPTLCK/TIOCSWINSZ等 ioctl 的跨平台封装;从应用层看,它支撑着 docker-ce 交互式容器的测试模拟与真实终端程序开发。掌握本文的两个示例,你便拥有了在 Go 中编写 shell 模拟器、终端复用器与自动化交互测试的全部基石。
延伸阅读(均在当前仓库内):包级文档 doc.go、进程启动逻辑 run.go、尺寸管理 util.go、Linux 平台实现 pty_linux.go,以及 docker-ce 中的实战用例 docker_cli_attach_unix_test.go 与 proxy_signal_test.go。
- 容器运行时
- 云原生
【免费下载链接】docker-ce
:warning: This repository is deprecated and will be archived (Docker CE itself is NOT deprecated) see the https://github.com/docker/docker-ce/blob/master/README.md :warning:
相关推荐
FanControl指南:10分钟免费驯服风扇噪音与高温
FanControl指南:10分钟免费驯服风扇噪音与高温 风扇忽高忽低地转,温度曲线却几乎走平——这通常是出厂写死的散热策略在起作用。FanControl 是一
桌面应用智能硬件Docker CLI 中的 Unix 伪终端基石:creack/pty 原理剖析与 Go 实战指南
Docker CLI 中的 Unix 伪终端基石:creack/pty 原理剖析与 Go 实战指南 导读 github.com/creack/pty 是 Go
CLI开发工具深入解析 kr/pty:在 Go 中使用 Unix 伪终端(PTY)的完整指南
深入解析 kr/pty:在 Go 中使用 Unix 伪终端(PTY)的完整指南 本指南以 scope 项目 vendor 目录中托管的 kr/pty (v1.1
云原生可观测性容器编排运维
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考