當你在終端上按下一個鍵時會發生什麼?
我對 終端 是怎麼回事困惑了很久。
但在上個星期,我使用 xterm.js 在瀏覽器中顯示了一個互動式終端,我終於想到要問一個相當基本的問題:當你在終端中按下鍵盤上的一個鍵(比如 Delete
,或 Escape
,或 a
),發送了哪些位元組?
像往常一樣,我們將通過做一些實驗來回答這個問題,看看會發生什麼 : )
遠程終端是非常古老的技術
首先,我想說的是,用 xterm.js
在瀏覽器中顯示一個終端可能看起來像一個新事物,但它真的不是。在 70 年代,計算機很昂貴。因此,一個機構的許多員工會共用一台電腦,每個人都可以有自己的 「終端」 來連接該電腦。
例如,這裡有一張 70 年代或 80 年代的 VT100 終端的照片。這看起來像是一台計算機(它有點大!),但它不是 —— 它只是顯示實際計算機發送的任何信息。
[](https://commons.wikimedia.org/wiki/File:DEC_VT100_terminal.jpg "Jason Scott, CC BY 2.0 https://creativecommons.org/licenses/by/2.0, via Wikimedia Commons")
當然,在 70 年代,他們並沒有使用 Websocket 來做這個,但來回發送的信息的方式和當時差不多。
(照片中的終端是來自西雅圖的 活電腦博物館 ,我曾經去過那裡,並在一個非常老的 Unix 系統上用 ed
編寫了 FizzBuzz,所以我有可能真的用過那台機器或它的一個兄弟姐妹!我真的希望活電腦博物館能再次開放,能玩到老式電腦是非常酷的。)
發送了什麼信息?
很明顯,如果你想連接到一個遠程計算機(用 ssh
或使用 xterm.js
和 Websocket,或其他任何方式),那麼需要在客戶端和伺服器之間發送一些信息。
具體來說:
客戶端 需要發送用戶輸入的鍵盤信息(如 ls -l
)。 伺服器 需要告訴客戶端在屏幕上顯示什麼。
讓我們看看一個真正的程序,它在瀏覽器中運行一個遠程終端,看看有哪些信息會被來回發送!
我們將使用 goterm 來進行實驗
我在 GitHub 上發現了這個叫做 goterm 的小程序,它運行一個 Go 伺服器,可以讓你在瀏覽器中使用 xterm.js
與終端進行交互。這個程序非常不安全,但它很簡單,很適合學習。
我 復刻了它,使它能與最新的 xterm.js
一起工作,因為它最後一次更新是在 6 年前。然後,我添加了一些日誌語句,以列印出每次通過 WebSocket 發送/接收的位元組數。
讓我們來看看在幾個不同的終端交互過程中的發送和接收情況吧!
示例:ls
首先,讓我們運行 ls
。下面是我在 xterm.js
終端上看到的情況:
~:/play$ ls
file
~:/play$
以下是發送和接收的內容:(在我的代碼中,我記錄了每次客戶端發送的位元組:sent: [bytes]
,每次它從伺服器接收的位元組:recv: [bytes]
)
sent: "l"
recv: "l"
sent: "s"
recv: "s"
sent: "r"
recv: "rnx1b[?2004lr"
recv: "filern"
recv: "x1b[~:/play$ "
我在這個輸出中注意到 3 件事:
- 回顯:客戶端發送
l
,然後立即收到一個l
發送回來。我想這裡的意思是,客戶端真的很笨 —— 它不知道當我輸入l
時,我想讓l
被回顯到屏幕上。它必須由伺服器進程明確地告訴它來顯示它。 - 換行:當我按下回車鍵時,它發送了一個
r'(回車)符號,而不是
n'(換行)。 - 轉義序列:
x1b
是 ASCII 轉義字元,所以x1b[?2004h
是告訴終端顯示什麼或其他東西。我想這是一個顏色序列,但我不確定。我們稍後會詳細討論轉義序列。
好了,現在我們來做一些稍微複雜的事情。
示例:Ctrl+C
接下來,讓我們看看當我們用 Ctrl+C
中斷一個進程時會發生什麼。下面是我在終端中看到的情況:
~:/play$ cat
^C
~:/play$
而這裡是客戶端發送和接收的內容。
sent: "c"
recv: "c"
sent: "a"
recv: "a"
sent: "t"
recv: "t"
sent: "r"
recv: "rnx1b[?2004lr"
sent: "x03"
recv: "^C"
recv: "rn"
recv: "x1b[?2004h"
recv: "~:/play$ "
當我按下 Ctrl+C
時,客戶端發送了 x03
。如果我查 ASCII 表,x03
是 「文本結束」,這似乎很合理。我認為這真的很酷,因為我一直對 Ctrl+C
的工作原理有點困惑 —— 很高興知道它只是在發送一個 x03
字元。
我相信當我們按 Ctrl+C
時,cat
被中斷的原因是伺服器端的 Linux 內核收到這個 x03
字元,識別出它意味著 「中斷」,然後發送一個 SIGINT
到擁有偽終端的進程組。所以它是在內核而不是在用戶空間處理的。
示例:Ctrl+D
讓我們試試完全相同的事情,只是用 Ctrl+D
。下面是我在終端看到的情況:
~:/play$ cat
~:/play$
而這裡是發送和接收的內容:
sent: "c"
recv: "c"
sent: "a"
recv: "a"
sent: "t"
recv: "t"
sent: "r"
recv: "rnx1b[?2004lr"
sent: "x04"
recv: "x1b[?2004h"
recv: "~:/play$ "
它與 Ctrl+C
非常相似,只是發送 x04
而不是 x03
。很好!x04
對應於 ASCII 「傳輸結束」。
Ctrl + 其它字母呢?
接下來我開始好奇 —— 如果我發送 Ctrl+e
,會發送什麼位元組?
事實證明,這只是該字母在字母表中的編號,像這樣。
Ctrl+a
=> 1Ctrl+b
=> 2Ctrl+c
=> 3Ctrl+d
=> 4- ...
Ctrl+z
=> 26
另外,Ctrl+Shift+b
的作用與 Ctrl+b
完全相同(它寫的是0x2
)。
鍵盤上的其他鍵呢?下面是它們的映射情況:
Tab
-> 0x9(與Ctrl+I
相同,因為 I 是第 9 個字母)Escape
->x1b
Backspace
->x7f
Home
->x1b[H
End
->x1b[F
Print Screen
->x1bx5bx31x3bx35x41
Insert
->x1bx5bx32x7e
Delete
->x1bx5bx33x7e
- 我的
Meta
鍵完全沒有作用
那 Alt
呢?根據我的實驗(和一些搜索),似乎 Alt
和 Escape
在字面上是一樣的,只是按 Alt
本身不會向終端發送任何字元,而按 Escape
本身會。所以:
alt + d
=>x1bd
(其他每個字母都一樣)alt + shift + d
=>x1bD
(其他每個字母都一樣)- 諸如此類
讓我們再看一個例子!
示例:nano
下面是我運行文本編輯器 nano
時發送和接收的內容:
recv: "rx1b[~:/play$ "
sent: "n" [[]byte{0x6e}]
recv: "n"
sent: "a" [[]byte{0x61}]
recv: "a"
sent: "n" [[]byte{0x6e}]
recv: "n"
sent: "o" [[]byte{0x6f}]
recv: "o"
sent: "r" [[]byte{0xd}]
recv: "rnx1b[?2004lr"
recv: "x1b[?2004h"
recv: "x1b[?1049hx1b[22;0;0tx1b[1;16rx1b(Bx1b[mx1b[4lx1b[?7hx1b[39;49mx1b[?1hx1b=x1b[?1hx1b=x1b[?25l"
recv: "x1b[39;49mx1b(Bx1b[mx1b[Hx1b[2J"
recv: "x1b(Bx1b[0;7m GNU nano 6.2 x1b[44bNew Buffer x1b[53b x1b[1;123Hx1b(Bx1b[mx1b[14;38Hx1b(Bx1b[0;7m[ Welcome to nano. For basic help, type Ctrl+G. ]x1b(Bx1b[mrx1b[15dx1b(Bx1b[0;7m^Gx1b(Bx1b[m Helpx1b[15;16Hx1b(Bx1b[0;7m^Ox1b(Bx1b[m Write Out x1b(Bx1b[0;7m^Wx1b(Bx1b[m Where Is x1b(Bx1b[0;7m^Kx1b(Bx1b[m Cutx1b[15;61H"
你可以看到一些來自用戶界面的文字,如 「GNU nano 6.2」,而這些 x1b[27m
的東西是轉義序列。讓我們來談談轉義序列吧!
ANSI 轉義序列
上面這些 nano
發給客戶端的 x1b[
東西被稱為「轉義序列」或 「轉義代碼」。這是因為它們都是以 「轉義」字元 x1b
開頭。它們可以改變游標的位置,使文本變成粗體或下劃線,改變顏色,等等。維基百科介紹了一些歷史,如果你有興趣的話可以去看看。
舉個簡單的例子:如果你在終端運行
echo -e 'e[0;31mhie[0m there'
它將列印出 「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
,然後我就能用手輸入轉義代碼了。工作的很好!
終端編碼有點奇怪
我想在這裡暫停一下,我覺得你按下的鍵被映射到位元組的方式是非常奇怪的。比如,如果我們今天從頭開始設計按鍵的編碼方式,我們可能不會把它設置成這樣:
Ctrl + a
和Ctrl + Shift + a
做的事情完全一樣。Alt
與Escape
是一樣的- 控制序列(如顏色/移動游標)使用與
Escape
鍵相同的位元組,因此你需要依靠時間來確定它是一個控制序列還是用戶只是想按Escape
。
但所有這些都是在 70 年代或 80 年代或什麼時候設計的,然後需要永遠保持不變,以便向後兼容,所以這就是我們得到的東西 :smiley:
改變窗口大小
在終端中,並不是所有你能做的事情都是通過來回發送位元組發生的。例如,當終端被調整大小時,我們必須以不同的方式告訴 Linux 窗口大小已經改變。
下面是 goterm 中用來做這件事的 Go 代碼的樣子:
syscall.Syscall(
syscall.SYS_IOCTL,
tty.Fd(),
syscall.TIOCSWINSZ,
uintptr(unsafe.Pointer(&resizeMessage)),
)
這是在使用 ioctl
系統調用。我對 ioctl
的理解是,它是一個系統調用,用於處理其他系統調用沒有涉及到的一些隨機的東西,通常與 IO 有關,我猜。
syscall.TIOCSWINSZ
是一個整數常數,它告訴 ioctl
我們希望它在本例中做哪件事(改變終端的窗口大小)。
這也是 xterm 的工作方式。
在這篇文章中,我們一直在討論遠程終端,即客戶端和伺服器在不同的計算機上。但實際上,如果你使用像 xterm 這樣的終端模擬器,所有這些工作方式都是完全一樣的,只是很難注意到,因為這些位元組並不是通過網路連接發送的。
文章到此結束啦
關於終端,肯定還有很多東西要了解(我們可以討論更多關於顏色,或者原始與熟化模式,或者 Unicode 支持,或者 Linux 偽終端界面),但我將在這裡停止,因為現在是晚上 10 點,這篇文章有點長,而且我認為我的大腦今天無法處理更多關於終端的新信息。
感謝 Jesse Luehrs 回答了我關於終端的十億個問題,所有的錯誤都是我的 :smiley:
via: https://jvns.ca/blog/2022/07/20/pseudoterminals/
作者:Julia Evans 選題:lujun9972 譯者:wxy 校對:wxy
本文轉載來自 Linux 中國: https://github.com/Linux-CN/archive