1.概述
历史上,用户接入一个 Unix 系统都是通过串行线(RS-232 连接)连接到一个终端上的。终端由阴极射线管(CRT)组成,能够
显示字符,而且在某些情况下可以显示出基本图形。
在早期的 Unix 上,连接到系统的终端由字符型设备来表示,名称以 /dev/ttyn 的形式给出。在Linux上,/dev/ttyn 设备
是系统上的虚拟控制台。
如今传统终端以及不常见了。现代 Unix 系统的常用接口是高性能位映射图形显示器上的 X Window 窗口管理器。
传统型终端和终端模拟器都需要同终端驱动程序相关联,由驱动程序负责处理设备上的输入和输出。如果是终端模拟器,这里的设备
就是一个伪终端。
当执行输入的时候,驱动程序可以工作在以下两种模式:
1.规范模式
这种模式下,终端的输入是按行处理的,而且可以进行行编辑操作。每一行都由换行符来结束,当用户按下回车键时可产生换行符。
在终端上执行 read() 调用只会在一行输入完成之后才会返回,且最多只返回一行。如果 read() 请求的字节数少于当前中可用的字符,
那么剩下的字节在下次 read() 调用时可用。这是默认的输入模式。
2.非规范模式
终端输入不会被匹配成行。像 vi,more 和 less 这样的程序会将终端至于非规范模式,这样不需要用户按下回车键它们就能读取到单个字符。
终端驱动程序也能对一系列的特殊字符做解释,比如中断字符 Ctrl+C以及文件结尾符 Ctrl+D。当有信号为前台进程组产生时,又或者是程序从终端读取
出现某种类型的输入条件时,此时就可能会出现这样的解释操作。将终端置于非规范模式下的程序通常也会禁止处理某些或者所有这些特殊字符。
终端驱动程序会对两个队列做操作:一个用于从终端设备将输入字符传送到读取进程上,另一个用于将输出字符从进程传送到终端上。如果开启了回显功能,
那么终端程序会自动将任意的输入字符插入到输出队列的尾部,这样输入字符也会称为终端的输出。
2.stty 命令
stty 命令是以命令行形式来模拟函数 tcgetattr() 和 tcsetattr()的功能,允许我们在 shell 上检视和修改终端属性。
stty -a :
speed 38400 baud; rows 37; columns 155; line = 0;
intr = ^C; quit = ^\; erase = ^?; kill = ^U; eof = ^D; eol = M-^?; eol2 = M-^?; swtch = <undef>; start = ^Q; stop = ^S; susp = ^Z; rprnt = ^R; werase = ^W;
lnext = ^V; flush = ^O; min = 1; time = 0;
-parenb -parodd -cmspar cs8 -hupcl -cstopb cread -clocal -crtscts
第一行显示了终端的线速,终端窗口大小以及数值形式给出了行规程。
3.终端的IO模式
终端驱动程序能够以规范模式和非规范模式来处理输入,这取决于对 ICANON 标志的设定。终端的3种模式:加工模式,cbreak 模式,以及原始模式。
1.规范模式
我们可以通过设定 ICANON 标志来打开规范模式。可以通过以下几点区分是否为规范模式下的输入。
1.输入被装配成行,通过如下几种行结束符来终结:NL, EOL, EOL2,EOF 或者 CR,除了EOF 之外,其他的行结束符都会传递给读取的进程。
2.打开了行编辑功能,这样可以修改当前中的输入。
3.如果设定了 IEXTEN 标志,则 REPINT 和 LNEXT 字符也是可用的。
在规范模式下,当存在有一行完整的输入时,终端上的 read() 调用才会返回。如果请求的字节数比一行中所包含的字节小,那么 read() 只会获取
到该行的一部分。剩余的字节只有在后续的 read() 调用中取得。如果 read() 调用被信号处理例程中断,且该信号没有系统调用重启,此时 read()
也会终止执行。
2.非规范模式
一些应用程序在用户没有提供终止符时也需要从终端读取字符,非规范模式正式用于这个目的。在非规范模式下(关闭 ICANON 标志)不会处理特殊的输入。
特别的一点是:输入不再装配成行,相反会立刻对应用程序可见。
在什么情况下一个非规范模式下的 read() 调用会完成?我们可以指定非规范模式下的 read() 调用在经历了一段特定的时间后,或者在读取了特定的数量
的字节后,又或者是两者兼有的情况下终止执行。termios 结构体中的 c_cc 数组里有2个元素可以用来决定这种行为:TIME 和 MIN。元素 TIME(通常通过
常量VTIME来索引)以十分之一秒为单位来指定超时时间。元素MIN(通过 VMIN 来索引)指定了被读取字节数的最小值。MIN和TIME的设置对规范模式下的终端
IO不产生任何影响。
参数MIN和TIME的精确操作和交互取决于它们各自是否包含有非零值。下面介绍4种情况。注意,在所有4种情况中,如果在 read() 调用过程中已经读取了
足够的字节数来满足 MIN 要求,那么 read() 会立刻返回可用的字节数和所请求的字节数中较小的那个值。
1. MIN == 0, TIME == 0 (轮询读取)
如果在调用过程中有数据可用,那么 read() 将立刻返回可用的字节数和所请求的字节数中较小的那个值。如果没有数据可用,read() 立刻返回0.
这种情况可服务于一般的轮询请求,允许应用程序以非阻塞的方式检查输入是否存在。这种模式有些类似于为终端设定 O_NONBLOOK 标志。但是,
在设定 O_NONBLOCK 标志后,如果没有数据可读,那么 read() 返回-1,并伴随错误码 EAGAIN。
2. MIN > 0, TIME == 0 (阻塞式读取)
这种情况下 read() 会阻塞(有可能永远阻塞下去),直到请求的字节数得到满足或者读取到了 MIN 个字节,此时就会返回这两者中较小的那一个。
像 less 这样的程序一般都会将 MIN 设为1,而把 TIME 设为0.这使得程序不用在轮询中忙等从而浪费 CPU 时间,只要用户按下单个键 read()
就能返回了。
3. MIN == 0, TIME > 0 (带有超时机制的读操作)
这种情况下当调用 read() 时会启用一个定时器。当至少有1个字节可用,或者经历了 TIME 个十分之一秒之后,read() 会立刻返回。在后一种情况下
read() 返回0.
这种情况对同串行设备(比如调制解调器)打交道的程序来说很有用。程序可以发送数据给设备然后等待响应。假如设备没响应,采用超时机制就能够避免程序
永远挂起。
4. MIN > 0, TIME > 0 (既有超时机制又有最小读取字节数的要求)
当输入的首个字节可用后,之后每接收到的一个字节就重启定时器。如果满足读取到了 MIN 个字节,或者请求的字节数已经读取完毕,此时 read() 会返回两者
之间较小的那个值。或者当接收连续字节之间的时隙超过了 TIME 个十分之一秒,此时 read() 会返回0.由于定时器只会在初始字节可用后才启动,因此此时可以返回
1字节。
这种情况对于处理生成转义序列的终端按键十分有用。
4.终端驱动程序能够以3种方式来处理输入
1.加工模式
本质就是带有处理默认特殊字符功能的规范模式。
2.cbreak 模式
处于加工模式和原始模式之间。输入是按照非规范模式的方式来处理的,但产生的信号的字符会被解释,且仍然会出现各种输入和输出的转换。cbreak模式并不会禁止
回显,但采用这种模式的应用程序通常都会禁止回显功能。cbreak 模式在与屏幕处理相关的应用程序很有用(比如 less)。
3.原始模式
它属于非规范模式,所有的输入和输出都不能做任何处理,而且不能回显。如果应用程序需要确保终端驱动程序绝对不会对传输的数据做任何修改,那么就应该使用这种模式。
5.终端线速(比特率)
不同的终端之间(以及串行线)传输和接收的速率(位数每秒)是不同的。函数 cfgetispeed() 和 cfsetispeed() 用来获取和修改输入的线速。函数 cfgetospeed() 和 cfsetospeed()
用来获取和修改输出的线速。
6.终端的行控制
tcsendbreak();
tcdrain();
tcflush();
tcflow();
7.终端窗口大小
在一个窗口环境中,一个处理屏幕的应用程序能够监视终端窗口的大小,这样当用户修改了窗口大小时能够适当的重新绘制屏幕。内核对此提供了2种方式来支持。
1.在终端窗口大小改变后发送一个 SIGWINCH 信号给前台进程组。默认情况下,该信号被忽略
2.在任意时刻---通常是接收到 SIGWINCH 信号之后---进程可以使用 ioctl() 的 TIOCGWINSZ 操作来获取终端窗口当前的大小。
8.终端标识
isatty(int fd); // 判断 fd 是否同一个终端关联
ttyname(int fd); // 返回与之相关的终端设备名称
tcgetattr();
tcsetattr();