无论在哪种操作系统中,终端 I/O 的处理都是非常繁琐的一部分,UNIX 系统也不例外。在大多数版本的编程手册中,终端I/O手册页常常是最长的几个部分之一。
在20世纪70年代后期,系统Ⅲ在V7的基础上发展出一套不同的终端例程,由此使得UNIX终端 I/O 处理分立为两种不同的风格。一种是系统Ⅲ的风格,由 System V 沿续下来,另一种是V7 的风格,它成为BSD派生的系统终端I/O处理的标准。如同信号一样,POSIX.1在这两种风格的基础上制定了终端I/O标准。本章将介绍POSIX.1的所有终端函数,以及某些平台特有的增加部分。
终端I/O系统之所以如此复杂,部分原因是人们将其应用在众多的事物上:终端、计算机之间的直接连接、调制解调器以及打印机等。
终端I/O有两种不同的工作模式。
(1)规范模式输入处理。在这种模式中,对终端输入以行为单位进行处理。对于每个读请求,终端驱动程序最多返回一行。
(2)非规范模式输入处理。输入字符不装配成行。
如果不做特殊处理,则默认模式是规范模式。例如,若shell将标准输入重定向到终端,并用read和write将标准输入复制到标准输出,则终端以规范模式进行工作,每次read最多返回一行。处理整个屏幕的程序(如 vi 编辑器)使用非规范模式,原因是它的命令可能是由单个字符组成的,并且不以换行符终止。另外,该编辑器并不希望系统对特殊字符进行处理,因为这些字符很可能与编辑命令中使用的字符重叠。例如,Ctrl+D字符通常是终端的文件结束符,但在vi中它是向下滚动半个屏幕的命令。
V7和较早的BSD风格类的终端驱动程序支持3种终端输入模式:(a)精细加工模式(输入装配成行,并对特殊字符进行处理);(b)原始模式(输入不装配成行,也不对特殊字符进行处理);(c)cbreak模式(输入不装配成行,但对某些特殊字符进行处理)。图18-20显示了将终端设置为cbreak或原始模式的POSIX.1函数。
POSIX.1定义了11个特殊输入字符,其中9个可以更改。本书已经用到了其中几个,例如文件结束符(通常是Ctrl+D)和挂起字符(通常是Ctrl+Z)。18.3节将对这些字符逐一进行说明。
可以认为终端设备是由通常位于内核中的终端驱动程序控制的。每个终端设备都有一个输入队列和一个输出队列,如图18-1所示。
图18-1 终端设备的输入、输出队列的逻辑结构
对此图要说明以下几点。
•如果打开了回显功能,则在输入队列和输出队列之间有一个隐含的连接。
•输入队列的长度MAX_INPUT(见图2-11)是有限值。当一个特定设备的输入队列已经填满时,系统的行为将依赖于实现。这种情况发生时大多数UNIX系统回显响铃字符。
•图中没有显示另一个输入限制 MAX_CANON。这个限制是一个规范输入行的最大字节数。
•虽然输出队列的长度通常也是有限的,但是程序并不能获得这个定义其长度的常量,因为当输出队列将要填满时,内核便直接使写进程休眠,直至写队列中有可用的空间。
•我们将说明如何使用冲洗函数 tcflush 冲洗输入或输出队列。与此类似,在说明 tcsetattr 函数时,将会了解到如何通知系统只有在输出队列为空时,才能改变一个终端的属性。(例如,想要改变输出属性时就要这样做。)也可以通知系统,让它在改变终端属性时丢弃输入队列中的所有东西。(如果正在改变输入属性,或者在规范模式和非规范模式之间进行转换,就需要这样做,以免以错误的模式对以前输入的字符进行解释。)
大多数 UNIX 系统在一个称为终端行规程(terminal line discipline)的模块中进行全部的规范处理。可以将这个模块设想成一个盒子,位于内核通用读、写函数和实际设备驱动程序之间(见图18-2)。
图18-2 终端行规程
由于将规范处理分离为单独的模块,所有的终端驱动程序都能够一致地支持规范处理。在第19章讨论伪终端时还将使用此图。
所有可以检测和更改的终端设备特性都包含在 termios 结构中。该结构定义在头文件<termios.h>中,本章使用这一头文件。
cc_t c_cc[NCCS]; /* control characters */
tcflag_t c_lflag; /* local flags */
tcflag_t c_cflag; /* control flags */
tcflag_t c_oflag; /* output flags */
tcflag_t c_iflag; /* input flags */
struct termios {
};
粗略地说,输入标志通过终端设备驱动程序控制字符的输入(例如,剥除输入字节的第8位,允许输入奇偶校验),输出标志则控制驱动程序输出(例如,执行输出处理、将换行符转换为CR/LF),控制标志影响RS-232串行线(例如,忽略调制解调器的状态线、每个字符的一个或两个停止位),本地标志影响驱动程序和用户之间的接口(例如,回显打开或关闭、可视地擦除字符、允许终端产生的信号以及对后台输出的作业控制停止信号)。
类型tcflag_t的长度足以保存每个标志值,它经常被定义为unsigned int或者unsigned long。c_cc数组包含了所有可以更改的特殊字符。NCCS是该数组中元素的数量,其典型值在15~20(因为大多数UNIX实现支持的特殊字符都比POSIX.1所定义的11个要多)。cc_t类型的长度足以保存每个特殊字符,典型的是unsigned char。
POSIX标准之前的System V版本有一个名为<termio.h>的头文件和一个名为termio的数据结构。为了与先前版本有所区别,POSIX.1在这些名字后加了一个s。
图 18-3 至图 18-6 列出了所有可以更改以影响终端设备特性的终端标志。注意,虽然 Single UNIX Specification定义了供所有平台启动所用的公共子集,但所有实现都有自己的扩充部分。这些扩充部分大多来自各系统之间的历史差异。18.5节将对这些标志值进行详细的讨论。
图18-3 c_cflag终端标志
图18-4 c_iflag终端标志
图18-5 c_lflag终端标志
给出了所有可用的选项后,如何才能检测和更改终端设备的这些特性呢?图18-7总结并列出了Single UNIX Specification所定义的对终端设备进行操作的各个函数。(列出的所有函数都是 POSIX 基本规范的组成部分。9.7 节已说明了 tcgetpgrp、tcgetsid 和 tcsetpgrp函数。)
注意,对终端设备,Single UNIX Specification没有使用经典的ioctl,而是使用了图18-7中列出的13个函数。这样做的理由是:对于终端设备的ioctl函数,其最后一个参数的数据类型随执行动作的不同而改变。因此,不可能对参数进行类型检查。
图18-6 c_oflag终端标志
图18-7 终端I/O函数汇总
虽然在终端设备上进行操作的只有13个函数,但是图18-7中的前两个函数(tcgetattr和tcsetattr)能处理大约 70种不同的标志(见图 18-3至图 18-6)。终端设备有大量选项可供使用,此外,对于某个特定设备(假设其为终端、调制解调器、打印机或任何其他设备),决定其需要哪些选项对我们来说也是一种挑战,这些都使得对终端设备的处理变得异常复杂。
图18-7中列出的13个函数之间的关系如图18-8所示。
POSIX.1没有指定将波特率信息存储在termios结构中的什么地方,它依赖于实现的细节。某些系统,如Solaris,将此信息存储在c_cflag字段中。Linux和BSD派生的系统,如FreeBSD和Mac OS X,则在此结构中有两个分开的字段:一个存储输入速度,另一个存储输出速度。
图18-8 与终端有关的各函数之间的关系
POSIX.1 定义了 11 个在输入时要特殊处理的字符。实现定义了另外一些特殊字符。图 18-9总结并列出了这些特殊字符。
图18-9 终端特殊输入字符汇总
图18-9 终端特殊输入字符汇总(续)
在POSIX.1的11个特殊字符中,其中有9个字符的值可以任意更改。不能更改的两个特殊字符是换行符和回车符(分别是\n和\r),也可能是STOP和START字符(依赖于实现)。为了更改,只需要修改 termios 结构中 c_cc 数组的相应项。该数组中的元素都用名字作为下标进行引用,每个名字都以字母V开头(见图18-9中的第3列)。
POSIX.1允许禁止使用这些字符。若将c_cc数组中的某项设置为_POSIX_VDISABLE的值,则禁止使用相应特殊字符。
在Single UNIX Specification的早期版本中,支持_POSIX_VDISABLE是可选项,现在则是必选项。
本书讨论的4种平台都支持此特性。Linux 3.2.0和Solaris 10将_POSIX_VDISABLE定义为0,而FreeBSD 8.0和Mac OS X 10.6.8则将其定义为0xff。
某些早期的UNIX系统所用的方法是:若与某一特性相应的特殊输入字符是0,则禁止使用该特性。
实例
在详细说明各特殊字符之前,先看一个更改特殊字符的小程序。图18-10所示的程序禁用中断字符,并将文件结束符设置为Ctrl+B。
图18-10 禁用中断字符并更改文件结束符
对此程序要说明以下几点。
•仅当标准输入是终端设备时才修改终端特殊字符。调用isatty(见18.9节)对此进行检测。
•用fpathconf获取_POSIX_VDISABLE值。
•函数 tcgetattr(见 18.4 节)从内核获取 termios 结构。在修改了此结构后,调用 tcsetattr 函数设置属性,只有我们所希望修改的属性被更改了,而其他属性保持不变。
•禁用中断键与忽略中断信号是不同的。图 18-10 中的程序所做的只是禁用使终端驱动程序产生SIGINT信号的特殊字符。我们仍可使用kill函数将该信号发送至进程。
下面较详细地说明各个特殊字符。我们称这些字符为特殊输入字符,但是其中有两个字符—STOP 和 START(Ctrl+S和Ctrl+Q),在输出时也要进行特殊处理。注意,这些字符中的大多数在被终端驱动程序识别并进行特殊处理后会被丢弃,并不将它们返回给执行读终端操作的进程。返回给读进程的例外字符是换行符(NL、EOL、EOL2)和回车符(CR)。
CR 回车符。不能更改此字符。以规范模式进行输入时识别此字符。在已设置ICANON (规范模式)和ICRNL(将CR映射为NL)但并未设置IGNCR(忽略CR)时,CR字符会被转换成 NL,并具有与 NL 字符相同的作用。此字符返回给读进程(很可能是在转换为NL之后)。
DISCARD 丢弃符。在扩充模式(IEXTEN)下进行输入时识别此字符。在输入另一个DISCARD字符之前或在丢弃条件被清除之前(见FLUSHO 选项),此字符使后续输出都被丢弃。此字符在处理后即被丢弃(即不传送给读进程)。
DSUSP 延迟挂起作业控制字符(delayed-suspend job-control character)。在扩充模式(IEXTEN)下,若支持作业控制,并且已设置ISIG标志,则在输入时识别此字符。与SUSP字符的相同之处是:延迟挂起字符产生SIGTSTP信号,该信号被发送至前台进程组中的所有进程(见图9-7)。但是,信号产生的时间并不是在键入延迟挂起字符之时,而是在某个进程从控制终端读到此字符时才产生。此字符在处理后即被丢弃(即不传送给读进程)。
EOF 文件结束符。以规范模式(ICANON)进行输入时识别此字符。当键入此字符时,等待被读的所有字节都被立即传送给读进程。如果没有字节等待读,则返回0。在行首输入一个 EOF 字符是向程序指示文件结束的正常方式。此字符在规范模式下处理后即被丢弃(即不传送给读进程)。
EOL 附加的行定界符,与 NL 作用相同。以规范模式(ICANON)进行输入时识别此字符,并将此字符返回给读进程。但是此字符不常用。
EOL2 另一个行定界符,与NL作用相同。对此字符的处理方式与EOL字符相同。
ERASE 向前擦除字符(退格)。以规范模式(ICANON)输入时识别此字符。它擦除行中的前一个字符,但不会超越行首字符擦除上一行中的字符。此字符在规范模式下处理后即被丢弃(即不传送给读进程)。
ERASE2 供替换的向前擦除字符(退格)。对此字符的处理与向前擦除字符(ERASE)完全相同。
INTR 中断字符。若已设置ISIG标志,则在输入中识别此字符。它产生SIGINT信号,该信号被送至前台进程组中的所有进程(见图9-7)。此字符在处理后即被丢弃(即不传送给读进程)。
KILL 杀死字符。(名字“杀死”在这里又一次被误用,kill函数是用来将某一信号发送给进程的,而此字符应被称为行擦除符,它与信号毫无关系。)以规范模式(ICANON)输入时识别此字符。它擦除一整行,并在处理后即被丢弃(即不传送给读进程)。
LNEXT 下一个字符的字面值(literal-next character)。以扩充方式(IEXTEN)输入时识别此字符,它使下一个字符的任何特殊含意都被忽略。这对本节提及的所有特殊字符都起作用。使用这一字符可向程序键入任何字符。LNEXT字符在处理后即被丢弃,但输入的下一个字符被传送给读进程。
NL 换行字符,也被称为行定界符。不能更改此字符。以规范模式(ICANON)输入时识别此字符。此字符返回给读进程。
QUIT 退出字符。若已设置ISIG标志,则在输入中识别此字符。它产生SIGQUIT信号,该信号又被送至前台进程组中的所有进程(见图9-7)。此字符在处理后即被丢弃(即不传送给读进程)。
回忆图10-1,INTR和QUIT的区别是:QUIT字符不仅按默认规则终止进程,而且还产生一个core文件。
REPRINT 再打印字符。以扩充规范模式(设置了 IEXTEN和ICANON标志)进行输入时识别此字符。它使所有未读的输入被输出(再回显)。此字符在处理后即被丢弃(即不传送给读进程)。
START 启动字符。若已设置IXON标志,则在输入中识别此字符。若已设置IXOFF标志,则自动产生此字符作为输出。已设置IXON时,接收到的START 字符使停止的输出(由以前输入的STOP字符造成)重新启动。在此情形下,此字符在处理后即被丢弃(即不传送给读进程)。
STATUS BSD 的状态请求字符。以扩充规范模式(设置了 IEXTEN 和 ICANON 标志)进行输入时识别此字符。它产生 SIGINFO信号,该信号又被送至前台进程组中的所有进程(见图 9-7)。另外,如果没有设置NOKERNINFO标志,则有关前台进程组的状态信息也显示在终端上。此字符在处理后即被丢弃(即不传送给读进程)。
STOP 停止字符。若已设置IXON标志,则在输入中识别此字符。若已设置IXOFF标志,则自动产生此字符作为输出。已设置IXON时,接收到STOP字符则停止输出。在此情形下,此字符在处理后即被丢弃(即不传送给读进程)。当输入一个START字符后,被停止的输出重新启动。
SUSP 挂起作业控制字符。若支持作业控制并且已设置ISIG标志,则在输入中识别此字符。它产生SIGTSTP信号,该信号又被送至前台进程组的所有进程(见图9-7)。此字符在处理后即被丢弃(即不传送给读进程)。
已设置 IXOFF 标志时,若新的输入不会使输入缓冲区溢出,则终端驱动程序自动产生一个START字符来恢复以前被停止的输入。
已设置IXOFF时,终端驱动程序自动产生一个STOP字符以防止输入缓冲区溢出。
WERASE 字擦除字符。以扩充规范模式(设置了IEXTEN和ICANON标志)进行输入时识别此字符。它使前一个字被擦除。首先,它向前跳过任意一个空白字符(空格或制表符),然后再向前跃过前一记号,使光标处在前一个记号的第一个字符位置上。通常,前一个记号在碰到一个空白字符时即终止。但是,可通过设置ALTWERASE标志来改变这个行为。此标志使前一个记号在碰到第一个非字母、非数字字符时即终止。此字符在处理后即被丢弃(即不传送给读进程)。
需要为终端设备定义的另一个“字符”是 BREAK 字符。BREAK 实际上并不是一个字符,而是在异步串行数据传送时发生的一个条件。根据串行接口的不同,可以有多种方式通知设备驱动程序发生了BREAK条件。
大多数早期的串行终端都有一个标记为BREAK的键,用其可以产生BREAK条件,这就是为什么大多数人认为BREAK就是一个字符的原因。某些较新的终端键盘没有BREAK键。在PC上,BREAK键可能有其他用途。例如,键入Ctrl+BREAK可中断Windows命令解释器。
对于异步串行数据传送,BREAK是一个0值的位序列,其持续时间长于要求发送一个字节的时间。整个0值位序列被视为是一个BREAK。18.8节将说明如何用tcsendbreak函数发送一个BREAK。
为了获得和设置termios结构,可以调用tcgetattr和tcsetattr函数。这样就可以检测和修改各种终端选项标志和特殊字符,使终端按我们所希望的方式进行操作。
#include <termios.h>
int tcgetattr(int fd, struct termios *termptr);
int tcsetattr(int fd, int opt, const struct termios *termptr);
两个函数的返回值:若成功,返回0;若出错,返回-1
这两个函数都有一个指向termios结构的指针作为其参数,它们或者返回当前终端的属性,或者设置该终端的属性。因为这两个函数只对终端设备进行操作,所以若fd没有引用终端设备则出错返回-1,errno设置为ENOTTY。
tcsetattr的参数opt使我们可以指定在什么时候新的终端属性才起作用。opt可以指定为下列常量中的一个。
TCSANOW 更改立即发生。
TCSADRAIN 发送了所有输出后更改才发生。若更改输出参数则应使用此选项。
TCSAFLUSH 发送了所有输出后更改才发生。更进一步,在更改发生时未读的所有输入数据都被丢弃(冲洗)。
Tcsetattr 函数的返回状态在使用时易产生混淆。如果它执行了任意一种所要求的动作,即使未能执行所有要求的动作,它也返回OK(表示成功)。如果该函数返回OK,则我们有责任检查该函数是否执行了所有要求的动作。这就意味着,在调用tcsetattr设置所希望的属性后,需调用tcgetattr,然后将实际终端属性与所希望的属性相比较,以检测两者是否有区别。
在终端第一次被打开时,其属性视具体情况而定。一些系统可能会将终端属性初始化为具体实现所定义的值,另一些系统可能会保留并使用最后一次使用终端时的属性值。通过打开一个带有O_TTY_INIT标志(见3.3节)的驱动设备,可以确认终端的行为是否遵循标准,这样就能在调用tcgetattr 时,确保初始化termios结构中的任何非标准部分,使得在修改属性和调用tcgetattr时,终端的表现符合预期。
本节将列出所有不同的终端选项标志,扩展图18-3至图18-6中的说明。我们将按字母顺序列出各个选项并指出每个选项出现在 4 个终端标志字段中的哪一个。(从选项名字中看不出它所处的字段。)还将说明每个选项是否是Single UNIX Specification定义的,并列出了支持该选项的平台。
列出的所有选项标志(除所谓的屏蔽字标志外)都用一位或多位(设置或清除)表示。屏蔽字标志定义多个位,它们组合在一起,可以定义一组值。屏蔽字标志有一个定义名,每个值也有一个名字。例如,为了设置字符长度,首先用字符长度屏蔽字标志 CSIZE 将表示字符长度的位清0,然后设置下列值之一:CS5、CS6、CS7或CS8。
由Linux和Solaris支持的6个延迟值也有屏蔽字标志:BSDLY、CRDLY、FFDLY、NLDLY、TABDLY和VTDLY。对于每个延迟值的长度请参阅Solaris中的termio(7I)手册页。在所有情况下,延迟屏蔽字为0就表示没有延迟。如果指定了延迟,则由OFILL和OFDEL标志决定是由驱动器进行实际延迟还是只传输填充字符。
实例
图18-11演示了如何使用这些屏蔽字标志取一个值或者设置一个值。
图18-11 tcgetattr和tcsetattr实例
下面说明各选项标志。
ALTWERASE (c_lflag,FreeBSD、Mac OS X)已设置此标志时,若输入WERASE字符,则使用一个替换的字擦除算法。它不是向前移动到前一个空白字符为止,而是向前移动到第一个非字母、非数字字符为止。
BRKINT (c_iflag,POSIX.1、FreeBSD、Linux、Mac OS X、Solaris)若已设置此标志,而未设置 IGNBRK,则在接到 BREAK 时,冲洗输入、输出队列,并产生一个SIGINT 信号。如果此终端设备是一个控制终端,则此信号就是为前台进程组产生的。
若未设置IGNBRK和BRKINT,但是设置了PARMRK,则BREAK被读作一个3字节序列\377、\0和\0;若也未设置PARMRK,则BREAK被读作单个字符\0。
BSDLY (c_oflag,XSI、Linux、Solaris)退格延迟屏蔽字。此屏蔽字的值是BS0或BS1。(c_cflag,Solaris)扩充的波特率。用于允许大于 B38400 的波特率。(将在18.7节讨论波特率。) CBAUDEXT
CCAR_OFLOW (c_cflag,FreeBSD、Mac OS X)使用 RS-232调制解调器DCD(Data-Carrier-Detect,数据载波检测)信号打开输出的硬件流控制。这与早期的MDMBUF标志相同。
CCTS_OFLOW (c_cflag,FreeBSD、Mac OS X、Solaris)使用RS-232 CTS(Clear-To-Send,清除发送)信号打开输出的硬件流控制。
CDSR_OFLOW (c_cflag,FreeBSD、Mac OS X)根据RS-232 DSR(Data-Set-Ready,数据准备就绪)信号进行输出的流控制。
CDTR_IFLOW (c_cflag,FreeBSD,Mac OS X)根据RS-232 DTR(Data-Terminal-Ready,数据终端就绪)信号进行输入的流控制。
CIBAUDEXT (c_cflag,Solaris)扩充的输入波特率。用于允许大于B38400的输入波特率。
(将在18.7节讨论波特率。)
CIGNORE (c_cflag,FreeBSD、Mac OS X)忽略控制标志。
CLOCAL (c_cflag,POSIX.1、FreeBSD、Linux、Mac OS X、Solaris)若设置,则忽略调制解调器状态线。这通常意味着该设备是直接连接的。例如,若未设置此标志,则打开一个终端设备常常会遭遇阻塞,直到调制解调器回应呼叫并建立连接。
CMSPAR (c_oflag,Linux)选择标记或空奇偶校验。若已设置 PARODD,则奇偶校验位总是1(标记奇偶校验)。否则奇偶校验位总是0(空奇偶校验)。
CRDLY (c_oflag,XSI、Linux、Solaris)回车延迟屏蔽字。此屏蔽字的可能值是CR0、CR1、CR2和CR3。
CREAD (c_cflag,POSIX.1、FreeBSD、Linux、Mac OS X、Solaris)若设置,则接收者被启用,可以接收字符。
CRTSCTS (c_cflag,FreeBSD、Linux、Mac OS X、Solaris)其行为依赖于平台。对于Solaris,若设置该标志,则允许带外硬件流控制。在另外 3 个平台上,则既允许带内硬件流控制,又允许带外硬件流控制(等价于 CCTS_OFLOW|CRTS_IFLOW)。
CRTS_IFLOW (c_cflag,FreeBSD、Mac OS X、Solaris)输入的RTS(Request-To-Send,请求发送)流控制。
CRTSXOFF (c_cflag,Solaris)若设置,则允许带内硬件流控制,RS-232 RTS信号的状态控制了流控制。
CSIZE (c_cflag,POSIX.1、FreeBSD、Linux、Mac OS X、Solaris)此字段是一个屏蔽字标志,它指定发送和接收的每个字节的位数。此长度不包括可能有的奇偶校验位。由此屏蔽字定义的字段值是 CS5、CS6、CS7 和CS8,分别表示每个字节包含5位、6位、7位和8位。
CSTOPB (c_cflag,POSIX.1、FreeBSD、Linux、Mac OS X、Solaris)若设置,则使用两个停止位,否则只使用一个停止位。
ECHO (c_lflag,POSIX.1、FreeBSD、Linux、Mac OS X、Solaris)若设置,则将输入字符回显到终端设备。在规范模式和非规范模式下都可以回显输入字符。
ECHOCTL (c_lflag,FreeBSD、Linux、Mac OS X、Solaris)若设置并且也设置ECHO,则除ASCII TAB、ASCII NL以及START和STOP字符外,其他ASCII控制字符(ASCII字符集中0至八进制37对应的字符)都被回显为^X,其中,X是相应控制字符加上八进制100所构成的字符。例如,ASCII Ctrl+A字符(八进制1)被回显为^A。ASCII DELETE字符(八进制177)则回显为^?。若未设置此标志,则ASCII控制字符按其原样回显。如同ECHO标志,在规范模式和非规范模式下,此标志对控制字符回显都起作用。
应当了解的是,某些系统以不同方式回显EOF字符,因为EOF的典型值是Ctrl+D (而Ctrl+D是ASCII EOT字符,它可能使某些终端挂断)。请查看有关手册。
ECHOE (c_lflag,POSIX.1、FreeBSD、Linux、Mac OS X、Solaris)若设置并且也设置ICANON,则ERASE字符从显示中擦除当前行中的最后一个字符。这通常是在终端驱动程序中写一个3字符序列实现的,该序列是:退格、空格、退格。若支持WERASE字符,则ECHOE用一个或若干个上述3字符序列擦除前一个字。若支持 ECHOPRT 标志,则这里说明的关于 ECHOE 的动作是在假定未设置ECHOPRT标志的条件下得出的。
ECHOK (c_lflag,POSIX.1、FreeBSD、Linux、Mac OS X、Solaris)若设置并且也设置ICANON,则KILL字符从显示中擦除当前行,或者输出NL字符(用以强调已擦除整个行)。
若支持ECHOKE标志,则关于ECHOK的说明是在假定未设置ECHOKE标志的条件下得出的。
ECHOKE (c_lflag,FreeBSD、Linux、Mac OS X、Solaris)若设置并且也设置ICANON,则回显 KILL 字符的方式是擦除行中的每一个字符。擦除每个字符的方法则由ECHOE和ECHOPRT标志选择。
ECHONL (c_lflag,POSIX.1、FreeBSD、Linux、Mac OS X、Solaris)若设置并且也设置ICANON,即使没有设置ECHO,也回显NL字符。
ECHOPRT (c_lflag,FreeBSD、Linux、Mac OS X、Solaris)若设置并且也设置ICANON和ECHO,则ERASE字符(以及WERASE字符,若受到支持)使所有正被擦除的字符按它们被擦除的方式被打印。这一方法常在硬拷贝终端上显示其作用,它可以使我们确切地看到哪些字符正被刪除。
EXTPROC (c_lflag,FreeBSD、Linux、Mac OS X)若设置,规范字符处理在操作系统之外执行。如果串行通信外设卡能够通过执行某些行规程处理减轻主机处理器负载,那么就可以这样设置。在使用伪终端时(见第19章),也可以这样设置。
FFDLY (c_oflag,XSI、Linux、Solaris)换页延迟屏蔽字。此屏蔽字标志值是FF0或FF1。
FLUSHO (c_lflag,FreeBSD、Linux、Mac OS X、Solaris)若设置,则冲洗输出。当键入 DISCARD 字符时设置此标志。当键入另一个 DISCARD 字符时,此标志被清除。可以通过设置或清除此终端标志来设置或清除此条件。
HUPCL (c_cflag,POSIX.1、FreeBSD、Linux、Mac OS X、Solaris)若设置,则当最后一个进程关闭设备时,调制解调器控制线降至低电平(也就是调制解调器的连接断开)。
ICANON (c_lflag,POSIX.1、FreeBSD、Linux、Mac OS X、Solaris)若设置,则按规范模式工作(见18.10节)。这使下列字符起作用:EOF、EOL、EOL2、ERASE、KILL、REPRINT、STATUS和WERASE。输入字符被装配成行。
ICRNL (c_iflag,POSIX.1、FreeBSD、Linux、Mac OS X、Solaris)若设置并且未设置IGNCR,则将接收到的CR字符转换成NL字符。
IEXTEN (c_lflag,POSIX.1、FreeBSD、Linux、Mac OS X、Solaris)若设置,则识别并处理扩展的、由实现定义的特殊字符。
IGNBRK (c_iflag,POSIX.1、FreeBSD、Linux、Mac OS X、Solaris)在已设置时,忽略输入中的BREAK条件。关于BREAK条件是产生SIGINT信号还是被作为数据读取,见BRKINT。
IGNCR (c_iflag,POSIX.1、FreeBSD、Linux、Mac OS X、Solaris)若设置,则忽略接收到的CR字符。若未设置此标志,而设置了ICRNL标志,则有可能将接收到的CR字符转换成NL字符。
IGNPAR (c_iflag,POSIX.1、FreeBSD、Linux、Mac OS X、Solaris)在已设置时,忽略带有结构出错(非BREAK)或奇偶出错的输入字节。
IMAXBEL (c_iflag,FreeBSD、Linux、Mac OS X、Solaris)当输入队列满时响铃。
INLCR (c_iflag,POSIX.1、FreeBSD、Linux、Mac OS X、Solaris)若设置,则将接收到的NL字符转换成CR字符。
如果不以规范模式工作,则读请求直接从输入队列取字符。在至少接到MIN个字节或两个字节之间的超时值TIME到期时,read才返回。详细情况参见18.11节。
INPCK (c_iflag,POSIX.1、FreeBSD、Linux、Mac OS X、Solaris)在已设置时,使输入奇偶校验起作用。若未设置INPCK,则使输入奇偶校验不起作用。
奇偶“产生和检测”和“输入奇偶校验”是两件不同的事。奇偶位的产生和检测是由PARENB标志控制的。设置该标志后通常会使串行接口的设备驱动程序对输出字符产生奇偶位,对输入字符则验证其奇偶性。PARODD 标志决定该奇偶性应当是奇还是偶。如果一个其奇偶性错误的输入字符到来,则检查INPCK标志的状态。若已设置此标志,则检查IGNPAR标志(以决定是否应忽略带奇偶出错的输入字节);若不应忽略此输入字节,则检查PARMRK标志以决定应该向读进程传送哪些字符。
ISIG (c_lflag,POSIX.1、FreeBSD、Linux、Mac OS X、Solaris)若设置,则判别输入字符是否是要产生终端信号的特殊字符(INTR、QUIT、SUSP和DSUSP);若是,则产生相应信号。
ISTRIP (c_iflag,POSIX.1、FreeBSD、Linux、Mac OS X、Solaris)在已设置此标志时,有效输入字节被剥离为7位。在未设置时,则处理全部8位。
IUCLC (c_iflag,Linux、Solaris)将输入的大写字符转换成小写字符。
IUTF8 (c_iflag,Linux、Mac OS X)允许使用UTF-8多字节字符进行字符擦除处理。
IXANY (c_iflag,XSI、FreeBSD、Linux、Mac OS X、Solaris)使任何字符都能重新启动输出。
IXOFF (c_iflag,POSIX.1、FreeBSD、Linux、Mac OS X、Solaris)若设置,则使启动-停止输入控制起作用。当终端驱动程序发现输入队列将要填满时,输出一个STOP字符。此字符应当由发送数据的设备识别,并使该设备停止。此后,当把输入队列中的字符处理完毕之后,终端驱动程序将输出一个START字符,使该设备恢复发送数据。
IXON (c_iflag,POSIX.1、FreeBSD、Linux、Mac OS X、Solaris)若设置,则使启动-停止输出控制起作用。当终端驱动程序接收到一个STOP字符时,输出停止。在输出停止时,下一个START字符恢复输出。若未设置此标志,则START和STOP字符由进程作为一般字符读取。
MDMBUF (c_cflag,FreeBSD、Mac OS X)按照调制解调器的载波标志进行输出流控制。这是CCAR_OFLOW标志的曾用名。
NLDLY (c_oflag,XSI、Linux、Solaris)换行延迟屏蔽字。此屏蔽字的值是NL0或NL1。
NOFLSH (c_lflag,POSIX.1、FreeBSD、Linux、Mac OS X、Solaris)按系统默认,当终端驱动程序产生 SIGINT 和 SIGQUIT 信号时,输入和输出队列都被冲洗。另外,当它产生SIGSUSP信号时,输入队列被冲洗。若已设置NOFLSH标志,则在这些信号产生时,不对输入、输出队列进行常规冲洗。
NOKERNINFO (c_lflag,FreeBSD、Mac OS X)在已设置时,此标志阻止STATUS字符打印前台进程组的信息。但是无论是否设置此标志,STATUS 字符都会使 SIGINFO信号被发送至前台进程组。
OCRNL (c_oflag,XSI、FreeBSD、Linux、Solaris)若设置,则将输出的 CR 字符转换成NL字符。
OFDEL (c_oflag,XSI、Linux、Solaris)若设置,则输出填充字符是ASCII DEL;否则是ASCII NUL。见OFILL标志。
OFILL (c_oflag,XSI、Linux、Solaris)若设置,则传递填充字符(ASCII DEL 或ASCII NUL,见OFDEL标志)以实现延迟,而不使用时间延迟。见6个延迟屏蔽字标志:BSDLY、CRDLY、FFDLY、NLDLY、TABDLY和VTDLY。
OLCUC (c_oflag,Linux、Solaris)若设置,则将小写字符转换成大写字符。
NLCR (c_oflag,XSI、FreeBSD、Linux、Mac OS X、Solaris)若设置,将输出的NL字符转换成CR-NL字符。
ONLRET (c_oflag,XSI、FreeBSD、Linux、Solaris)若设置,则假定输出的 NL 字符执行回车功能。
ONOCR (c_oflag,XSI、FreeBSD、Linux、Solaris)若设置,则在0列不输出CR字符。
ONOEOT (c_oflag,FreeBSD、Mac OS X)若设置,则在输出中丢弃EOT(^D)字符。在某些将Ctrl+D解释为挂断的终端上,设置此标志可能是必需的。
OPOST (c_oflag,POSIX.1、FreeBSD、Linux、Mac OS X、Solaris)若设置,则进行实现定义的输出处理。关于c_oflag字段的各种实现定义标志,见图18-6。
OXTABS (c_oflag,FreeBSD、Mac OS X)若设置,则制表符在输出中被扩展为空格。这与将水平制表符延迟(TABDLY)设置为XTABS或TAB3所产生的效果相同。
PARENB (c_cflag,POSIX.1、FreeBSD、Linux、Mac OS X、Solaris)若设置,则对输出字符产生奇偶位,对输入字符执行奇偶校验。若已设置PARODD,则奇偶校验是奇校验;否则是偶校验。另见对INPCK、IGNPAR和PARMRK标志的讨论。
PAREXT (c_cflag,Solaris)选择标记或空奇偶性。若PARODD设置,则奇偶位总是1 (标记奇偶性);否则,奇偶位总是0(空奇偶性)。
PARMRK (c_iflag,POSIX.1、FreeBSD、Linux、Mac OS X、Solaris)在已设置时,若未设置IGNPAR,则带有结构出错(非BREAK)的字节或带有奇偶出错的字节将被进程读作一个3字符序列\377、\0和X,其中X是接收到的出错字节。若未设置ISTRIP,则一个有效的\377被传送给进程时为\377, \377。若未设置IGNPAR和PARMRK,则带有结构出错误或奇偶出错的字节都被读作一个字符\0。
PARODD (c_cflag,POSIX.1、FreeBSD、Linux、Mac OS X、Solaris)若设置,则输出和输入字符的奇偶性都是奇,否则为偶。注意,PARENB 标志控制奇偶性的产生和检测。
PENDIN (c_lflag,FreeBSD、Linux、Mac OS X、Solaris)若设置,则在下一个字符输入时,尚未读的任何输入都由系统重新打印。这一动作与键入REPRINT字符时的作用相类似。
TABDLY (c_oflag,XSI、Linux、Mac OS X、Solaris)水平制表符延迟屏蔽字。此屏蔽字的值是TAB0、TAB1、TAB2或TAB3。
在已设置CMSPAR或PAREXT标志时,PARODD标志也控制是否使用标记或空奇偶性。
XTABS 的值等于 TAB3。此值使系统将制表符扩展成空格。系统假定制表符的长度为8个空格,不能更改此假定。
TOSTOP (c_lflag,POSIX.1、FreeBSD、Linux、Mac OS X、Solaris)若设置,并且该实现支持作业控制,则将信号SIGTTOU 送到试图写控制终端的一个后台进程的进程组。按默认,此信号暂停该进程组中所有进程。如果写控制终端的后台进程忽略或阻塞此信号,则终端驱动程序不产生此信号。
VTDLY (c_oflag,XSI、Linux、Solaris)垂直制表延迟屏蔽字。此屏蔽字的值是VT0和VT1。
XCASE (c_lflag,Linux、Solaris)若设置,并且也设置ICANON,则终端被假定为只支持大写字符,全部输入转换为小写字符。要想输入一个大写字符,要在其前面加一个反斜杠。与之类似,系统输出大写字符时,也要在其前面加一个反斜杠。(如今这个选项标志已弃用,因为只支持大写字符的终端即使不是全部,也是绝大部分都已经不存在了。)
上节说明的所有选项都可以被检查和更改:在程序中用 tcgetattr 和 tcsetattr 函数(见18.4节)进行检查和更改;在命令行(或shell脚本)中用stty(1)命令进行检查和更改。简单地说,stty(1)命令就是图18-7中所列的前6个函数的接口。如果以-a选项执行此命令,则显示终端的所有选项:
$ stty -a
speed 9600 baud; 25 rows; 80 columns;
lflags: icanon isig iexten echo echoe -echok echoke -echonl echoctl
-echoprt -altwerase -noflsh -tostop -flusho pendin -nokerninfo
-extproc
iflags: -istrip icrnl -inlcr -igncr ixon -ixoff ixany imaxbel -ignbrk
brkint -inpck -ignpar -parmrk
oflags: opost onlcr -ocrnl -oxtabs -onocr -onlret
cflags: cread cs8 -parenb -parodd hupcl -clocal -cstopb -crtscts
-dsrflow -dtrflow -mdmbuf
cchars: discard = ^O; dsusp = ^Y; eof = ^D; eol = <undef>;
eol2 = <undef>; erase = ^H; erase2 = ^?; intr = ^C; kill = ^U;
lnext = ^V; min = 1; quit = ^; reprint = ^R; start = ^Q;
status = ^T; stop = ^S; susp = ^Z; time = 0; werase = ^W;
若在选项名前有一个连字符,表示该选项禁用。最后4行显示各终端特殊字符(见18.3节)的当前设置。第1行显示当前终端窗口的行数和列数,18.12节将对终端窗口大小进行讨论。
stty命令使用它的标准输入获得和设置终端的选项标志。虽然,某些较早的实现使用标准输出,但POSIX.1要求使用标准输入。本书讨论的4种实现提供了在标准输入上操作的stty版本。
这意味着如果希望了解名为ttyla的终端的设置,那么可以键入
stty -a </dev/ttyla
术语波特率(baud rate)是一个历史沿用的术语,现在它指的是“位/秒”(bit per second)。虽然大多数终端设备对输入和输出使用同一波特率,但是只要硬件许可,可以将它们设置为两个不同值。
#include <termios.h>
speed_t cfgetispeed(const struct termios *termptr);
speed_t cfgetospeed(const struct termios *termptr);
两个函数的返回值:波特率值
int cfsetispeed(struct termios *termptr, speed_t speed);
int cfsetospeed(struct termios *termptr, speed_t speed);
两个函数的返回值:若成功,返回0;出错,返回-1
两个cfget函数的返回值,以及两个cfset函数的speed参数都是下列常量之一:B50、B75、B110、B134、B150、B200、B300、B600、B1200、B1800、B2400、B4800、B9600、B19200或B38400。常量B0表示“挂断”。在调用tcsetattr时,如若将输出波特率指定为B0,则调制解调器的控制线就不再起作用。
大多数系统定义了另外的波特率值,如B57600以及B115250。
使用这些函数时,必须认识到输入、输出波特率是存储在设备的termios结构中的,如图18-8所示。在调用两个cfget函数中的任意一个之前,要先用tcgetattr获得设备的termios结构。与此类似,在调用两个cfset函数中的任意一个之后,要做的就是在termios结构中设置波特率。为使这种更改影响到设备,应当调用tcsetattr函数。即使所设置的两个波特率中的任意一个出错,在调用tcsetattr之前可能也不会发现这个错误。
这4个波特率函数的存在使应用程序不必考虑具体实现在termios结构中表示波特率的不同方法。Linux和BSD派生的平台趋向于存储波特率的数值。(即9 600波特率存储成值9 600),然而,System V派生的平台(如Solaris)趋向于以位屏蔽方式编码波特率。从cfget函数得到的速度值以及向cfset函数传送的速度值都未转换,与它们存储在termios结构中的表示形式一样。
下列4个函数提供了终端设备的行控制能力。4个函数都要求参数fd引用一个终端设备,否则出错返回-1,errno设置为ENOTTY。
#include <termios.h>
int tcdrain(int fd);
int tcflow(int fd, int action);
int tcflush(int fd, int queue);
int tcsendbreak(int fd, int duration);
4个函数的返回值:若成功,返回0;若出错,返回-1
tcdrain 函数等待所有输出都被传递。tcflow 函数用于对输入和输出流控制进行控制。action参数必定是下列4个值之一。
TCOOFF 输出被挂起。
TCOON 重新启动以前被挂起的输出。
TCIOFF 系统发送一个STOP字符,这将使终端设备停止发送数据。
TCION 系统发送一个START字符,这将使终端设备恢复发送数据。
tcflush函数冲洗(抛弃)输入缓冲区(其中的数据是终端驱动程序已接收到,但用户程序尚未读取的)或输出缓冲区(其中的数据是用户程序已经写入,但尚未被传递的)。queue参数必定是下列3个常量之一。
TCIFLUSH 冲洗输入队列。
TCOFLUSH 冲洗输出队列。
TCIOFLUSH 冲洗输入队列和输出队列。
tcsendbreak函数在一个指定的时间区间内发送连续的0值位流。若duration参数为0,则此种传递延续0.25~0.5秒。POSIX.1说明若duration非0,则传递时间依赖于实现。
历史上,在大多数UNIX系统版本中,控制终端的名字一直是/dev/tty。POSIX.1提供了一个运行时函数,可用来确定控制终端的名字。
#include <stdio.h>
char *ctermid(char *ptr);
返回值:若成功,返回指向控制终端名的指针;若出错,返回指向空字符串的指针
如果ptr非空,则被认为是一个指针,指向长度至少为 L_ctermid 字节的数组,进程的控制终端名存储在该数组中。常量L_ctermid被定义在<stdio.h>中。若ptr是一个空指针,则该函数为数组(通常作为静态变量)分配空间。同样,进程的控制终端名存储在该数组中。
在这两种情况中,该数组的起始地址都被作为函数值返回。因为大多数 UNIX 系统都使用/dev/tty作为控制终端名,所以此函数的主要作用是改善向其他操作系统的可移植性。
当调用ctermid函数时,本书说明的所有4种平台都返回字符串/dev/tty。
实例:ctermid函数
图18-12给出的是POSIX.1 ctermid函数的一个实现。
图18-12 POSIX.1 ctermid函数的实现
注意,因为我们无法确定调用者的缓冲区大小,所以也就不能防止过度使用该缓冲区。
另外还有两个UNIX 系统比较感兴趣的函数:isatty 和ttyname。如果文件描述符引用一个终端设备,则isatty返回真。ttyname返回的是在该文件描述符上打开的终端设备的路径名。
#include <unistd.h>
int isatty(int fd);
返回值:若为终端设备,返回1(真);否则,返回0(假)
char *ttyname(int fd);
返回值:指向终端路径名的指针;若出错,返回NULL
实例:isatty函数
如图18-13 所示,isatty 函数很容易实现。我们只尝试使用了其中一个终端专用函数(如果成功执行,它不改变任何东西),并查看了其返回值。
图18-13 POSIX.1 isatty函数的实现
使用图18-14中的程序测试isatty函数。
图18-14 测试isatty函数
运行图18-14中的程序,得到如下输出:
$ ./a.out
fd 0: tty
fd 1: tty
fd 2: tty
$ ./a.out </etc/passwd 2>/dev/null
fd 0: not a tty
fd 1: tty
fd 2: not a tty
实例:ttyname函数
ttyname函数(见图18-15)比较长,因为它要搜索所有设备表项,寻找匹配项。
图18-15 POSIX.1 ttyname函数的实现
此处使用的技术是读/dev目录,寻找具有相同设备号和i节点编号的表项。回忆4.24节,每个文件系统都有一个唯一的设备号(stat 结构中的 st_dev 字段,见 4.2 节),文件系统中的每个目录项都有一个唯一的 i 节点编号(stat 结构中的 st_ino 字段)。在此函数中,假定在找到一个匹配的设备号和匹配的i节点号时,就能找到所希望的目录项。也能验证这两个表项与 st_rdev 字段(终端设备的主设备号和次设备号)相匹配,还能验证该目录项是一个字符特殊文件。但是,因为已经验证了文件描述符参数既是一个终端设备,又是一个字符特殊文件,而且因为在UNIX系统中,匹配的设备号和i节点编号是唯一的,所以不再需要进行另外的比较。
终端名可能在/dev的子目录中。于是,需要搜索/dev下的整个文件系统树。我们跳过了少数几个可能会产生不正确结果或奇怪结果的目录:/dev/.、/dev/..和/dev/fd。我们也跳过了一些别名:/dev/stdin、/dev/stdout以及/dev/stderr,因为它们是/dev/fd目录中文件的符号链接。
使用图18-16中的程序测试这一实现。
图18-16 测试ttyname函数
运行图18-16中的程序,得到:
$ ./a.out < /dev/console 2> /dev/null
fd 0: /dev/console
fd 1: /dev/ttys001
fd 2: not a tty
规范模式很简单:发一个读请求,当一行已经输入后,终端驱动程序即返回。以下几个条件造成读返回。
•所请求的字节数已读到时,读返回。无需读一个完整的行。如果读了部分行,那么也不会丢失任何信息,下一次读从前一次读的停止处开始。
•当读到一个行定界符时,读返回。回忆 18.3 节,在规范模式中,下列字符被解释为“行结束”:NL、EOL、EOL2和EOF。另外,在18.5节中也曾说明,如若已设置ICRNL,但未设置IGNCR,则CR字符的作用与NL字符一样,也终止一行。
在这5个行界定符中,只有一个EOF符在终端驱动程序对其进行处理后即被丢弃。其他4个字符则作为其所处行的最后一个字符返回给调用者。
•如果捕捉到信号,并且该函数不再自动重启(见10.5节),则读也返回。
实例:getpass函数
下面说明getpass函数,它读入用户在终端上键入的口令。此函数由login(1)和crypt(1)程序调用。为了读取口令,该函数必须关闭回显,但仍可使终端以规范模式进行工作,因为不管键入什么作为口令都能构成一个完整行。图18-17显示了UNIX系统中的一个典型实现。
图18-17 getpass函数的实现
在此例中,应当考虑以下几个方面。
•调用ctermid函数打开控制终端,而不是直接将/dev/tty写在程序中。
•只是读、写控制终端,如果不能以读、写模式打开此设备则出错返回。还有一些其他的使用约定。在GNU C函数库版本中,如果不能以读、写模式打开控制终端,则getpass读取标准输入,写到标准错误。在Solaris版本中,如果不能打开控制终端,则getpass失败。
•阻塞两个信号SIGINT和SIGTSTP。如果不这样做,在输入INTR字符时就会使程序异常中止,并使终端仍处于禁止回显状态。与此相类似,输入 SUSP 字符时将使程序停止,并且在禁止回显状态下返回到 shell。在禁止回显时,我们选择了阻塞这两个信号。如果这两个信号是在读取口令期间产生的,则它们会一直被保持,直到getpass返回,阻塞才会解除。也有其他方法来处理这些信号。有些getpass版本忽略SIGINT(保存它以前的动作),在返回前将其动作恢复为以前的值。这就意味着,在该信号被忽略期间所发生的这种信号都会丢失。其他版本捕捉 SIGINT(保存它以前的动作),如果捕捉到此信号,则在恢复终端状态和信号动作后,用kill函数发送此信号。没有一个getpass版本捕捉、忽略或阻塞SIGQUIT,所以输入QUIT字符就会使程序异常中止,并且很可能使终端保持在禁止回显状态。
• 请注意,某些shell,尤其是Korn shell,在以交互方式读输入时都使终端处于回显状态。这些shell是提供命令行编辑的shell,因此在每次输入一条交互命令时都处理终端状态。所以如果在这种shell下调用此程序,并且用QUIT字符使其异常中止,则这种shell可能会恢复回显状态。其他不提供命令行编辑的shell(如Bourne shell)将使程序异常中止,并使终端保持在不回显状态。如果对终端做了这种操作,则stty命令能使终端恢复到回显状态。
•使用标准I/O读、写控制终端。我们特地将流设置为不带缓冲的,否则在流的读、写之间可能会有某些交叉(这样就需要多次调用 fflush)。也可使用不带缓冲的 I/O(见第 3章),但是在这种情况下就只能用read来模仿getc函数。
•最多只存储8个字符作为口令。输入的其他多余字符则全部被忽略。
图18-18中的程序调用getpass并且打印我们输入的内容。这是为了验证ERASE和KILL字符能否正常工作(如同它们在规范模式下应该表现的那样)。
图18-18 调用getpass函数
如果调用 getpass 函数的程序使用的是明文口令,那么为了安全起见,在程序完成后应在内存中清除它。如果该程序会产生其他用户可能读取的core文件(回忆10.2节,core的系统默认许可权使每个用户都能读它),或者如果某个其他进程能够设法读该进程的存储空间,则它们就可能会读到这个明文口令。(“明文”是指我们在 getpass 打印的提示符处键入的口令。大多数UNIX系统程序会对这个明文口令进行修改,将它转换成一个“加密”口令。例如,口令文件(见6.2节)中的pw_passwd字段包含的是加密口令,而不是明文口令。)
可以通过关闭termios结构中c_lflag字段的ICANON标志来指定非规范模式。在非规范模式中,输入数据不装配成行,不处理下列特殊字符(见18.3节):ERASE、KILL、EOF、NL、EOL、EOL2、CR、REPRINT、STATUS和WERASE。
如前所述,规范模式很容易理解:系统每次至多返回一行。但在非规范模式下,系统如何知道在什么时候将数据返回给我们呢?如果它一次返回一个字节,那么系统开销就会过大。(回忆图3-6,从中可以看到每次读一个字节的开销有多大。如果每次返回的数据加倍,那么系统调用的开销就可以减半。)在启动读数据之前,往往不知道要读多少数据,所以系统不能总是一次返回多个字节。
解决方法是,当已读了指定量的数据后,或者已经超过了给定量的时间后,即通知系统返回。这种技术使用了termios结构中c_cc数组的两个变量:MIN和TIME。c_cc数组中的这两个元素的下标名为VMIN和VTIME。
MIN指定一个read返回前的最小字节数。TIME指定等待数据到达的分秒数(分秒为秒的1/10)。有下列4种情形。
情形A:MIN>0,TIME>0
TIME指定一个字节间定时器(interbyte timer),它只在第一个字节被接收时启动。
在该定时器超时之前,若已接到MIN个字节,则read返回MIN个字节。如果在
接到MIN 个字节之前,该定时器已超时,则read返回已接收到的字节。(因为定
时器是在第一个字节被接收后启动的,所以在定时器超时时,read 至少会返回一
个字节。)在这种情形中,第一个字节被接收之前,调用者会一直阻塞。如果在调
用read时数据已经可用,则就如同在read后数据被立即接收了一样。
情形B:MIN>0,TIME==0
read在接收到MIN个字节之前不返回。这会造成read无限期阻塞。
情形C:MIN==0,TIME>0
TIME指定一个调用read时启动的读定时器。(与情形A相比较,两者是不同的。
在情形A中,非0 TIME表示字节间定时器,该定时器要等到第一个字节被接收时
才启动。)在接到一个字节或者该定时器超时时,read即返回。如果是定时器超时,
则read返回0。
情形D:MIN==0,TIME==0
如果有数据可用,则 read 最多返回所要求的字节数。如果无数据可用,则 read
立即返回0。
在所有这些情形中,MIN 只是最小值。如果程序要求的数据多于 MIN 个字节,那么它或许能接收到所要求的字节数。这也适用于MIN==0的情形C和情形D。
图18-19总结并列出了非规范模式输入的4种不同情形。在这个图中,nbytes是read的第三个参数(返回的最大字节数)。
图18-19 非规范输入的4种情形
请注意,POSIX.1允许下标VMIN和VTIME的值分别与VEOF和VEOL的相同。确实,Solaris就是这样做的,这样就提供了与System V的早期版本的兼容性。但是,这也带来了可移植性问题。从非规范模式转换为规范模式时,必须恢复VEOF和VEOL。如果VMIN等于VEOF,且不恢复它们的值,那么当把VMIN的典型值设置为1时,文件结束符就变成了Ctrl+A。解决这一问题最简单的方法是:在要转入非规范模式时,将整个termios结构保存起来,以后再要转回规范模式时恢复它。
实例
图18-20中的程序定义了函数tty_cbreak和tty_raw,它们将终端分别设置为cbreak模式(cbreak mode)和原始模式(raw mode)。(术 语cbreak和原始来自于V7的终端驱动程序。) tty_reset函数的功能是将终端恢复到原始的工作状态(也就是调用tty_cbreak或tty_raw之前的工作状态)。
如果已调用 tty_cbreak,那么在调用 tty_raw 之前需要调用 tty_reset。如果已调用tty_raw,然后又要调用 tty_cbreak,那么在此之前同样也要调用 tty_reset。这减少了出错时终端处于不可用状态的机会。
该程序还提供了另外两个函数:tty_atexit和tty_termios。tty_atexit可被登记为退出处理程序,以保证exit 恢复终端工作模式。tty_termios 则返回一个指向原来规范模式下termios结构的指针。
图18-20 将终端模式设置为cbreak模式或原始模式
cbreak模式的定义如下。
•非规范模式。如本节开始处所述,这种模式关闭了对某些输入字符的处理。这种模式没有关闭对信号的处理,所以用户始终可以键入一个能够触发终端产生信号的字符。请注意,调用者应当捕捉这些信号,否则这种信号就有可能终止程序,并且使终端保持在cbreak模式。
作为一般规则,在编写更改终端模式的程序时,应当捕捉大多数信号,以便在程序终止前恢复终端模式。
•关闭回显。
•每次输入一个字节。为此,将MIN设置为1,将TIME设置为0。这是图18-19中的情形B。至少有一个字节可用时,read才返回。
对原始模式的定义如下。
•非规范模式。也关闭了对信号产生字符(ISIG)和扩充输入字符(IEXTEN)的处理。
另外还禁用了BRKINT字符,使BREAK字符不再产生信号。
•关闭回显。
•禁止输入中的CR到NL映射(ICRNL)、输入奇偶检测(INPCK)、剥离输入字节的第8位(ISTRIP)以及输出流控制(IXON)。
•8位字符(CS8),且禁用奇偶校验(PARENB)。
•禁止所有输出处理(OPOST)。
•每次输入一个字节(MIN=1,TIME=0)。
图18-21中的程序测试原始模式和cbreak模式。
图18-21 测试原始终端模式和cbreak终端模式
运行图18-21中的程序,可以观察这两种终端工作模式的工作情况。
$ ./a.out
Enter raw mode characters, terminate with DELETE
4
33
133
61
70
176
键入Delete
Enter cbreak mode characters, terminate with SIGINT
1 键入Ctrl+A
10 键入退格
signal caught 键入中断键
在原始模式中,输入的字符是Ctrl+D(04)和特殊功能键F7。在所用的终端上,此功能键产生5个字符:ESC(033)、[(0133)、1(061)、8(070)和˜(0176)。注意,在原始模式下关闭了输出处理(˜OPOST),所以在每个字符后没有得到回车符。另外还要注意的是,在cbreak模式下,不对输入特殊字符进行处理(因此没对 Ctrl+D、文件结束符和退格进行特殊处理),但是仍对终端产生的信号进行处理。
大多数UNIX系统都提供了一种跟踪当前终端窗口大小的方法,在窗口大小发生变化时,使内核通知前台进程组。内核为每个终端和伪终端都维护了一个winsize结构:
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) */
};
此结构的规则如下。
•用ioctl(见3.15节)的TIOCGWINSZ命令可以取此结构的当前值。
•用 ioctl 的 TIOCSWINSZ 命令可以将此结构的新值存储到内核中。如果此新值与存储在内核中的当前值不同,则前台进程组会收到SIGWINCH信号。(注意,从图10-1中可以看出,此信号的系统默认动作是被忽略。)
•除了存储此结构的当前值以及在此值改变时产生一个信号以外,内核对该结构不进行任何其他操作。对结构中的值进行解释完全是应用程序的工作。
提供这种功能的目的是,当窗口大小发生变化时应用程序能够得到通知(如vi编辑器)。应用程序接收此信号后,可以获取窗口大小的新值,然后重绘屏幕。
实例
图 18-22 所示的程序打印当前窗口大小,然后休眠。每次窗口大小改变时,程序就捕捉到SIGWINCH信号,然后打印新的窗口大小。我们必须用一个信号终止此程序。
图18-22 打印窗口大小
在一个带窗口终端的系统上运行图18-22中的程序得到:
$ ./a.out
35 rows, 80 columns 初始大小
SIGWINCH received 更改窗口大小:捕捉到信号
40 rows, 123 columns
SIGWINCH received 再一次
42 rows, 33 columns
^C $ 键入中断键以终止
termcap 的意思是终端能力(terminal capability),它涉及文本文件/etc/termcap 和一套读此文件的例程。termcap 这种技术是在伯克利开发的,注意是为了支持 vi 编辑器。termcap文件包含了对各种终端的说明:终端支持哪些功能(如行数、列数、终端是否支持退格),如何使终端执行某些操作(如清屏、将光标移动到给定位置)。把这些信息从编译过的程序中取出来并把它们放在易于编辑的文本文件中,这样就使得vi编辑器能在很多不同的终端上运行。
最后,将支持termcap文件的例程从vi编辑器中抽取出来,放在一个单独的curses库中。为使这套库可供要进行屏幕处理的任何程序使用,还增加了很多功能。
termcap这种技术并不是很完善。当越来越多的终端被加到数据文件中时,为找到一个特定的终端,需要花费更长的时间扫描此数据文件。这个数据文件还用两个字符的名字来标识不同的终端属性。这些缺陷迫使开发人员开发出了 terminfo 以及与其相关的curses库。在terminfo中,终端说明基本上都是文本说明的编译版本,在运行时易于被快速定位。terminfo 最初由SVR2开始使用,此后所有System V的版本都使用它。
历史上,基于System V的系统使用terminfo,BSD派生的系统使用termcap,但是现在,系统通常两者都提供。然而Mac OS X仅支持terminfo。
Goodheart[1991]对terminfo和curses库进行了详细说明,但此书已不再增印。Strang[1986]说明了curses函数库的伯克利版本。Strang、Mui和O’Reilly[1988]则对termcap和terminfo进行了说明。
可在http://invisible-island.net/ncurses/ncurses.html或http://www.gnu. org/software/ncurses上找到与SVR4 curses接口兼容的开放版ncurses函数库。
不论是 termcap 还是 terminfo,它们本身都不处理本章所述及的问题:更改终端的模式、更改终端特殊字符、处理窗口大小等。它们所提供的是在各种终端上执行典型操作(清屏、移动光标)的方法。另一方面,在本章所述问题方面,curses 能提供某种具体细节方面的帮助。curses提供了很多函数,用来设置原始模式、设置cbreak模式、打开和关闭回显等。注意,curses 库是为基于字符的哑终端设计的,而如今,它们大部分已被以基于像素的图形终端所代替。
终端有很多特征和选项,其中大多数都可按需进行更改。本章描述了很多更改终端操作(即更改特殊输入字符和可选择标志)的函数,还介绍了可对终端设备进行设置或恢复的各个终端特殊字符以及众多选项。
终端的输入模式有两种—规范的(每次一行)和非规范的。本章中包含了若干这两种工作模式的实例,也提供了一些函数,它们在POSIX.1终端选项和较早的BSD cbreak模式及原始模式之间进行映射。本章还说明了如何获取和改变终端窗口大小。
18.1 编写一个调用 tty_raw 并且不恢复终端模式就终止的程序。如果系统提供 reset(1)命令(本书说明的4种平台全都提供),使用该命令恢复终端模式。
18.2 c_cflag字段的PARODD标志允许我们设置奇检验或偶校验,而BSD中的tip程序也允许奇偶校验位为0或1。它是如何实现的?
18.3 如果你系统中的stty(1)命令输出MIN和TIME值,做下面的练习。登录系统两次,其中一次登录时打开vi编辑器,在另外一次登录中用stty命令确定vi设置的MIN和TIME值(因为vi将终端设置为非规范模式)。(如果你的终端上有窗口系统正在运行,那么你也可以进行同样的测试,方法是:登录一次,然后用两个分开的窗口。)