62 终端

整体概览

终端可以分成两种工作模式:1. 规范模式(输入按照行来处理);2. 非规范模式,比如 vim、less 等程序中。

终端驱动程序的作用是:操作两个队列,一个用于从终端设备把输入字符传送到读取进程上,另外一个用于将输出字符从进程传输到终端上。如果开启了终端回显功能,那么终端输入队列上面的新字符也会自动被追加到输出队列的尾部。终端驱动程序还能识别终端输入中的特殊字符,并根据字符含义做出相应的行为(比如 ctrl + d 和 ctrl + c)。

如果进程想要知道终端上面有多少没有读取的内容(假设终端的是其标准输入),在 Linux 上面可以使用 ioctl(fd, FIONREAD, &cnt)。这个特性在 SUS 中没有规定。

获取和修改终端属性

可以用 tcgetattr(2) 和 tcsetattr(2) 操作终端属性。这只能由前台进程调用,如果是后台进程,会因为没有控制终端而收到终端驱动程序发来的 SIGTTOU 信号。

stty 命令

stty 命令能够在命令行上查看和修改终端属性,相当于是提供了 tcgetattr(2) 和 tcsetattr(2) 的功能。

Tip

还有一个 tty 命令,它能够打印和标准输入流相连接的终端的名称。比如

$ tty
/dev/pts/6

查看终端属性

使用 stty -a 可以查看终端当前的属性。

stty 只能读取标准输入的终端信息。如果想要查看其他终端的信息,可以使用 stty -a < /dev/tty3。在 Linux 上面有个扩展,可以使用 -F(fetch)选项来读取其他终端的信息,比如 stty -a -F /dev/tty3

修改终端特殊字符映射

可以使用 stty intr ^L 将中止操作按键从 ^C 改成 ^L。指定字符可以使用以下方法之一:

  1. ^X 这样的 ^ 加上一个字符。
  2. 用 8 进制或者 16 进制数字表示。
  3. 直接输入字符本身。在这种方式下要先输入 literal next 字符(一般是 ctrl + v),然后输入对应的字符(比如 ctrl + l)

还可以将非控制字符修改成终端的特殊字符,比如一个字母按键。但是这样的操作不常见。

修改终端标志

stty tostop     # 添加 TOSTOP 标志
stty -tostop    # 去除 TOSTOP 标志

恢复终端状态

有时候程序出错可能打印了一些不合适的转义字符,导致终端不能正常显示。这个时候可以通过以下方式来还原终端标志和控制字符的状态。

<control+j> stty sane <control+j>

其中 control + j 才是真正的换行符(ASCII 是 10),之所以用这个是因为平常 enter 按键输入的换行符(ASCII 是 13)可能在终端状态有错的情况下不能正常使用了。

实测感觉没啥用,还是 reset 管用。

一些终端特殊字符的默认设定

struct termios 中有 cc_t c_cc[NCCS]; 域,用来存储终端的控制字符,这个信息可以使用 tcgetattr(2) 和 tcsetattr(2) 获取和改写。有 c_cc 下标的字符才能更改实质起作用的字符来达到特殊功能。唯独 CR 和 NL 没有 c_cc 下标,这也表示它们不能被改写。下面有些终端特殊字符是在规范模式下起作用(比如 bash 正在等待用户键入命令并换行),有些是始终起作用。

  • CR:^M,回车。在默认设置了 ICRNL 的规范模式下,CR 会被映射成 NL,因此 CR 这种情况下会被转换成 ^J
  • EOF:^D,文件结尾。
  • ERASE:^?,擦除字符,行编辑时使用。不能用 ^/(在 bash 上面是撤销一次行编辑),一定要加上 shift 按键。
  • INTR:^C,中断程序。
  • KILL:^U,擦除一行,行编辑时使用。
  • LNEXT:^V,字面化下一个字符。
  • NL:^J,换行。
  • QUIT:^\,退出,发送 SIGQUIT。
  • REPRINT:^R,重新打印输入行。在 bash 中这个字符用来表示搜索命令历史,我不知道怎么启用重新打印输入行的功能。
  • START:^Q,开始输出。
  • STOP:^S,停止输出。按了这个之后,终端就不再从输出队列中读取字符显示出来了,看上去就像输出被阻塞了一样。和 START 是配套使用的。
  • SUSP:^Z,暂停。
  • WERASE:^W,擦除一个字(word),行编辑用。

还有几个没有和具体字符关联起来的终端特殊字符:

  • EOL:行结尾。
  • EOL2:另外一种行结尾。

