我对终端Terminal是怎么回事困惑了很久。
但在上个星期,我使用 xterm.js在浏览器中显示了一个交互式终端,我终于想到要问一个相当基本的问题:当你在终端中按下键盘上的一个键(比如Delete,或Escape,或a),发送了哪些字节?
像往常一样,我们将通过做一些实验来回答这个问题,看看会发生什么 : )
首先,我想说的是,用 xterm.js在浏览器中显示一个终端可能看起来像一个新事物,但它真的不是。在 70 年代,计算机很昂贵。因此,一个机构的许多员工会共用一台电脑,每个人都可以有自己的 “终端” 来连接该电脑。
例如,这里有一张 70 年代或 80 年代的 VT100 终端的照片。这看起来像是一台计算机(它有点大!),但它不是 —— 它只是显示实际计算机发送的任何信息。
EC_VT100_terminal.jpg">
当然,在 70 年代,他们并没有使用 Websocket 来做这个,但来回发送的信息的方式和当时差不多。
(照片中的终端是来自西雅图的 活电脑博物馆Living Computer Museum,我曾经去过那里,并在一个非常老的 Unix 系统上用ed编写了 FizzBuzz,所以我有可能真的用过那台机器或它的一个兄弟姐妹!我真的希望活电脑博物馆能再次开放,能玩到老式电脑是非常酷的。)
很明显,如果你想连接到一个远程计算机(用 ssh或使用xterm.js和 Websocket,或其他任何方式),那么需要在客户端和服务器之间发送一些信息。
具体来说:
客户端需要发送用户输入的键盘信息(如ls -l)。服务器需要告诉客户端在屏幕上显示什么。
让我们看看一个真正的程序,它在浏览器中运行一个远程终端,看看有哪些信息会被来回发送!
我在 GitHub 上发现了这个叫做 goterm的小程序,它运行一个 Go 服务器,可以让你在浏览器中使用xterm.js与终端进行交互。这个程序非常不安全,但它很简单,很适合学习。
我 复刻了它,使它能与最新的xterm.js一起工作,因为它最后一次更新是在 6 年前。然后,我添加了一些日志语句,以打印出每次通过 WebSocket 发送/接收的字节数。
让我们来看看在几个不同的终端交互过程中的发送和接收情况吧!
首先,让我们运行 ls。下面是我在xterm.js终端上看到的情况:
以下是发送和接收的内容:(在我的代码中,我记录了每次客户端发送的字节:sent: [bytes],每次它从服务器接收的字节:recv: [bytes])
我在这个输出中注意到 3 件事:
好了,现在我们来做一些稍微复杂的事情。
接下来,让我们看看当我们用 Ctrl+C中断一个进程时会发生什么。下面是我在终端中看到的情况:
而这里是客户端发送和接收的内容。
当我按下 Ctrl+C时,客户端发送了\x03。如果我查 ASCII 表,\x03是 “文本结束”,这似乎很合理。我认为这真的很酷,因为我一直对Ctrl+C的工作原理有点困惑 —— 很高兴知道它只是在发送一个\x03字符。
我相信当我们按 Ctrl+C时,cat被中断的原因是服务器端的 Linux 内核收到这个\x03字符,识别出它意味着 “中断”,然后发送一个SIGINT到拥有伪终端的进程组。所以它是在内核而不是在用户空间处理的。
让我们试试完全相同的事情,只是用 Ctrl+D。下面是我在终端看到的情况:
而这里是发送和接收的内容:
它与 Ctrl+C非常相似,只是发送\x04而不是\x03。很好!\x04对应于 ASCII “传输结束”。
接下来我开始好奇 —— 如果我发送 Ctrl+e,会发送什么字节?
事实证明,这只是该字母在字母表中的编号,像这样。
另外,Ctrl+Shift+b的作用与Ctrl+b完全相同(它写的是0x2)。
键盘上的其他键呢?下面是它们的映射情况:
那 Alt呢?根据我的实验(和一些搜索),似乎Alt和Escape在字面上是一样的,只是按Alt本身不会向终端发送任何字符,而按Escape本身会。所以:
让我们再看一个例子!
下面是我运行文本编辑器 nano时发送和接收的内容:
你可以看到一些来自用户界面的文字,如 “GNU nano 6.2”,而这些 \x1b[27m的东西是转义序列。让我们来谈谈转义序列吧!
上面这些 nano发给客户端的\x1b[东西被称为“转义序列”或 “转义代码”。这是因为它们都是以 “转义”字符\x1b开头。它们可以改变光标的位置,使文本变成粗体或下划线,改变颜色,等等。维基百科介绍了一些历史,如果你有兴趣的话可以去看看。
举个简单的例子:如果你在终端运行
它将打印出 “hi there”,其中 “hi” 是红色的,“there” 是黑色的。本页有一些关于颜色和格式化的转义代码的例子。
我认为有几个不同的转义代码标准,但我的理解是,人们在 Unix 上使用的最常见的转义代码集来自 VT100(博客文章顶部图片中的那个老终端),在过去的 40 年里没有真正改变。
转义代码是为什么你的终端会被搞乱的原因,如果你 cat一些二进制数据到你的屏幕上 —— 通常你会不小心打印出一堆随机的转义代码,这将搞乱你的终端 —— 如果你cat足够多的二进制数据到你的终端,那里一定会有一个0x1b的字节。
在前面几节中,我们谈到了 Home键是如何映射到\x1b[H的。这 3 个字节是Escape + [ + H(因为Escape是\x1b)。
如果我在 xterm.js终端手动键入Escape,然后是[,然后是H,我就会出现在行的开头,与我按下Home完全一样。
我注意到这在我的电脑上的 Fish shell 中不起作用 —— 如果我键入 Escape,然后输入[,它只是打印出[,而不是让我继续转义序列。我问了我的朋友 Jesse,他写过一堆 Rust 终端代码,Jesse 告诉我,很多程序为转义代码实现了一个超时—— 如果你在某个最小的时间内没有按下另一个键,它就会决定它实际上不再是一个转义代码了。
显然,这在 Fish shell 中可以用 fish_escape_delay_ms来配置,所以我运行了set fish_escape_delay_ms 1000,然后我就能用手输入转义代码了。工作的很好!
我想在这里暂停一下,我觉得你按下的键被映射到字节的方式是非常奇怪的。比如,如果我们今天从头开始设计按键的编码方式,我们可能不会把它设置成这样:
但所有这些都是在 70 年代或 80 年代或什么时候设计的,然后需要永远保持不变,以便向后兼容,所以这就是我们得到的东西 :smiley:
在终端中,并不是所有你能做的事情都是通过来回发送字节发生的。例如,当终端被调整大小时,我们必须以不同的方式告诉 Linux 窗口大小已经改变。
下面是 goterm中用来做这件事的 Go 代码的样子:
这是在使用 ioctl系统调用。我对ioctl的理解是,它是一个系统调用,用于处理其他系统调用没有涉及到的一些随机的东西,通常与 IO 有关,我猜。
syscall.TIOCSWINSZ是一个整数常数,它告诉ioctl我们希望它在本例中做哪件事(改变终端的窗口大小)。
在这篇文章中,我们一直在讨论远程终端,即客户端和服务器在不同的计算机上。但实际上,如果你使用像 xterm 这样的终端模拟器,所有这些工作方式都是完全一样的,只是很难注意到,因为这些字节并不是通过网络连接发送的。
关于终端,肯定还有很多东西要了解(我们可以讨论更多关于颜色,或者原始与熟化模式,或者 Unicode 支持,或者 Linux 伪终端界面),但我将在这里停止,因为现在是晚上 10 点,这篇文章有点长,而且我认为我的大脑今天无法处理更多关于终端的新信息。
感谢 Jesse Luehrs回答了我关于终端的十亿个问题,所有的错误都是我的 :smiley:
via: https://jvns.ca/blog/2022/07/20/pseudoterminals/
作者:Julia Evans选题:lujun9972译者:wxy校对:wxy
本文由 LCTT原创编译,Linux中国荣誉推出