APUE笔记:Terminal I/O(十八)

本文记录《UNIX环境高级编程》第3版 第18章 Terminal I/O 的一些知识点。


Overview

终端输入输出有两种模式:

  1. 规范模式输入处理。在这种模式下,终端输入按行进行处理。终端驱动程序每次读取请求最多返回一行。
  2. 非规范模式输入处理。输入字符不会被组合成 lines。

如果我们不做任何特殊处理,规范模式就是默认模式。例如,如果 shell 将标准输入重定向到终端,而使用 read 和 write 函数将标准输入复制到标准输出,此时终端处于规范模式,每次 read 调用最多返回一行内容。像 vi 编辑器这类需要操作整个屏幕的程序,则会使用非规范模式,因为它们的命令可能是单个字符,且不以换行符结尾。此外,这类编辑器不希望系统对特殊字符进行处理,因为这些字符可能会与编辑器的命令产生冲突。例如,Control-D 字符通常是终端的文件结束符,但它也是 vi 编辑器中向下滚动半屏的命令。

我们可以将终端设备视为由终端驱动程序控制,该驱动程序通常位于内核中。每个终端设备都有一个输入队列和一个输出队列,如图18.1所示。

Logical picture of input and output queues for a terminal device

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

Terminal line discipline

通过将规范处理隔离在一个单独的模块中,所有终端驱动程序都能一致地支持规范处理。在第19章讨论伪终端时会再回到这部分内容。

可以查看和修改的所有终端设备特性都包含在一个 termios 结构中。该结构在头文件 <termios.h> 中定义。

1
2
3
4
5
6
7
struct termios {
tcflag_t c_iflag; /* input flags */
tcflag_t c_oflag; /* output flags */
tcflag_t c_cflag; /* control flags */
tcflag_t c_lflag; /* local flags */
cc_t c_cc[NCCS]; /* control characters */
};

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中的第三列所示)。

Summary of special terminal input characters

POSIX.1允许我们禁用这些字符。如果将 c_cc 数组中某个条目的值设置为 _POSIX_VDISABLE 的值,那么就会禁用相应的特殊字符。


Getting and Setting Terminal Attributes

要获取和设置 termios 结构,需要调用两个函数: tcgetattrtcsetattr。通过这种方式,可以检查和修改各种选项标志以及特殊字符,从而让终端按我们期望的方式运行。

1
2
3
4
#include <termios.h>
int tcgetattr(int fd, struct termios *termptr);
int tcsetattr(int fd, int opt, const struct termios *termptr);
//Both return: 0 if OK, −1 on error

这两个函数都接受一个指向 termios 结构的指针,要么返回当前的终端属性,要么设置终端的属性。由于这两个函数仅对终端设备进行操作,所以如果文件描述符(fd)所指向的不是终端设备,errno 就会被设置为 ENOTTY,并且返回-1。

tcsetattropt 参数可以指定新的终端属性何时生效。该参数被指定为以下常量之一:

  • TCSANOW 更改立即生效。
  • TCSADRAIN 所有输出传输完成后才会发生更改。如果我们要更改输出参数,应使用此选项。
  • TCSAFLUSH 所有输出传输完成后才会发生更改。此外,当更改发生时,所有未读取的输入数据都将被丢弃(刷新)。

tcsetattr 的返回状态在正确使用时可能会令人困惑。该函数如果能够执行任何所请求的操作,即使无法执行所有请求的操作,也会返回OK。如果函数返回OK,我们有责任检查所有请求的操作是否都已执行。这意味着,在我们调用 tcsetattr 来设置所需的属性之后,需要调用 tcgetattr,并将终端的实际属性与所需属性进行比较,以检测任何差异。

首次打开的终端有哪些属性?答案是“取决于具体情况”。有些系统可能会将终端属性初始化为实现定义的值。其他系统可能会保留终端上次使用时的属性值。如果希望确保终端行为符合标准,可以使用 O_TTY_INIT 标志打开终端设备。这将确保当调用 tcgetattr 时,termios 结构的任何非标准部分都会被初始化,这样当更改属性并调用tcsetattr 时,终端就能按预期工作。


stty Command

选项可以在程序内部通过 tcgetattrtcsetattr 函数来检查和修改,也可以从命令行(或shell脚本)通过 stty(1) 命令来操作。如果我们在执行这个命令时加上 -a 选项,它就会显示所有的终端选项:

1
2
3
4
5
6
7
$ stty -a
speed 38400 baud; rows 44; columns 206; 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

前面带有连字符的选项名称处于禁用状态。


Terminal Window Size

大多数UNIX系统都提供了一种跟踪当前终端窗口大小的方法,并且当窗口大小改变时,内核会通知前台进程组。内核为每个终端和伪终端维护一个 winsize 结构:

1
2
3
4
5
6
struct winsize {
unsigned short ws_row; /* rows, in characters */
unsigned short ws_col; /* columns, in characters */
unsigned short ws_xpixel; /* horizontal size, pixels (unused) */
unsigned short ws_ypixel; /* vertical size, pixels (unused) */
}

此结构的规则如下:

  • 可以使用 TIOCGWINSZioctl 来获取此结构的当前值。

  • 可以使用 TIOCSWINSZioctl 将该结构的新值存储到内核中。如果这个新值与内核中存储的当前值不同,就会向前台进程组发送一个 SIGWINCH 信号。

  • 除了存储该结构的当前值并在值发生变化时生成信号外,内核不会对这个结构进行任何其他操作。对该结构的解读完全取决于应用程序。

提供此功能是为了在窗口大小改变时通知应用程序(例如vi编辑器)。当应用程序接收到该信号时,它可以获取新的大小并重新绘制屏幕。