几十年前,人们将 Teleprinter(电传打字机) 连接到早期的大型计算机上,作为输入和输出设备,将输入的数据发送到计算机,并打印出响应。
电传打字机有输入设备也有输出设备,分别对应的是电传打字机上的按键和纸带。
为了把不同型号的电传打字机接入计算机,需要在操作系统内核安装驱动,为上层应用屏蔽所有的低层细节。
电传打字机通过两根电缆连接:一根用于向计算机发送指令,一根用于接收计算机的输出。这两根电缆插入 UART (Universal Asynchronous Receiver and Transmitter,通用异步接收和发送器)的串行接口连接到计算机。
操作系统包含一个 UART 驱动程序,管理字节的物理传输,包括奇偶校验和流量控制。然后输入的字符序列被传递给 TTY 驱动,该驱动包含一个 line discipline。
line discipline 负责转换特殊字符(如退格、擦除字、清空行),并将收到的内容回传给电传打字机,以便用户可以看到输入的内容。line discipline 还负责对字符进行缓冲,当按下回车键时,缓冲的数据被传递给与 TTY 相关的前台用户进程。用户可以并行的执行几个进程,但每次只与一个进程交互,其他进程在后台工作。
今天电传打字机已经进了博物馆,但 Linux/Unix 仍然保留了当初 TTY 驱动和 line discipline 的设计和功能。终端不再是一个需要通过 UART 连接到计算机上物理设备。终端成为内核的一个模块,它可以直接向 TTY 驱动发送字符,并从 TTY 驱动读取响应然后打印到屏幕上。也就是说,用内核模块模拟物理终端设备,因此被称为终端模拟器(terminal emulator)。
终端模拟器(terminal emulator) 是运行在内核的模块,我们也可以让终端模拟程序运行在用户区。运行在用户区的终端模拟程序,就被称为伪终端(pseudo terminal, PTY)。
PTY 运行在用户区,更加安全和灵活,同时仍然保留了 TTY 驱动和 line discipline 的功能。常用的伪终端有 xterm,gnome-terminal,以及远程终端 ssh。我们以 Ubuntu 桌面版提供的 gnome-terminal 为例,介绍伪终端如何与 TTY 驱动交互。
PTY 是通过打开特殊的设备文件 /dev/ptmx 创建,由一对双向的字符设备构成,称为 PTY master 和 PTY slave。
gnome-terminal 持有 PTY master 的文件描述符 /dev/ptmx。gnome-terminal 负责监听键盘事件,通过PTY master接收或发送字符到 PTY slave,还会在屏幕上绘制来自PTY master的字符输出。
gnome-terminal 会 fork 一个 shell 子进程,并让 shell 持有 PTY slave 的设备文件 /dev/pts/[n],shell 通过 PTY slave 接收字符,并输出处理结果。
PTY master 和 PTY slave 之间是 TTY 驱动,会在 master 和 slave 之间复制数据,并进行会话管理和提供 line discipline 功能。
在 gnome-terminal 中执行 tty 命令,可以看到代表PTY slave的设备文件:
[root@kubevirtci web-console]# tty/dev/pts/0
执行 ps -l 命令,也可以确认 shell 关联的伪终端是 pts/0:
[root@kubevirtci web-console]# ps -lF S UID PID PPID C PRI NI ADDR SZ WCHAN TTY TIME CMD4 S 0 1091 1090 0 80 0 - 1923 do_wai pts/0 00:00:00 bash4 R 0 20771 1091 0 80 0 - 2523 - pts/0 00:00:00 ps
注意到 TTY 这一列指出了当前进程的终端是 pts/0。
下面以实际的例子,看看在 terminal 执行一个命令的全过程。
我们经常通过 ssh 连接到一个远程主机,这时候远程主机上的 ssh server 就是一个伪终端 PTY,它同样持有 PTY master,但 ssh server 不再监听键盘事件,以及在屏幕上绘制输出结果,而是通过 TCP 连接,向 ssh client 发送或接收字符。
我们简单梳理一下远程终端是如何执行命令的。
注意在客户端,我们在屏幕上看到的所有字符都来自于远程服务器。包括我们输入的内容,也是远程服务器上的 line discipline 应用 echo 规则的结果,将这些字符回显了回来。
想进一步探究,可以阅读 TTY驱动的源码 https://github.com/torvalds/linux/blob/master/drivers/tty/tty_io.c 和 line discipline的源码 https://github.com/torvalds/linux/blob/master/drivers/tty/n_tty.c
代码放在: https://github.com/backendcloud/example/tree/master/pts
package mainimport (
"fmt"
"os"
"strconv"
"syscall"
"unsafe")func ioctl(fd, cmd, ptr uintptr) error {
_, _, e := syscall.Syscall(syscall.SYS_IOCTL, fd, cmd, ptr)
if e != 0 {
return e }
return nil}func ptsname(f *os.File) (string, error) {
var n uint32
err := ioctl(f.Fd(), syscall.TIOCGPTN, uintptr(unsafe.Pointer(&n)))
if err != nil {
return "", err }
return "/dev/pts/" + strconv.Itoa(int(n)), nil}func unlockpt(f *os.File) error {
var u int32
// use TIOCSPTLCK with a zero valued arg to clear the slave pty lock
return ioctl(f.Fd(), syscall.TIOCSPTLCK, uintptr(unsafe.Pointer(&u)))}func StartPty() (pty, tty *os.File, err error) {
p, err := os.OpenFile("/dev/ptmx", os.O_RDWR | syscall.O_NOCTTY, 0)
if err != nil {
return nil, nil, err }
sname, err := ptsname(p)
if err != nil {
return nil, nil, err }
err = unlockpt(p)
if err != nil {
return nil, nil, err }
fmt.Println("sname is :", sname)
t, err := os.OpenFile(sname, os.O_RDWR|syscall.O_NOCTTY, 0)
if err != nil {
return nil, nil, err }
return p, t, nil}func main() {
m, s, err := StartPty()
if err != nil {
fmt.Printf("start pty: " , err)
os.Exit(-1)
}
defer m.Close()
defer s.Close()
n, err := m.Write([]byte("hello world!\n")) ;
fmt.Printf("write master, %d:%v\n", n, err)
buf := make([]byte, 256)
n, err = s.Read(buf)
fmt.Println("read from slave:", string(buf[0:n]))
n, err = s.Write([]byte("slave!\n"))
fmt.Printf("write slave, %d:%v\n", n, err)
n, err = m.Read(buf[:])
fmt.Println("read from master:", string(buf[0:n]))}
执行结果:
[root@kubevirtci pts]# go run main.go sname is : /dev/pts/3write master, 13:<nil>read from slave: hello world!write slave, 7:<nil>read from master: hello world![root@kubevirtci pts]#
首先明确一下,这里说的 Web Terminal 是指在网页中实现的,类似于终端客户端软件的东西。
有了前面的铺垫,我们很容易基于WebSocket来实现WebConsole了,具体的架构图如下所示:
实现 Web Terminal 现在比较主流的实现方案是:在浏览器端,需要嵌入xterm.js插件,实现对终端的输入输出支持能力。服务端使用 node-pty 做 PTY 的操作工具。而通讯方面,SSH 用的是 TCP,Web 上能用的也就是 WebSocket 了。