Linux中國

當你在終端上按下一個鍵時會發生什麼?

我對 終端 Terminal 是怎麼回事困惑了很久。

但在上個星期,我使用 xterm.js 在瀏覽器中顯示了一個互動式終端,我終於想到要問一個相當基本的問題:當你在終端中按下鍵盤上的一個鍵(比如 Delete,或 Escape,或 a),發送了哪些位元組?

像往常一樣,我們將通過做一些實驗來回答這個問題,看看會發生什麼 : )

遠程終端是非常古老的技術

首先,我想說的是,用 xterm.js 在瀏覽器中顯示一個終端可能看起來像一個新事物,但它真的不是。在 70 年代,計算機很昂貴。因此,一個機構的許多員工會共用一台電腦,每個人都可以有自己的 「終端」 來連接該電腦。

例如,這裡有一張 70 年代或 80 年代的 VT100 終端的照片。這看起來像是一台計算機(它有點大!),但它不是 —— 它只是顯示實際計算機發送的任何信息。

[DEC 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 來做這個,但來回發送的信息的方式和當時差不多。

(照片中的終端是來自西雅圖的 活電腦博物館 Living Computer Museum ,我曾經去過那裡,並在一個非常老的 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 件事:

  1. 回顯:客戶端發送 l,然後立即收到一個 l 發送回來。我想這裡的意思是,客戶端真的很笨 —— 它不知道當我輸入l 時,我想讓 l 被回顯到屏幕上。它必須由伺服器進程明確地告訴它來顯示它。
  2. 換行:當我按下回車鍵時,它發送了一個 r'(回車)符號,而不是n'(換行)。
  3. 轉義序列: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 => 1
  • Ctrl+b => 2
  • Ctrl+c => 3
  • Ctrl+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 呢?根據我的實驗(和一些搜索),似乎 AltEscape 在字面上是一樣的,只是按 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(因為 Escapex1b)。

如果我在 xterm.js 終端手動鍵入 Escape ,然後是 [,然後是 H,我就會出現在行的開頭,與我按下 Home 完全一樣。

我注意到這在我的電腦上的 Fish shell 中不起作用 —— 如果我鍵入 Escape,然後輸入 [,它只是列印出 [,而不是讓我繼續轉義序列。我問了我的朋友 Jesse,他寫過 一堆 Rust 終端代碼,Jesse 告訴我,很多程序為轉義代碼實現了一個 超時 —— 如果你在某個最小的時間內沒有按下另一個鍵,它就會決定它實際上不再是一個轉義代碼了。

顯然,這在 Fish shell 中可以用 fish_escape_delay_ms 來配置,所以我運行了 set fish_escape_delay_ms 1000,然後我就能用手輸入轉義代碼了。工作的很好!

終端編碼有點奇怪

我想在這裡暫停一下,我覺得你按下的鍵被映射到位元組的方式是非常奇怪的。比如,如果我們今天從頭開始設計按鍵的編碼方式,我們可能不會把它設置成這樣:

  • Ctrl + aCtrl + Shift + a 做的事情完全一樣。
  • AltEscape 是一樣的
  • 控制序列(如顏色/移動游標)使用與 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

本文由 LCTT 原創編譯,Linux中國 榮譽推出


本文轉載來自 Linux 中國: https://github.com/Linux-CN/archive

對這篇文章感覺如何?

太棒了
0
不錯
0
愛死了
0
不太好
0
感覺很糟
0
雨落清風。心向陽

    You may also like

    Leave a reply

    您的郵箱地址不會被公開。 必填項已用 * 標註

    此站點使用Akismet來減少垃圾評論。了解我們如何處理您的評論數據

    More in:Linux中國