stty 控制输入流

通常,我们可以使用 Ctrl + C 来发送 SIGINT(2) 信号实现程序强制关闭

这里有一个很容易引起困惑的地方: Ctrl + C 是如何与 SIGINT(2) 绑定起来的呢?

在某些未正确配置终端的情况下,我们还会发现按下上下左右箭头,终端并不会跳转到前一条命令或是移动光标,而是打印一些奇怪的字符 ^A^B^M,这又是什么意思呢?

从本质上来说,我们在终端的输入,实际上都会被转换为 ASCII,其中 0~9、a-z 等可视字符自然是原样传输,而其他一些控制字符,则会被系统转换为对应的信号或是特殊操作被处理

console、terminal、tty、shell

这一系列的名词都来自于计算机的原始形态,在目前的场景下,含义已经有所不同。

控制台

最初的计算机概念类似与一个大服务器,重要操作会直接通过“服务器”上的寄存器、旋钮等物理方式控制,如同飞机的操作台,这被称作控制台(console)

终端

而普通用户则借助有线连接,通过终端设备(terminal)接入
在这里,终端是一种统称,所有在另一端的设备都称作终端(与控制台相对)

tty

一般来说终端的具体形式是一个特殊的打字机(teletype),简称 tty。其包括输入和输出两部分,分别是键盘与一个显示器(或是打印到纸上)

外壳

tty 作为与人对接的接口,自然是使用人类可读的内容与计算机进行交互。由于打出的命令并不能被计算机直接识别,因此还需要系统内核开放一些接口供用户方便地使用,这称作外壳(shell)。

因为是用户态的接口,所以 shell 可能会有不同的实现以供用户选择,常见的有 sh、bash、zsh……

如今命令行的实现

由于计算机的轻量化,控制台与终端的概念可以认为被合二为一了,我们使用命令行为如下形式

%3 terminal 终端 tty tty terminal->tty shell Shell tty->shell

如果用 C/S 模型来类比,可以 不严谨地近似认为 终端就是客户端,tty 是服务端,连接建立后 tty 会运行一个 Shell 程序来处理用户的输入

如果执行 ps,可以看到下面的输出,bash 就是当前 tty 运行的命令,而 ps 则是 bash 命令的调用运行的任务

$ ps

PID TTY          TIME CMD
145 pts/0    00:00:00 bash
223 pts/0    00:00:00 ps

stty 命令

前面明确了 shell 实际上是一个程序,虽然和我们写的 Hello World 不太一样,但是本质是没区别的。

信号改绑

如果要实现一个监听 Ctrl + C 的程序,随便一搜,可以看到的例子都是监听 SIGINT(2)。然而这并不是我们实际输入的 Ctrl + C,那么在用户输入和程序之间必然有一个东西实现了这个转换。

而他们中间存在的,看上去没什么用的 tty 就实现了这个操作。

$ stty -a

speed 38400 baud; rows 30; columns 120; line = 0;
intr = ^C; quit = ^\; erase = ^?; kill = ^U; eof = ^D; eol = <undef>; eol2 = <undef>; swtch = <undef>; start = ^Q;
stop = ^S; susp = ^Z; rprnt = ^R; werase = ^W; lnext = ^V; discard = ^O; min = 1; time = 0;
-parenb -parodd -cmspar cs8 -hupcl -cstopb cread -clocal -crtscts
-ignbrk -brkint -ignpar -parmrk -inpck -istrip -inlcr -igncr icrnl ixon -ixoff -iuclc -ixany -imaxbel -iutf8
opost -olcuc -ocrnl onlcr -onocr -onlret -ofill -ofdel nl0 cr0 tab0 bs0 vt0 ff0
isig icanon iexten echo echoe echok -echonl -noflsh -xcase -tostop -echoprt echoctl echoke -flusho -extproc

在这里,可以看到 intr = ^C 的字段,这表示 Ctrl + C 会触发 intr(SIGINT) 信号

使用 stty intr ^G,即可改绑按键,再次调用命令,可以发现 Ctrl + C 已经不会停止进程了,取而代之的是 Ctrl + G

原生输入

信号可以改绑,自然就可以取消绑定。有一些伪命令行客户端(看起来是个命令行,但是和我们通常的 shell 有不同),如 mongodb、mysql 的客户端内。我们通常希望按下上箭头会跳回上一条命令,但是往往输出的是奇怪的字符。
这是因为我们输入的上箭头被 tty 处理为信号了,程序内部并没有收到我们输入的上箭头的键码

在这种场景下,可以使用 stty raw 来设定使用原生输入,不做处理。这会导致所有信号都不会被监听,如果程序内没有处理 SIGINT,那么在程序自己退出前,无论按多少次 Ctrl + C,程序只会收到 2 这个数字本身
(使用 stty -raw 可还原)

如同上面的例子一样,在一些需要命令行透传的场景下可能会有意义,比如实现一个终端,自己去处理各个信号;或是手动实现 sshdocker exec 客户端,交由服务端程序去处理信号

输入回显

在开发一些按键监听的功能(比如 AutoHotkey),我们为了确认按键是否按下,往往需要去调试输出按下的按键。这在 GUI 程序内似乎是理所当然的。

但是在终端中,我们按下键立即可以看到我们输入的内容似乎也是理所当然的,这种回显实际上也是由 tty 实现的。

如果使用 stty -echo,即可关闭回显。
无论我们按下什么键,都不会有东西显示在终端中。但是如果我们能够确认输入是正确的,按下回车则可以正确执行命令。
(使用 stty echo 可还原)

在大部分情况下,我们都需要回显,但是在部分场景下,可能关闭回显会更好。比如 ssh 或是 docker exec,都可以将我们的输入输出与另一个 “设备” 的输入输出相连。
由于每一个程序本身都有自己的 tty,那么如果不做处理的结果就是导致我们会看到两个输出回显(这些工具的客户端本身已经做好了处理,自己实现时可能会遇到这些问题)