APUE笔记:Terminal I/O(十八)
本文记录《UNIX环境高级编程》第3版 第18章 Terminal I/O 的一些知识点。
Overview
终端输入输出有两种模式:
- 规范模式输入处理。在这种模式下,终端输入按行进行处理。终端驱动程序每次读取请求最多返回一行。
- 非规范模式输入处理。输入字符不会被组合成 lines。
如果我们不做任何特殊处理,规范模式就是默认模式。例如,如果 shell 将标准输入重定向到终端,而使用 read 和 write 函数将标准输入复制到标准输出,此时终端处于规范模式,每次 read 调用最多返回一行内容。像 vi 编辑器这类需要操作整个屏幕的程序,则会使用非规范模式,因为它们的命令可能是单个字符,且不以换行符结尾。此外,这类编辑器不希望系统对特殊字符进行处理,因为这些字符可能会与编辑器的命令产生冲突。例如,Control-D 字符通常是终端的文件结束符,但它也是 vi 编辑器中向下滚动半屏的命令。
我们可以将终端设备视为由终端驱动程序控制,该驱动程序通常位于内核中。每个终端设备都有一个输入队列和一个输出队列,如图18.1所示。

大多数UNIX系统在一个名为终端线路规程(terminal line discipline)的模块中实现所有的规范处理。可以将这个模块视为一个盒子,它位于内核的通用读写函数与实际的设备驱动程序之间(见图18.2)。

通过将规范处理隔离在一个单独的模块中,所有终端驱动程序都能一致地支持规范处理。在第19章讨论伪终端时会再回到这部分内容。
可以查看和修改的所有终端设备特性都包含在一个 termios
结构中。该结构在头文件 <termios.h> 中定义。
1 | struct termios { |
tcflag_t
类型足够大,能够容纳每个标志值,通常被定义为无符号整数(unsigned int)或无符号长整数(unsigned long)。c_cc
数组包含所有我们可以修改的特殊字符。NCCS
是该数组中的元素数量,通常在 15 到 20 之间(因为大多数 UNIX
系统的实现支持的特殊字符数量超过了 POSIX 定义的 11
个)。cc_t
类型足够大,可容纳每个特殊字符,通常为无符号字符(unsigned char)。
下表总结了由Single UNIX Specification定义的、用于操作终端设备的各种函数。
| Function | Description |
|---|---|
tcgetattr |
fetch attributes (termios structure) |
tcsetattr |
set attributes (termios structure) |
cfgetispeed |
get input speed |
cfgetospeed |
get output speed |
cfsetispeed |
set input speed |
cfsetospeed |
set output speed |
tcdrain |
wait for all output to be transmitted |
tcflow |
suspend transmit or receive |
tcflush |
flush pending input and/or output |
tcsendbreak |
send BREAK character |
tcgetpgrp |
get foreground process group ID |
tcsetpgrp |
set foreground process group ID |
tcgetsid |
get process group ID of session leader for controlling TTY |
Special Input Characters
POSIX.1 定义了 11 个在输入时会被特殊处理的字符。各实现还定义了额外的特殊字符。图 18.9 总结了这些特殊字符。
在11个POSIX.1特殊字符中,可以将其中9个更改为几乎任何我们喜欢的值。例外情况是换行符和回车符(分别为
\n 和
\r),或许还有停止(STOP)和开始(START)字符(这取决于具体实现)。要做到这一点,需要修改
termios 结构的 c_cc
数组中的相应条目。该数组中的元素通过名称来引用,每个名称都以V开头(如图18.9中的第三列所示)。

POSIX.1允许我们禁用这些字符。如果将 c_cc
数组中某个条目的值设置为 _POSIX_VDISABLE
的值,那么就会禁用相应的特殊字符。
Getting and Setting Terminal Attributes
要获取和设置 termios 结构,需要调用两个函数:
tcgetattr 和
tcsetattr。通过这种方式,可以检查和修改各种选项标志以及特殊字符,从而让终端按我们期望的方式运行。
1 |
|
这两个函数都接受一个指向 termios
结构的指针,要么返回当前的终端属性,要么设置终端的属性。由于这两个函数仅对终端设备进行操作,所以如果文件描述符(fd)所指向的不是终端设备,errno
就会被设置为 ENOTTY,并且返回-1。
tcsetattr 的 opt
参数可以指定新的终端属性何时生效。该参数被指定为以下常量之一:
TCSANOW更改立即生效。TCSADRAIN所有输出传输完成后才会发生更改。如果我们要更改输出参数,应使用此选项。TCSAFLUSH所有输出传输完成后才会发生更改。此外,当更改发生时,所有未读取的输入数据都将被丢弃(刷新)。
tcsetattr
的返回状态在正确使用时可能会令人困惑。该函数如果能够执行任何所请求的操作,即使无法执行所有请求的操作,也会返回OK。如果函数返回OK,我们有责任检查所有请求的操作是否都已执行。这意味着,在我们调用
tcsetattr 来设置所需的属性之后,需要调用
tcgetattr,并将终端的实际属性与所需属性进行比较,以检测任何差异。
首次打开的终端有哪些属性?答案是“取决于具体情况”。有些系统可能会将终端属性初始化为实现定义的值。其他系统可能会保留终端上次使用时的属性值。如果希望确保终端行为符合标准,可以使用
O_TTY_INIT 标志打开终端设备。这将确保当调用
tcgetattr 时,termios
结构的任何非标准部分都会被初始化,这样当更改属性并调用tcsetattr
时,终端就能按预期工作。
stty Command
选项可以在程序内部通过 tcgetattr 和
tcsetattr
函数来检查和修改,也可以从命令行(或shell脚本)通过 stty(1)
命令来操作。如果我们在执行这个命令时加上 -a
选项,它就会显示所有的终端选项:
1 | $ stty -a |
前面带有连字符的选项名称处于禁用状态。
Terminal Window Size
大多数UNIX系统都提供了一种跟踪当前终端窗口大小的方法,并且当窗口大小改变时,内核会通知前台进程组。内核为每个终端和伪终端维护一个
winsize 结构:
1 | struct winsize { |
此结构的规则如下:
可以使用
TIOCGWINSZ的ioctl来获取此结构的当前值。可以使用
TIOCSWINSZ的ioctl将该结构的新值存储到内核中。如果这个新值与内核中存储的当前值不同,就会向前台进程组发送一个SIGWINCH信号。除了存储该结构的当前值并在值发生变化时生成信号外,内核不会对这个结构进行任何其他操作。对该结构的解读完全取决于应用程序。
提供此功能是为了在窗口大小改变时通知应用程序(例如vi编辑器)。当应用程序接收到该信号时,它可以获取新的大小并重新绘制屏幕。