除了 CR、NL、EOL、EOL2 之外的特殊字符被终端驱动程序解释之后就会被丢弃,不会传给读取输入的进程。

终端标志

一些终端特殊字符的工作方式(包括是否启用)可以由终端标志来控制。此外,终端标志还有别的功能。

终端标志有四个字段组成:c_iflagc_oflagc_cflagc_lflag

终端标志太多了,这里只罗列几个例子(不是书上给的例子,是我自己找的感兴趣的,所以有可能不是什么重点)。

  • c_iflag 中的 IUTF8,表示输入为 UTF-8,从 Linux 2.6.4 开始支持,默认关闭。如果输入是 UTF-8 但是没有开启 UTF-8 字符,那么退格按键只能删除一个字节而不是整个字,这会导致缓冲区混乱。
  • c_oflag 中的 ONLCR,表示输出时将 NL 映射为 CR-NL,默认启用。这就是为什么我们看到换行也带有将光标移动到行首的效果。
  • c_lflag 中的 ECHO,表示回显字符,默认启用。
  • c_lflag 中的 ISIG,表示键入相关字符会给前台进程发送信号(INTR、QUIT、SUSP),默认启用。
  • c_lflag 中的 TOSTOP,表示为后台输出产生 SIGTTOU 信号,默认关闭。

终端的 I/O 模式

1. 规范模式

略。

2. 非规范模式

非规范模式下运行的程序能用 read() 调用从用户输入那里读到什么?

termios 结构体中的 c_cc 数组里有两个元素可用来决定这种行为:TIME(用 VTIME 来索引)和 MIN(用 VMIN 来索引)。TIME 表示在多长时间没有新的输入时返回(每次收到新的字节会重置定时器),单位是 0.1 秒。MIN 表示读多少个就能返回。TIME 和 MIN 要结合起来才能确定 read() 系统调用的行为:

  • TIME=0, MIN=0 表示轮询,查一次不管有没有都不会阻塞。
  • TIME>0, MIN=0 表示超时等待,如果超时还没有任何输入也就作罢,如果有任何输入就可以返回。
  • TIME=0, MIN>0 表示至少读取 MIN 个字节返回,没读够就阻塞。
  • TIME>0, MIN>0 表示至少读取 MIN 个字节,或者自从新增字节以来超时了才返回。

由于有些 UNIX 实现中,规范模式下和非规范模式下 termios 设置的含义可能不同,为了可移植性最好先保存 termios,切换到非规范模式下,完成操作,切换回来的时候复原原来的 termios 设置。

3. 加工(cooked)模式、cbreak 模式以及原始(raw)模式

在 7 版 UNIX 操作系统(以及早期 BSD)中终端驱动程序可以在加工模式、cbreak 模式以及原始模式三种模式之间切换。加工模式近似于规范模式,原始模式近似于非规范模式。而 cbreak 模式介于两者之间,输入是按照非规范的方式处理的,但是产生信号的字符会被解释、也会发生各种输入和输出的转换,也不禁止回显。

书中举例:cbreak 模式在 less 这样和屏幕处理相关的应用中有用,因为要允许逐个字符的输入,但同时还需要能接收终端信号。(less 好像是处理了 ^C^L 的信号,但是没有处理 ^Z。)

现在 POSIX termios 接口已经不能通过单个比特位(UNIX 原来能)在原始模式和 cbreak 模式上做出选择了,只能修改 termios 中的字段模拟出这些模式。Ncurses 库就提供了 cbreak()raw() 函数。

终端线速

有一些相关的系统调用。对虚拟终端设置线速没有意义,但是和计算机连接的其他设备,比如串口 / USB 串口的终端线速可能有意义。

终端的行控制

int tcsendbreak(int fd, int duration);
int tcdrain(int fd);
int tcflush(int fd, int queue_selector);
int tcflow(int fd, int action);

其中 tcflush() 用来刷新终端的输入或输出或两者。刷新输入队列将使得终端驱动程序已经接收但是还没有传给输入程序的数据丢失,比如向用户索要密码之前先清除多余的字符。

终端窗口大小

终端窗口大小变化时,程序会收到 SIGWINCH 信号,该信号的默认处理方式是忽略。一些程序收到了这个信号后会调用 ioctl()TIOCGWINSZ 操作来获取终端窗口大小。

终端标识

isatty() 可以判断文件描述符是否和终端关联。

ttyname() 可以获得终端的名称。