20世纪70年代后期,系统Ⅲ(UNIX System III)发展出一套不同于V7( Version 7 Unix)的终端IO例程,使得UNIX终端IO处理分立为两种不同风格:一种是系统Ⅲ风格,它延续到了System V;另一种是V7风格,它成为了BSD派生的系统的标准。和信号一样,POSIX.1在这两种分隔的基础上制定了终端IO的标准,本章介绍POSIX.1终端IO标准。

终端IO之所以复杂,部分原因是人们将其应用到很多地方:终端、计算机之间的直接连接、调制解调器、打印机等。

终端IO有两种工作模式:
1.规范模式输入处理:终端输入以行为单位进行处理,对于每个读请求,终端驱动程序最多返回一行。
2.非规范模式输入处理:输入字符不装配成行。

默认模式是规范模式,如shell将标准输入重定向到终端,并用read和write函数将标准输入复制到标准输出时,终端以规范模式进行工作,每次read调用只返回一行。处理整个屏幕的程序(如vi编辑器)使用非规范模式,原因是它的命令不以换行符终止且可能是单个字符;且该编辑器不希望对特殊字符进行处理,因为这些字符可能与编辑命令中使用的字符重叠,如Ctrl+D字符通常是终端的文件结束符,但在vi中它是向下滚动半个屏幕的命令。

V7和较早的BSD风格的终端驱动程序支持3种终端输入模式:
1.精细加工模式:输入装配成行,并对特殊字符进行处理。
2.原始模式:输入不装配成行,也不对特殊字符进行处理。
3.cbreak模式:输入不装配成行,但对某些特殊字符进行处理。

POSIX.1定义了11个特殊输入字符,如文件结束符(Ctrl+D)、挂起字符(Ctrl+Z),其中9个可以更改。

可认为终端设备是由通常位于内核中的终端驱动程序控制的。每个终端设备都有一个输入队列和一个输出队列:

对于上图的说明:
1.如果打开了回显功能,则在输入队列和输出队列之间有一个隐含的连接。
2.输入队列的长度MAX_INPUT是有限值,当一个特定设备的输入队列已经填满时,系统的行为将依赖于实现,这种情况大多UNIX系统回显报警符号(\a,可用Ctrl+G输入)。
3.上图中没有显示另一个输入限制MAX_CANON,这是一个规范输入行的最大字节数。
4.虽然输出队列长度也是有限的,但程序不能获得定义输出队列长度上限的常量,因为当输出队列将要填满时,内核会使写进程休眠,直至写队列中有可用空间。
5.我们将说明如何使用冲洗函数tcflush冲洗输入或输出队列,以及如何使用tcsetattr函数通知系统在输出队列为空时改变一个终端的属性和改变终端属性时丢弃输入队列中所有内容(如在规范模式和非规范模式之间进行切换时,以免新的模式对以前输入的字符进行解释)。

大多UNIX系统在一个称为终端行规程(terminal line discipline)的模块中进行全部的规范处理。此模块位于内核通用读、写函数和实际设备驱动程序之间:

由于将规范处理分离为单独的模块,所有的终端驱动程序都能一致地支持规范处理。

所有可以检测和更改的终端设备特性都包含在termios结构中,该结构定义在头文件termios.h中:

粗略地说,输入标志通过终端设备驱动程序控制字符的输入(如剥除输入字节的第8位、开启输入奇偶校验);输出标志控制驱动程序输出(如将换行符转换为CR/LF);控制标志影响RS-232串行线(如忽略调制解调器状态线、每个字符1个或是2个停止位(停止位用于分隔字节,让接收者知道正在传输的字节已结束));本地标志影响驱动程序和用户之间的接口(如打开或关闭回显、可视地擦除字符、使终端产生的信号起作用、后台作业有向终端输出时使其进入阻塞状态(即向进程发送SIGTTOUT信号))。

类型tcflag_t的长度足以保存每个标志值,它常被定义为unsigned int或unsigned long。termios.c_cc数组中包含了所有可以更改的特殊字符,NCCS是该数组中元素的数量,其值一般在15~20之间(大多UNIX实现支持的特殊字符比POSIX.1定义的11个要多)。cc_t类型的长度足以保存每个特殊字符,一般被定义为unsigned char。

POSIX标准之前的System V版本有一个名为termio.h的头文件和一个名为termio的数据结构,为了与先前版本有区别,POSIX.1在这些名字后面加了一个s。

以下是可以更改以影响终端设备特性的终端标志,虽然SUS定义了所有平台所用的公共子集,但实现都有自己的扩充部分,这些扩充部分大多来自各系统之间的历史差异。




以下是SUS定义的对终端设备进行操作的函数,它们都是POSIX基本规范的组成部分:

对于终端设备,SUS没有使用经典的ioctl函数,而是使用了上图中13个函数,理由是:ioctl函数的最后一个参数的数据类型随执行动作的不同而改变,不能对参数进行类型检查。

终端设备有大量选项可供使用,对于某个设备(假设是终端或调制解调器或打印机或其他任何设备),决定其需要哪些选项是一种挑战。

上图中各函数之间的关系:

上图中滤特率应该是波特率。

以下是在输入时要特殊处理的字符:


POSIX.1的11个特殊字符中,不能更改的两个字符是换行符(\n)和回车符(\r)。有些实现中,STOP和START字符也不能更改。为了更改特殊字符,只需修改termios结构中的c_cc数组的相应项,该数组中的元素都用上图中的名字作为下标进行引用。

POSIX.1允许禁止某些字符的使用,将c_cc数组中某项设为_POSIX_VDISABLE时,表示禁止使用相应特殊字符。

早期SUS版本中,支持_POSIX_VDISABLE是可选项,现在是必选项。Linux 3.2.0和Solaris 10将_POSIX_VDISABLE定义为0,FreeBSD 8.0和Mac OS X 10.6.8将其定义为0xff。某些早期UNIX系统中,若与某一特性相应的特殊输入字符是0,则禁止使用此特性。

以下程序禁用中断字符,并将文件结束符设为Ctrl+B:

#include <termios.h>
#include <unistd.h>
#include <stdlib.h>
#include <stdio.h>int main(void) {struct termios term;long vdisable;if (isatty(STDIN_FILENO) == 0) {printf("standard input is not a terminal device\n");exit(1);}if ((vdisable = fpathconf(STDIN_FILENO, _PC_VDISABLE)) < 0) {printf("fpathconf error or _POSIX_VDISABLE not in effect\n");exit(1);}if (tcgetattr(STDIN_FILENO, &term) < 0) {    /* fetch tty state */printf("tcgetattr error\n");exit(1);}term.c_cc[VINTR] = vdisable;    /* disable INTR character */term.c_cc[VEOF] = 2;    /* EOF is Control-B */if (tcsetattr(STDIN_FILENO, TCSAFLUSH, &term) < 0) {printf("tcsetattr error\n");exit(1);}exit(0);
}

对于以上程序要说明以下几点:
1.仅当标准输入是终端设备才修改终端特殊字符,调用isatty对此进行检测。
2.调用fpathconf获取_POSIX_VDISABLE值。
3.函数tcgetattr从内核获取termios结构。修改此结构后,调用tcsetattr设置属性,这样只有我们希望修改的属性被更改了,其他属性保持不变。
4.禁用中断键和忽略中断信号是不同的。以上程序只是禁用终端驱动程序产生SIGINT信号的特殊字符,我们仍可以使用kill函数将该信号发送至进程。

以下详细说明各个特殊字符,我们称这些字符为特殊输入字符,但其中的STOP(Ctrl+S)和START(Ctrl+Q)字符在输出时也要特殊处理,以下字符中的大多数在被驱动程序识别并进行特殊处理后会被丢弃,并不将它们返回给执行读终端操作的进程,只有换行符(NL、EOL、EOL2)和回车符(CR)会返回给读进程:
1.CR:回车符。不能更改此字符。以规范模式进行输入时识别此字符。在已设置ICANON(规范模式)和ICRNL(将CR映射为NL)但未设置IGNCR(忽略CR)时,CR字符会被转换成NL字符。此字符会返回给读进程,进程收到时可能它已经被转换成了NL。
2.DISCARD:丢弃符。在扩充模式(IEXTEN)下进行输入时识别此字符。在输入另一个DISCARD字符前或在丢弃条件被清除前,此字符使后续输出都被丢弃。此字符在处理后被丢弃,不传送给读进程。
3.DSUSP:延迟挂起作业控制字符。在扩充模式(IEXTEN)下,若支持作业控制,且已设置ISIG标志,则输入时识别此字符。与SUSP字符相同之处是:延迟挂起字符也产生SIGTSTP信号,该信号被发送给前台进程组中的所有进程。但信号产生的时间不是在键入延迟挂起作业控制字符时,而是在某个进程从控制终端读到此字符时才产生。此字符在处理后被丢弃,不传送给读进程。
4.EOF:文件结束符。以规范模式进行输入时识别此字符。键入此字符时,等待被读的所有字节都被立即传送给读进程。在行首输入EOF是向程序指示文件结束的正常方式。此字符在规范模式下处理后即被丢弃,不传送给读进程。
5.EOL:附加的行定界符,与NL作用相同。以规范模式进行输入时识别此字符,并将此字符返回给读进程。此字符不常用。
6.EOL2:另一个行定界符,与NL作用相同。对此字符处理方式与EOL字符相同。
7.ERASE:向前擦除字符(退格)。以规范模式输入时识别此字符。它擦除行中的前一个字符,但不会超越行首字符擦除上一行中字符。此字符在规范模式下处理后被丢弃,不传送给读进程。
8.ERASE2:ERASE字符的替换,对此字符的处理与ERASE字符完全相同。
9.INTR:中断字符。若已设置ISIG标志,则在输入中识别此字符。它产生SIGINT信号,该信号被送至前台进程组中的所有进程。此字符在处理后被丢弃,不传送给读进程。
10.KILL:杀死字符(杀死一词在这里又一次被误用,此字符应被称为行擦除符(另一次误用是kill函数,它用来将某一信号发送给进程))。以规范模式输入时识别此字符,它擦除一整行,并在处理后被丢弃,不传送给读进程。
11.LNEXT:下一个字符的字面值。以扩充方式输入时识别此字符,它使下一个字符的任何特殊含义都被忽略,这对所有特殊字符都起作用,使用这一字符可向程序键入任何字符。该字符在处理后被丢弃,但下一个字符被传送给读进程。
12.NL:换行符,也被称为行定界符,不能更改此字符。以规范模式输入时识别此字符。此字符返回给读进程。
13.QUIT:退出字符。若已设置ISIG标志,则在输入中识别此字符。它产生SIGQUIT信号,该信号被送至前台进程组中的所有进程。此字符在处理后被丢弃,不传送给读进程。INTR(SIGINT)和QUIT(SIGQUIT)的区别是,默认,QUIT字符(SIGQUIT)不仅终止进程,而且还产生一个core文件。
14.REPRINT:再打印字符。以扩充规范模式(设置了IEXTEN和ICANON标志)输入时识别此字符。它使所有未读的输入被输出(再回显)。此字符在处理后被丢弃,不传送给读进程。
15.START:启动字符。若已设置IXON标志,则在输入中识别此字符,且若已设置IXOFF标志,则自动产生此字符作为输出。已设置IXON时,接收到的START字符使停止的输出(由以前输入的STOP字符造成)重新启动,此字符在处理后被丢弃,不传送给读进程;设置IXOFF时,若新的输入不会使输入缓冲区溢出,则终端驱动程序会自动产生一个START字符来恢复以前被停止的输入。
16.STATUS:BSD的状态请求字符。以扩充规范模式(设置了IEXTEN和ICANON标志)进行输入时识别此字符。它产生SIGINFO信号,该信号被送至前台进程组中的所有进程。如果没有设置NOKERNINFO标志,则将有关前台进程组的状态信息也显示在终端上。此字符在处理后被丢弃,不传送给读进程。
17.STOP:停止字符。若已设置IXON标志,则在输入中识别此字符,且若已设置IXOFF标志,则自动产生此字符作为输出。已设置IXON时,收到STOP字符则停止输出,此字符在处理后被丢弃,不传送给读进程,而后再输入一个START字符后,被停止的输出重新启动;已设置IXOFF时,终端驱动程序会在输入缓冲要溢出前自动产生STOP字符以防止输入缓冲区溢出。
18.SUSP:挂起作业控制字符。若支持作业控制且已设置ISIG标志,则在输入中识别此字符。它产生SIGTSTP信号,该信号被送至前台进程组的所有进程。此字符在处理后被丢弃,不传送给读进程。
19.WERASE:字擦除字符。以扩充规范模式进行输入时识别此字符。它使前一个字被擦除。首先,它向前跳过任意一个空白字符(空格或制表符),然后向前跳过一个标记,使光标位于前一个标记的第一个字符位置上。通常一个标记碰到空白字符终止,可通过设置ALTWERASE标志改变此行为,ALTWERASE标志使前一个记号在碰到第一个非字母、非数字字符时终止。此字符在处理后被丢弃,不传送给读进程。

需要为终端设备定义的另一个字符是BREAK字符,BREAK字符实际不是一个字符,而是在异步串行数据传送时发生的一个条件,根据串行接口的不同,通知设备驱动程序发生BREAK条件的方式也不同。

大多早期串行终端上有一个BREAK键,用其可产生BREAK条件,这就是为什么很多人认为BREAK是一个字符。PC上的BREAK键(PB或PauseBreak键)可能有其他用途,如Ctrl+BREAK可中断Windows cmd。

对于异步串行数据传送,BREAK是一个值为0的bit流,其持续时间长于发送一个字节的时间,整个0值序列被视为一个BREAK。

使用tcgetattr和tcsetattr函数获取和设置termios结构,这样就可以检测和修改各种终端选项标志和特殊字符:

这两个函数都有参数termptr,它是一个指向termios结构的指针,该指针参数用来获取和设置终端属性。这两个函数只对终端设备进行操作,如果fd参数引用的不是终端设备,则返回-1,errno设为ENOTTY。

tcsetattr函数的参数opt用来指定什么时候新终端属性起作用,可选值为:
1.TCSANOW:更改立即发生。
2.TCSADRAIN:发送了所有输出后才发生,若更改输出相关参数应使用此选项。
3.TCSAFLUSH:发送了所有输出后才发生,且更改发生时所有未读的输入数据都被冲洗(丢弃)。

tcsetattr函数设置属性时,如果任意一个属性设置成功,它就返回0(成功),即使有其他的属性设置失败也返回成功,因此,在函数成功返回后,我们需要调用tcgetattr获取当前终端属性看是否全部设置成功。

终端被打开时,属性视具体实现而定,一些系统会将终端属性初始化为具体实现定义的值,一些会保留最后一次使用终端时的属性。以O_TTY_INIT标志打开终端时,会将终端的属性值设为遵循SUS标准的值。

终端选项标志都用一位或多位表示。屏蔽字定义了多个位的集合,这个集合可以定义一组值,如屏蔽字CSIZE是表示字符长度的屏蔽字,我们可以用此屏蔽字设置和获取字符长度:

#include <termios.h>
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>int main(void) {struct termios term;if (tcgetattr(STDIN_FILENO, &term) < 0) {printf("tcgetattr error\n");exit(1);}switch (term.c_cflag & CSIZE) {case CS5:printf("5 bits/byte\n");break;case CS6:printf("6 bits/byte\n");break;case CS7:printf("7 bits/byte\n");break;case CS8:printf("8 bits/byte\n");break;default:printf("unknown bits/byte\n");}term.c_cflag &= ~CSIZE;    /* zero out the bits */term.c_cflag |= CS8;    /* set 8 bits/byte */if (tcsetattr(STDIN_FILENO, TCSANOW, &term) < 0) {printf("tcsetattr error\n");exit(1);}exit(0);
}

以下是各终端选项标志说明,标志后括号中的是该标志在termios结构的哪个成员中:
1.ALTWERASE(c_lflag):设置此标志后,若输入WERASE字符,则使用一个替换的字擦除算法,不是向前移动到前一个空白字符,而是向前移动到第一个非字母、非数字字符为止。
2.BRKINT(c_iflag):如果设置了此标志,同时没有设置IGNBRK标志,则接收到BREAK字符时,冲洗输入输出队列,并产生一个SIGINT信号。如果终端设备是一个控制终端,则SIGINT信号会发给前台进程组。如果IGNBRK和BRKINT都没设置,但设置了PARMRK,则BREAK被读作一个3字节序列\377、\0、\0(\377是8进制字符转义,8进制377转换为十进制后是255,即\377表示ASCII码为255的字符,标准ASCII码有128个,共7位,扩展ASCII码有256个,共8位);如果PARMRK也没设置,则BREAK被读作\0。
3.BSDLY(c_oflag):退格延迟屏蔽字。这个屏蔽字对应的值是BS0或BS1。
4.CBAUDEXT(c_cflag):扩充的波特率。用于允许大于B38400的波特率。
5.CCAR_OFLOW(c_cflag):使用RS-232调制解调器的DCD(Data-Carrier-Detect,数据载波检测)信号来开启输出的硬件流控制。这与早期的MDMBUF标志相同。
6.CCTS_OFLOW(c_cflag):使用RS-232的CTC(Clear-To-Send,清除发送)信号打开输出的硬件流控制。
7.CDSR_OFLOW(c_cflag):按RS-232的DTR(Data-Set-Ready,数据准备就绪)信号进行输入的流控制。
8.CDTR_IFLOW(c_cflag):按RS-232的DTR(Data-Terminal-Ready,数据终端就绪)信号进行输入的流控制。
9.CIBAUDEXT(c_cflag):扩充的输入波特率。用于允许大于B28400的输入波特率。
10.CIGNORE(c_cflag):忽略控制标志。
11.CLOCAL(c_cflag):若设置,则忽略调制解调器状态线,这通常意味着该设备是直接连接的。如未设置此标志,则打开一个终端设备常常会遭遇阻塞,直到调制解调器回应呼叫并建立连接。
12.CMSPAR(c_oflag):标记或置空奇偶校验。如设置了PARODD,则奇偶校验位总是1,否则总是0。
13.CRDLY(c_oflag):回车延迟屏蔽字。此屏蔽字对应的值为CR0、CR1、CR2、CR3。
14.CREAD(c_cflag):若设置,启用接收从而可以接收字符。
15.CRTSCTS(c_cfalg):行为依赖于平台,Solaris上设置此标志时,允许带外硬件流控制;在本书另外三个平台上,既允许带内硬件流控制,又允许带外硬件流控(等价于CCTS_OFLOW|CRTS_IFLOW)。
16.CRTS_IFLOW(c_cflag):输入的RTS(Request-To-Send,请求发送)流控制。
17.CRTSXOFF(c_cflag):若设置,允许带内硬件流控制,RS-232的RTS信号的状态控制着流控制。
18.CSIZE(c_cflag):屏蔽字,它指定发送和接收的每个字节的位数,此长度不包括可能有的奇偶校验位。此屏蔽字对应的值为CS5、CS6、CS7、CS8,分别表示每个字节含5位、6位、7位、8位。
19.CSTOPB(c_cflag):若设置,使用两个停止位,否则使用一个停止位。
20.ECHO(c_lflag):若设置,则将输入字符回显到终端设备。规范模式和非规范模式下都可以回显输入字符。
21.ECHOCTL(c_lflag):若设置且同时设置了ECHO,则除ASCII TAB、ASCII NL、STOP、START字符外,其他ASCII控制字符(ASCII字符集中0到八进制37对应的字符)都被回显为^X(X是相应控制字符的ASCII码加上八进制100所构成的字符,如输入ASCII Ctrl+A,即八进制1,会被回显为^A,A的ASCII码为八进制101);若未设置此标志,则ASCII控制字符按其原样回显。此标志在规范模式和非规范模式下都起作用。有些系统在回显EOF时不遵循以上规则,因为EOF的典型值是Ctrl+D,而Ctrl+D是ASCII EOT字符,它可能使某些终端挂断。
22.ECHOE(c_lflag):若设置,且ICANON和ECHOKE(如果支持ECHOKE标志)没有设置,则ERASE字符从显示中擦除当前行中的最后一个字符,这通常是在终端驱动程序中写一个3字符序列实现的,序列为退格、空格、退格。若设置了WERASE字符,则设置此字符时会用一个或多个相同的3字节序列擦除前一个字。
23.ECHOKE(c_cflag):若设置,且也设置了ICANON,则回显KILL字符的方式是擦除行中的每个字符,擦除每个字符的方式由ECHOE和ECHOPRT标志共同决定。
24.ECHONL(c_lflag):若设置,且也设置了ICANON,即使没有设置ECHO,也回显NL字符。
25.ECHOPRT(c_lflag):若设置,且也设置了ICANON和ECHO,则ERASE和WERASE(若支持WERASE字符)字符使所有要被擦除的字符在擦除后打印出来,这在硬拷贝终端上很有用,我们可以确切看到哪个字符被删除。
26.EXTPROC(c_lflag):若设置,字符处理可在操作系统外执行,如果串行通信外设卡能通过执行某种行规程减轻主机处理器负载,就可以这样设置。使用伪终端时也可以设置此标志。
27.FFDLY(c_oflag):换页延迟屏蔽字,此屏蔽字对应的值为FF0、FF1。
28.FLUSHO(c_lflag):若设置,则冲洗输出。键入DISCARD字符时设置此标志,键入另一个DISCARD字符时清除此标志。我们也可以通过tcsetattr函数设置或清除此标志。
29.HUPCL(c_cflag):若设置,则当最后一个进程关闭设备时,调制解调器控制线将至低电平(即断开与调制解调器的连接)。
30.ICANON(c_lflag):若设置,则按规范模式工作,以下字符会起作用:EOF、EOL、EOL2、ERASE、KILL、REPRINT、STATUS、WERASE。输入字符被装配成行。
31.ICRNL(c_iflag):若设置,且未设置IGNCR,则将接收到的CR字符转换成NL字符。
32.IEXTEN(c_lflag):若设置,则识别由实现定义的扩展字符。
33.IGNBRK(c_iflag):若设置,忽略输入中的BREAK条件。BREAK条件是产生SIGINT信号还是作为数据读取,见BRKINT。
34.IGNCR(c_iflag):若设置,则忽略接收到的CR字符,若未设置,且设置了ICRNL标志,则可能将接收到的CR字符转换为NL字符。
35.IGNPAR(c_iflag):若设置,忽略带有结构出错(非BREAK)或奇偶出错的输入字节。
36.IMAXBEL(c_iflag):当输入队列满时响铃。
37.INLCR(c_iflag):若设置,将接收到的NL字符转换成CR字符。
38.INPCK(c_iflag):若设置,使输入奇偶校验起作用。奇偶位的“产生和检测”和“输入奇偶校验”是不同的,奇偶位的产生和检测由PARENB标志控制,设置后通常会使串行接口的设备驱动程序对输出字符产生奇偶位且对输入字符验证奇偶性;PARODD标志决定奇偶性是奇还是偶。如果一个奇偶性错误的字符到来,才检查INPCK标志的状态,若设置,则检查IGNPAR标志(是否忽略奇偶出错的输入字节),如果不忽略奇偶出错的输入字节,则检查PARMRK标志来决定应该向读进程传送哪些字符。
39.ISIG(c_flag):若设置,则判别输入字符是否是要产生终端信号的特殊字符(INTR、QUIT、SUSP、DSUSP),若是,则产生相应信号。
40.ISTRIP(c_iflag):若设置,有效输入字节被剥除为7位,未设置时,处理全部8位。
41.IUCLC(c_iflag):将输入的大写字符转换成小写字符。
42.IUTF8(c_iflag):使字符擦除对UTF-8多字节字符生效。
43.IXANY(c_iflag):任何字符都能重启输出。
44.IXOFF(c_iflag):若设置,当终端驱动程序发现输入队列将要填满时,输出一个STOP字符,此字符应由发送数据的设备识别,并使该设备停止,之后输入队列中的字符处理完毕后,终端驱动程序将输出一个START字符,使该设备恢复数据发送。
45.IXON(c_iflag):若设置,当终端驱动程序接收到STOP字符时,输出停止,在输出停止时,下一个START字符恢复输出。若未设置,STOP和START作为一般字符被进程读取。
46.MDMBUF(c_cflag):按调制解调器的载波标志进行输出流控制。是CCAR_OFLOW的曾用名。
47.NLDLY(c_oflag):换行延迟屏蔽字,该屏蔽字对应的值为NL0、NL1。
48.NOFLSH(c_lflag):按系统默认,当终端驱动程序产生SIGINT、SIGQUIT信号时,输入和输出队列都被冲洗;当它产生SIGSUSP信号时,输入队列被冲洗。若设置此标志,则产生这些信号时,不对输入输出队列进行常规冲洗。
49.NOKERNINFO(c_lflag):若设置,阻止STATUS字符打印前台进程组的信息。但无论是否设置此标志,STATUS字符都会使SIGINFO信号发送到前台进程组。
50.OCRNL(c_oflag):若设置,将输出的CR字符转换成NL字符。
51.OFDEL(c_oflag):若设置,则输出的填充字符是ASCII DEL,否则是ASCII NUL。
52.OFILL(c_oflag):若设置,则传递填充字符(填充字符是什么取决于OFDEL)以实现延迟来取代时间延迟。6个延迟标志字分别为BSDLY、CRDLY、FFDLY、NLDLY、TABDLY、VTDLY。
53.OLCUC(c_oflag):若设置,将小写字符转换为大写字符。
54.ONLCR(c_oflag):若设置,输出的NL字符会转换为CR-NL字符。
55.ONLRET(c_oflag):若设置,则输出的NL字符执行回车功能。
56.ONOCR(c_oflag):若设置,在0列不输出CR。
57.ONOEOT(c_oflag):若设置,在输出中丢弃EOT(^D)字符。在某些将Ctrl+D解释为挂断的终端上,此标志是必需的。在POSIX系统中,Ctrl+D不会产生EOT,而是立即交付当前的行缓冲。
58.OPOST(c_oflag):若设置,使用实现定义的输出处理。
59.OXTABS(c_oflag):若设置,则制表符在输出中被扩展为空格,其效果与TABDLY被设为XTABS或TAB3所产生的效果相同。
69.PARENB(c_cflag):若设置,对输出字符产生奇偶校验位,对输入字符执行奇偶校验。若设置PARODD,则奇偶校验是奇校验,否则是偶校验。另外影响奇偶校验的标志:INPCK、IGNPAR、PARMRK。
70.PAREXT(c_cflag):标记或置空奇偶校验位。若设置了PAREXT,且设置了PARODD,则奇偶位总是1(即标记奇偶位);若设置PAREXT,且没设置PAROOD,则奇偶位总是0(即置空奇偶位)。
71.PARMRK(c_iflag):若设置,且未设置IGNPAR,则带有结构出错的非BREAK字符或奇偶出错的字节被进程读作一个3字符序列\377、\0、X(X指接收到的出错字节)。若设置,且未设置ISTRIP,则一个有效的\377字节被传送给进程时为\377、\377。若未设置IGNPAR,且未设置PARMRK,则带结构出错的非BREAK字符或奇偶出错的字节被读作字符\0。
72.PARODD(c_cflag):若设置,则输出和输入字符的奇偶性都是奇,否则为偶。PARENB标志控制奇偶性的产生和检测。
73.PENDIN(c_lflag):若设置,在下个字符输入时,尚未读的任何输入都由系统重新打印。此标志与键入REPRINT字符时的作用相似。
74.TABDLY(c_oflag):水平制表符延迟屏蔽字,此屏蔽字对应的值为TAB0、TAB1、TAB2、TAB3。XTABS的值等于TAB3,此值使系统将制表符扩展成空格。系统假定制表符长度占8个空格,且不能被更改。
75.TOSTOP(c_lflag):若设置,且支持作业控制,则将信号SIGTTOU送到试图写控制终端的后台进程的进程组。默认,SIGTTOU暂停进程组中所有进程,如果写控制终端的后台进程忽略或阻塞此信号,则终端驱动程序不产生此信号。
76.VTDLY(c_oflag):垂直制表延迟屏蔽字,此屏蔽字对应的值是VT0、VT1。
77.XCASE(c_lflag):若设置,且设置了ICANON,则终端被假定为只支持大写字符,而输入会被转换为小写字符,如想输入一个大写字符,需要在其前面加一个反斜杠。当系统输出大写字符时,也要在前面加一个反斜杠。此标志已被弃用,因为只支持大写的终端几乎不存在了。

以上所有标志都可被检查和更改:在程序中使用tcgetattr和tcsetattr函数;在命令行使用stty命令。stty命令就是图18-7中前6个函数的接口。stty命令的-a选项显示所有终端标志:

上图中,标志前有-的表示该选项被禁用。输出的第一行显示当前终端窗口的行数和列数。cchars中显示特殊字符如何输入,如erase2,需要输入Ctrl+?,Shift+/输入问号,因此erase2输入需要Ctrl+shift+/。

stty命令获取的是它的标准输入的终端选项标志,较早的实现使用标准输出,但POSIX.1要求使用标准输入,如希望了解ttyla终端的设置,可输入:

stty -a < /dev/ttyla

波特率指位/秒,虽然大多终端设备对输入和输出使用同一波特率,但只要硬件允许,可将它们设为两个值。使用以下函数设置和获取输入输出的波特率:

两个cfget函数的返回值和两个cfset函数的speed参数都是以下常量之一:B50、B75、B110、B134、B150、B200、B300、B600、B1200、B1800、B2400、B4800、B9600、B19200、B38400。常量B0表示挂断。调用tcsetattr时,如将输出波特率指定为B0,则调制解调器的控制线不再有效。

大多系统定义了另外的波特率值。

输入输出波特率是存储在设备的termios结构中的,在调用两个cfget函数前,要先调用tcgetattr获得设备的termios结构;在调用两个cfset函数后,应调用tcsetattr函数,即使设置的波特率可能出错,在调用tcsetattr也不会发现这个错误。

以上4个波特率相关函数使应用不用考虑具体实现在termios结构中表示波特率的不同方法。Linux和BSD派生的平台趋向于存储波特率的数值,System V派生的平台趋向于以位屏蔽方式编码波特率。从cfget函数得到的波特率和向cfset函数传送的速度值都没有被改变,与它们存储在termios结构中的表示形式一样。

以下函数提供终端设备的行控制能力,它们都要求fd长度引用一个终端设备,否则出错返回-1,并将errno设为ENOTTY:

tcdrain函数等待所有输出都被传递。

tcflow函数可进行输入输出流控制,其action参数取值如下:
1.TCOOFF:输出被挂起。
2.TCOON:重新启动被挂起的输出。
3.TCIOFF:系统发送一个STOP字符,这将使终端设备停止发送数据。
4.TCION:系统发送一个START字符,这将使终端设备恢复发送数据。

tcflush函数丢弃输入缓冲区(其中的数据是终端驱动程序已接收到,但用户程序尚未读取的)或输出缓冲区(其中的数据是用户程序已经写入,但尚未被传递的),queue参数可选值:
1.TCIFLUSH:冲洗输入队列。
2.TCOFLUSH:冲洗输出队列。
3.TCIOFLUSH:冲洗输入和输出队列。

tcsendbreak函数在一个指定的时间区间内发送连续的0值bit流,若duration参数为0,则发送0.25~0.5秒。POSIX.1说明,若参数duration非0,则传递时间依赖于实现。

历史上,大多UNIX系统中,控制终端的名字一直是/dev/tty,POSIX.1提供了一个运行时函数来获取控制终端名:

若ptr参数非空,则被认为是一个长度至少为L_ctermid的字节数组,进程控制终端名会存在此数组中。常量L_ctermid定义在头文件stdio.h中。若ptr参数为空,函数会分配空间(通常是静态的)来存放控制终端名。这两种情况下,数组的初始地址都作为函数值返回。

由于大多UNIX系统都使用/dev/tty作为控制终端名,因此ctermid函数的主要作用是改善向其他操作系统的可移植性。

POSIX.1的ctermid函数的一个实现:

#include <stdio.h>
#include <string.h>static char ctermid_name[L_ctermid];char *ctermid(char *str) {if (str == NULL) {str = ctermid_name;}return strcpy(str, "/dev/tty");    /* strcpy() returns str */
}

由于我们无法确定调用者缓冲区的大小,所以就不能防止缓冲区溢出。

isatty函数判断文件描述符引用的是不是终端设备;ttyname函数返回该文件描述符上打开的终端设备路径名:

POSIX.1的isatty函数可通过判断调用终端专用函数的返回值来实现:

#include <termios.h>int isatty(int fd) {struct termios ts;return tcgetattr(fd, &ts) != -1;    /* true if no error (is a tty) */
}

ttyname函数的实现,它要搜索所有设备表项,寻找匹配项:

#include <sys/stat.h>
#include <dirent.h>
#include <limits.h>
#include <string.h>
#include <termios.h>
#include <unistd.h>
#include <stdlib.h>struct devdir {struct devdir *d_next;char *d_name;
};static struct devdir *head;
static struct devdir *tail;
static char pathname[_POSIX_PATH_MAX + 1];static void add(char *dirname) {struct devdir *ddp;int len;len = strlen(dirname);/* * Skip ., .., and /dev/fd.*/if ((dirname[len - 1] == '.') && (dirname[len - 2] == '/' || (dirname[len - 2] == '.' && dirname[len - 3] == '/'))) {return;       }if (strcmp(dirname, "/dev/fd") == 0) {return;}if ((ddp = malloc(sizeof(struct devdir))) == NULL) {return;}if ((ddp->d_name = strdup(dirname)) == NULL) {free(ddp);return;}ddp->d_next = NULL;if (tail == NULL) {head = ddp;tail = ddp;} else {tail->d_next = ddp;tail = ddp;}
}static void cleanup(void) {struct devdir *ddp, *nddp;ddp = head;while (ddp != NULL) {nddp = ddp->d_next;free(ddp->d_name);free(ddp);ddp = nddp;}head = NULL;tail = NULL;
}static char *searchdir(char *dirname, struct stat *fdstatp) {struct stat devstat;DIR *dp;int devlen;struct dirent *dirp;strcpy(pathname, dirname);if ((dp = opendir(dirname)) == NULL) {return NULL;}strcat(pathname, "/");devlen = strlen(pathname);while ((dirp = readdir(dp)) != NULL) {strncpy(pathname + devlen, dirp->d_name, _POSIX_PATH_MAX - devlen);/** Skip aliases.*/if (strcmp(pathname, "/dev/stdin") == 0|| strcmp(pathname, "/dev/stdout") == 0|| strcmp(pathname, "/dev/stderr") == 0) {continue;    }if (stat(pathname, &devstat) < 0) {continue;}if (S_ISDIR(devstat.st_mode)) {add(pathname);continue;}if (devstat.st_ino == fdstatp->st_ino&& devstat.st_dev == fdstatp->st_dev) {    /* found a match */closedir(dp);return pathname;}}closedir(dp);return NULL;
}char *ttynme(int fd) {struct stat fdstat;struct devdir *ddp;char *rval;if (isatty(fd) == 0) {return NULL;}if (fstat(fd, &fdstat) < 0) {return NULL;}if (S_ISCHR(fdstat.st_mode) == 0) {return NULL;}rval = searchdir("/dev", &fdstat);if (rval == NULL) {for (ddp = head; ddp != NULL; ddp = ddp->d_next) {if ((rval = searchdir(ddp->d_name, &fdstat)) != NULL) {break;}}}cleanup();return rval;
}

上例代码中,读/dev目录,寻找具有相同设备号和i节点编号的文件。每个文件系统都有唯一的一个设备号(stat.st_dev),文件系统中每个目录项都有一个唯一的i节点编号(stat.st_ino)。以上我们假定在找到一个匹配的设备号和匹配的i节点号时,就能找到所希望的目录项,在UNIX系统中,匹配的设备号和i节点编号是唯一的,可以这么做,如果不在UNIX系统中,我们还可以把stat.st_rdev(终端设备的主设备号和次设备号)、是否是字符特殊文件等加入匹配条件。

上例代码中,我们跳过了可能会产生不正确结果的目录:当前目录、/dev/fd,同时也跳过了一些别名:/dev/stdin、/dev/stdout、/dev/stderr,因为它们是/dev/fd目录中的文件的符号链接。

规范模式下,发一个读请求时,以下几个条件会造成读返回:
1.所请求的字节数已读到,无需读一个完整的行就返回,下次读会从前一次读的停止处开始。
2.读到一个行定界符时,读返回。规范模式下,NL、EOL、EOL2、EOF被解释为行结束符,另外,如果定义了ICRNL,但未设置IGNCR,则CR字符作用与NL字符相同,也终止一行。以上5个字符中,只有EOF在终端驱动程序对其处理后被丢弃,其余都会作为其所处行的最后一个字符返回给调用者。
3.如果捕捉到信号,且函数不自动重启,则读返回。

以下getpass函数作用是读入用户在终端上键入的口令,此函数由login和crypt(对密码加密)程序调用,为读取口令,该函数关闭回显,且使终端以规范模式工作,以下是UNIX系统中的一个典型实现:

#include <signal.h>
#include <stdio.h>
#include <termios.h>#define MAX_PASS_LEN 8    /* max #chars for user to enter */char *getpass(const char *prompt) {static char buf[MAX_PASS_LEN + 1];    /* null byte at end */char *ptr;sigset_t sig, osig;struct termios ts, ots;FILE *fp;int c;if ((fp = fopen(ctermid(NULL), "r+")) == NULL) {return NULL;}setbuf(fp, NULL);    // 对控制终端IO时无缓冲sigemptyset(&sig);sigaddset(&sig, SIGINT);    /* block SIGINT */sigaddset(&sig, SIGTSTP);    /* BLOCK sigtstp */sigprocmask(SIG_BLOCK, &sig, &osig);    /* and save mask */tcgetattr(fileno(fp), &ts);    /* save tty state */ots = ts;    /* structure copy */ts.c_lflag &= ~(ECHO | ECHOE | ECHOK | ECHONL);tcsetattr(fileno(fp), TCSAFLUSH, &ts);fputs(prompt, fp);ptr = buf;while ((c = getc(fp)) != EOF && c != '\n') {if (ptr < &buf[MAX_PASS_LEN]) {*ptr++ = c;}}*ptr = 0;    /* null terminate */putc('\n', fp);    /* we echo a newline */tcsetattr(fileno(fp), TCSAFLUSH, &ots);    /* restore TTY state */sigprocmask(SIG_SETMASK, &osig, NULL);    /* restore mask */fclose(fp);    /* done with /dev/tty */return buf;
}

关于上例代码:
1.调用ctermid获取控制终端名从而打开它,而非直接将/dev/tty写在程序中。
2.上例只是读写控制终端,不能以读写模式打开控制终端时出错返回。在GNU C函数库版本中,如果不能以读写模式打开控制终端,则getpass函数读标准输入、写标准错误;Solaris版本中,如果不能打开控制终端,则getpass函数失败。
3.阻塞信号SIGINT和SIGTSTP,如果不这样,在输入INTR字符时会使程序异常终止,并使终端仍处于禁止回显状态;输入SUSP字符(Ctrl+Z)时程序会停止,并且在禁止回显状态下返回shell。上例代码中,在这两个信号被忽略期间发生的这两个信号都会被忽略。另一种处理这两个信号的方式是捕捉信号,捕捉到信号后,恢复终端状态和信号动作,之后用kill函数再次向自己发送此信号。没有一个版本的getpass函数捕捉、忽略或阻塞SIGQUIT,因此输入QUIT字符会使程序异常终止,且很可能(如果在禁止回显后,开启回显前输入QUIT字符)使终端保持在禁止回显状态。
4.某些shell,如Korn shell,在以交互方式每次读输入时都会重新打开回显。这些shell提供命令行编辑功能,也因此每次我们输入一条交互命令时都会操纵终端的状态。因此,当我们在这种shell中运行以上程序,然后用QUIT字符终止进程,它可能为我们重新开启回显。其他不提供这种命令行编辑功能的shell,如Bourne shell,会终止进程并使终端保持在不回显模式,此时,可使用stty命令使终端恢复回显状态。
5.使用不带缓冲的标准IO读、写控制终端,如果带缓冲,可能有些程序中会读写多次,如果没有调用fflush冲洗输出流,可能上次的输出在下次输出时才输出。也可使用不带缓冲的IO,即使用read函数代替getc函数。
6.只取前8个字符的输入,后面的输入全被忽略。

调用getpass并打印输入的内容,验证ERASE和KILL字符在规范模式下是否正常工作:

#include <signal.h>
#include <stdlib.h>
#include <stdio.h>
#include <termios.h>#define MAX_PASS_LEN 8    /* max #chars for user to enter */char *getpass(const char *prompt) {static char buf[MAX_PASS_LEN + 1];    /* null byte at end */char *ptr;sigset_t sig, osig;struct termios ts, ots;FILE *fp;int c;if ((fp = fopen(ctermid(NULL), "r+")) == NULL) {return NULL;}setbuf(fp, NULL);    // 对控制终端IO时无缓冲sigemptyset(&sig);sigaddset(&sig, SIGINT);    /* block SIGINT */sigaddset(&sig, SIGTSTP);    /* BLOCK sigtstp */sigprocmask(SIG_BLOCK, &sig, &osig);    /* and save mask */tcgetattr(fileno(fp), &ts);    /* save tty state */ots = ts;    /* structure copy */ts.c_lflag &= ~(ECHO | ECHOE | ECHOK | ECHONL);tcsetattr(fileno(fp), TCSAFLUSH, &ts);fputs(prompt, fp);ptr = buf;while ((c = getc(fp)) != EOF && c != '\n') {if (ptr < &buf[MAX_PASS_LEN]) {*ptr++ = c;printf("%c", c);}}*ptr = 0;    /* null terminate */putc('\n', fp);    /* we echo a newline */tcsetattr(fileno(fp), TCSAFLUSH, &ots);    /* restore TTY state */sigprocmask(SIG_SETMASK, &osig, NULL);    /* restore mask */fclose(fp);    /* done with /dev/tty */return buf;
}int main() {char *ptr;if ((ptr = getpass("Enter password:")) == NULL) {printf("getpass error\n");exit(1);}printf("password: %s\n", ptr);/* now use password (probably encrypt it) ... */while (*ptr != 0) {*ptr++ = 0;    /* zero it out when we're done with it */}exit(0);
}

运行以上程序,如输入中带ERASE字符:

abcd[ERASE字符]efghij

则会输出:

abcefghi

如果输入中带KILL字符:

abcd[KILL字符]efghijklm

则会输出:

efghijkl

上例代码中,如果getpass函数读入的是明文口令,为了安全起见,在程序完成后应在内存中清除它。如果该程序会产生core文件(默认每个用户都可读core文件),或某个其他进程能读到该进程的内存空间,则就可能会读到我们输入的明文口令。大多UNIX系统程序会修改这个明文口令,将其转换为一个加密口令(如口令文件的pw_passwd字段就是加密口令)。

通过关闭termios.c_lflag字段的ICANON标志来指定非规范模式,非规范模式中,输入数据不装配成行,不处理以下特殊字符:ERASE、KILL、EOF、NL、EOL、EOL2、CR、REPRINT、STATUS、WERASE。

非规范模式下进行读操作时,读入了指定量的数据后,或超过给定的量时间后,即通知系统返回。其中指定量的数据由termios.c_cc数组中的变量MIN(下标为VMIN)指定,给定量的时间由termios.c_cc数组中的变量TIME(下标为VTIME,单位为十分之一秒)指定。MIN和TIME有以下4种情形:
1.MIN>0、TIME>0:TIME指定一个定时器,在第一个字节被接收时启动,并且在后续每次字节输入时被重启动,在超过该定时器前,若已接到MIN个字节或read调用所要求的的字节数,则read函数返回,如果定时器超时,则read函数返回已接收到的字节(由于定时器是在第一个字节被接收后启动的,因此至少会返回一个字节)。如果在调用read时数据已经可用,则立即返回。
2.MIN>0、TIME=0:read函数在接收到MIN个字节前不返回,这会造成无限期阻塞。
3.MIN=0、TIME>0:调用read时启动一个定时器,接到一个字节或定时器超时时,read调用返回,如果是定时器超时,则read函数返回0。
4.MIN=0、TIME=0:如调用read时无数据可读,立即返回0,如有数据可读,最多返回所要求的字节数。

以上4种情形的总结:

MIN只是表示最小值,如果程序要求的数据量大于MIN,是可能接收到所要求的字节数的。

POSIX.1允许下标VMIN、VTIME的值分别与VEOF、VEOL的相同,Solaris就是这样做的,这提供了与System V的早期版本的兼容性,但这也带来了可移植性问题。从非规范模式转换为规范模式时,必须恢复VEOF和VEOL,可以在要转入非规范模式时将整个termios结构保存起来,以后转回规范模式时恢复它。

以下函数tty_cbreak和ttr_raw分别将终端设为cbreak模式和原始模式,tty_reset函数将终端恢复到调用tty_cbreak或tty_raw之前的工作状态。如果已调用tty_cbreak(tty_raw),那么在调用tty_raw(tty_cbreak)前也要调用tty_reset,这减少了出错时终端处于不可用状态的机会。tty_atexit函数可被登记为退出处理程序,以保证调用exit退出时恢复终端工作模式(main中直接return也会调用,main结尾时没有return或exit也会调用)。tty_termios函数返回一个指向开始时termios结构的指针:

#include <termios.h>
#include <errno.h>static struct termios save_termios;
static int ttysavefd = -1;
static enum {RESET, RAW, CBREAK} ttystate = RESET;int tty_cbreak(int fd) {    /* put terminal into a cbreak mode */int err;struct termios buf;if (ttystate != RESET) {errno = EINVAL;return -1;}if (tcgetattr(fd, &buf) < 0) {return -1;}save_termios = buf;    /* struct copy *//** Echo off, canonical mode off.*/buf.c_lflag &= ~(ECHO | ICANON);/** 1 byte at a time, no timer.*/buf.c_cc[VMIN] = 1;buf.c_cc[VTIME] = 0;if (tcsetattr(fd, TCSAFLUSH, &buf) < 0) {return -1;}/** Verify that the changes stuck. tcsetattr can return 0 on* partial success.*/if (tcgetattr(fd, &buf) < 0) {err = errno;tcsetattr(fd, TCSAFLUSH, &save_termios);errno = err;return -1;}if ((buf.c_lflag & (ECHO | ICANON)) || buf.c_cc[VMIN] != 1 ||buf.c_cc[VTIME] != 0) {/** Only some of the changes were made. Restore the * original settings.*/tcsetattr(fd, TCSAFLUSH, &save_termios);errno = EINVAL;return -1;}ttystate = CBREAK;ttysavefd = fd;return 0;
}int tty_raw(int fd) {    /* put terminal into a raw mode */int err;struct termios buf;if (ttystate != RESET) {errno = EINVAL;return -1;}if (tcgetattr(fd, &buf) < 0) {return -1;}save_termios = buf;    /* struct copy *//** Echo off, canonical mode off, extended input* processing off, signal chars off.*/buf.c_lflag &= ~(ECHO | ICANON | IEXTEN | ISIG);/** No SIGINT on BREAK, CR-to-NL off, intput parity* check off, don't strip 8th bit on input, output* flow control off.*/buf.c_iflag &= ~(BRKINT | ICRNL | INPCK | ISTRIP | IXON);/** Clear size bits, parity checking off.*/buf.c_cflag &= ~(CSIZE | PARENB);/** Set 8 bits/char*/buf.c_cflag |= CS8;/** Output processing off.*/buf.c_oflag &= ~(OPOST);/** 1 byte at a time, no timer.*/buf.c_cc[VMIN] = 1;buf.c_cc[VTIME] = 0;if (tcsetattr(fd, TCSAFLUSH, &buf) < 0) {return -1;}/** Verify that the changes stuck. tcsetattr can return 0 on * partial success.*/if (tcgetattr(fd, &buf) < 0) {err = errno;tcsetattr(fd, TCSAFLUSH, &save_termios);errno = err;return -1;}if ((buf.c_lflag & (ECHO | ICANON | IEXTEN | ISIG)) ||(buf.c_iflag & (BRKINT | ICRNL | INPCK | ISTRIP | IXON)) ||(buf.c_cflag & (CSIZE | PARENB | CS8)) != CS8 ||(buf.c_oflag & OPOST) || buf.c_cc[VMIN] != 1 ||buf.c_cc[VTIME] != 0) {/** Only some of the changes were made. Restore the * original settings.*/tcsetattr(fd, TCSAFLUSH, &save_termios);errno = EINVAL;return -1;}ttystate = RAW;ttysavefd = fd;return 0;
}int tty_reset(int fd) {    /* restore terminal's mode */if (ttystate == RESET) {return 0;}if (tcsetattr(fd, TCSAFLUSH, &save_termios) < 0) {return -1;}ttystate = RESET;return 0;
}void tty_atexit(void) {    /* can be set up by atexit(tty_atexit) */if (ttysavefd >= 0) {tty_reset(ttysavefd);}
}struct termios *tty_termios(void) {    /* let caller see original tty state */return &save_termios;
}

cbreak模式定义:
1.非规范模式,这种模式关闭了对某些输入字符的处理,但没有关闭对信号的处理,用户可以键入一个能够触发终端产生信号的字符,调用者应当捕捉这些信号,否则信号可能终止程序,从而使终端保持在cbreak模式。
2.关闭回显。
3.每次输入一个字节,为此,将MIN设为1,TIME设为0,从而至少有一个字节可用时,read函数才返回。

原始模式定义:
1.非规范模式,同时还关闭了对信号产生字符(ISIG)和扩充输入字符(IEXTEN)的处理、禁用了BRKINT字符(使BREAK字符不再产生信号)。
2.关闭回显。
3.禁止输入中的CR到NL映射(ICRNL)、输入奇偶检测(INPCK)、剥离输入字节的第8位(ISTRIP)、输入流控制(IXON)。
4.8位字符(CS8),且禁用奇偶校验(PARENB)。
5.禁止所有输出处理(OPOST)。
6.每次输入一个字节(MIN=1,TIME=0)。

以下程序测试原始模式和cbreak模式:

#include <stdio.h>
#include <unistd.h>
#include <signal.h>
#include <stdlib.h>static void sig_catch(int signo) {printf("signal caught\n");tty_reset(STDIN_FILENO);exit(0);
}int main(void) {int i;char c;if (signal(SIGINT, sig_catch) == SIG_ERR) {    /* catch signals */printf("signal (SIGINT) error\n");exit(1);}if (signal(SIGQUIT, sig_catch) == SIG_ERR) { printf("signal (SIGQUIT) error\n");exit(1);}if (signal(SIGTERM, sig_catch) == SIG_ERR) {printf("signal (SIGTERM) error\n");exit(1);}if (tty_raw(STDIN_FILENO) < 0) {printf("tty_raw error\n");exit(1);}printf("Enter raw mode characters, terminate with DELETE\n");while ((i = read(STDIN_FILENO, &c, 1)) == 1) {if ((c &= 255) == 48) {    /* 0177 = ASCII DELETE */break;}printf("%o\n", c);}if (tty_reset(STDIN_FILENO) < 0) {printf("tty_reset error\n");exit(1);}if (i <= 0) {printf("read error\n");exit(1);}if (tty_cbreak(STDIN_FILENO) < 0) {printf("tty_cbreak error\n");exit(1);}printf("\nEnter cbreak mode characters, terminate with SIGINT\n");while ((i = read(STDIN_FILENO, &c, 1)) == 1) {c &= 255;printf("%o\n", c);}if (tty_reset(STDIN_FILENO) < 0) {printf("tty_reset error\n");exit(1);}if (i <= 0) {printf("read error\n");}exit(0);
}

运行它:

原始模式中,输入了字符Ctrl+D(04)和功能键F7(在此终端上产生了5个字符:ESC(033)、[(0133)、1(061)、8(070)、~(0176))。由于在原始模式下关闭了输出处理(OPOST),所以每个字符后没有回车符。在cbreak模式下,不对输入特殊字符进行处理(因此没有对Ctrl+A、退格进行处理),但仍对终端产生的信号进行处理。

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

关于以上结构:
1.可用TIOCGWINSZ为参数调用ioctl函数获取此结构当前值。
2.可用TIOCSWINSZ为参数调用ioctl函数将此结构的新值存储到内核,如果新值与内核中的当前值不同,则前台进程组会收到SIGWINCH信号,此信号的系统默认动作是忽略。
3.除了存储此结构当前值和在此值发生改变时产生一个信号外,内核对该结构不进行任何其他操作,对结构中的值进行解释完全是应用程序的工作。

提供以上功能的目的是,当窗口大小发生变化时,应用程序(如vi)能得到通知,应用程序接收此信号后,可以获取窗口大小新值,然后重绘屏幕。

以下程序打印当前窗口大小,然后休眠,每次窗口大小改变时,程序就捕捉到SIGWINCH信号,然后打印新窗口大小,我们必须用一个信号终止此程序:

#include <termios.h>
#include <unistd.h>
#include <signal.h>
#include <stdlib.h>
#include <stdio.h>#ifndef TIOCGWINSZ
#include <sys/ioctl.h>
#endifstatic void pr_winsize(int fd) {struct winsize size;if (ioctl(fd, TIOCGWINSZ, (char *)&size) < 0) {printf("TIOCGWINSZ error\n");exit(1);}printf("%d rows, %d columns\n", size.ws_row, size.ws_col);
}static void sig_winch(int signo) {printf("SIGWINCH received\n");pr_winsize(STDIN_FILENO);
}int main(void) {if (isatty(STDIN_FILENO) == 0) {exit(1);}if (signal(SIGWINCH, sig_winch) == SIG_ERR) {printf("signal error\n");exit(1);}pr_winsize(STDIN_FILENO);    /* print initial size */for (; ; ) {    /* and sleep forever */pause();}
}

运行它:

termcap涉及文本文件/etc/termcap和一套读此文件的例程,它是在伯克利开发的,主要是为了支持vi编辑器。termcap文件包含了对各种终端的说明:终端行数、列数、是否支持退格、如何使终端执行某些操作(如清屏、将光标移动到给定位置),把这些信息放入文本文件中使得vi编辑器能在很多不同终端上运行。

后来,将支持termcap文件的例程从vi编辑器中抽取出来,放在一个单独的curses库中。为使这套库可供任何要进行屏幕处理的程序使用,还增加了很多功能。

termcap这种技术并不是很完善,当越来越多的终端被加到数据文件中时,为找到一个特定的终端,需要花费更长的时间扫描数据文件,并且这个数据文件中只用两个字符的名字表示各种各样的终端属性,这些缺陷迫使开发人员开发出了terminfo以及与其相关的curses库,在terminfo中,终端说明基本上都是文本说明的编译版本,在运行时易于被快速定位。terminfo最初由SVR2开始使用,此后所有System V的版本都使用它。

历史上,基于System V的系统使用terminfo,BSD派生的系统使用termcap。但现在,系统通常两者都提供,然而Mac OS X仅支持terminfo。

termcap和terminfo提供的是在各种终端上执行典型操作(清屏、移动光标)的方法。curses提供了很多函数,用来设置原始模式、cbreak模式、打开和关闭回显等。curses库是为基于字符的哑终端(一般特指只能输入、输出和显示字符的终端)设计的,如今大多是基于像素的图形终端。

将终端改为原始模式,然后不恢复直接退出:

#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>int main() {tty_raw(STDIN_FILENO);
}

之后可在终端输入reset命令将终端状态初始化。

奇校验或偶校验的实现方法:为128个字符建一张表,根据用户要求设置最高位(奇偶校验位),然后用8位I/O处理奇偶位。

书上说,vi运行时,会进入非规范模式,此时MIN=1,TIME=1,此时read调用会一直等待,直到至少键入一个字符,键入该字符后,只对后续的字符等待十分之一秒即返回,但根据linux手册:

键入一个字符后,已经达到了MIN,应该只会返回MIN个字节,不知道为什么还会等待十分之一秒。

打开一个伪终端并运行vi,然后再另一个伪终端查看此时打开vi的终端的MIN和TIME:

以下是我测试的机器上的vim版本,MIN为1,TIME为0。

UNIX环境高级编程 学习笔记 第十八章 终端I/O相关推荐

  1. UNIX环境高级编程 学习笔记 第十六章 网络IPC:套接字

    socket的设计目标之一:同样的接口既可以用于计算机间通信,也可以用于计算机内通信.socket接口可采用许多不同的网络协议进行通信,本章讨论限制在因特网事实上的通信标准:TCP/IP协议栈. 套接 ...

  2. 《UNIX环境高级编程》笔记 第十九章-伪终端

    1. 概念 伪终端这个术语是指,对于一个应用程序而言,它看上去像一个终端,但事实上它并不是一个真正的终端.下图是伪终端的一个典型安排 通常,一个进程打开伪终端主设备,然后调用fork,子进程建立一个新 ...

  3. Unix环境高级编程学习笔记(七) 多线程

    线程概述 线程(thread)技术早在60年代就被提出,但真正应用多线程到操作系统中去,是在80年代中期,solaris是这方面的佼佼者.传统的Unix也支持线程的概念,但是在一个进程(process ...

  4. Unix环境高级编程学习笔记(一)

    第二章 文件I/O 1.文件描述符   对于内核而言,所有打开的文件都通过文件描述符引用,文件描述符是一个非负整数.   Unix shell使用文件描述符0表示标准输入,1表示标准输出,2表示标准出 ...

  5. Unix环境高级编程学习笔记(五)

    第七章 进程环境 1.main函数:int main(int argc, char *argv[]) 2.进程中止: 正常中止:(1)从main返回;(2)调用exit;(3)调用_exit或_Exi ...

  6. Unix环境高级编程学习笔记(二)

    第四章 文件和目录 本章将描述文件系统特征和文件性质 1.stat.fstat和lstat函数 原型:#include<sys/stat.h> int stat(const char* r ...

  7. 文件和目录(二)--unix环境高级编程读书笔记

    在linux中,文件的相关信息都记录在stat这个结构体中,文件长度是记录在stat的st_size成员中.对于普通文件,其长度可以为0,目录的长度一般为1024的倍数,这与linux文件系统中blo ...

  8. 《UNIX环境高级编程》笔记 第五章-标准IO库

    1. 流和FILE对象 在第三章的系统调用都是围绕文件描述符fd的.但是标准I/O库函数操作则是围绕流进行的.当使用标准I/O库打开或创建一个文件时,使用一个流与一个文件关联. 当打开一个流时,标准I ...

  9. 高级IO(一)--UNIX环境高级编程读书笔记

    在前面学习了文件IO,标准IO和终端IO,现在学习高级IO,UNIX中怎么有这么多的IO. 1.非阻塞IO 可以将系统调用分为两类:低速系统调用和其他.低速系统调用是可能会使进程永远阻塞的一类系统调用 ...

  10. 《UNIX环境高级编程》笔记 第十三章-守护进程

    1. 概念 守护进程(daemon)是生存期长的一种进程.它们常常在系统引导装入时启动,仅在系统关闭时才终止.因为它们没有控制终端,所以说它们是在后台运行的. Linux的大多数服务就是用守护进程实现 ...

最新文章

  1. 注册服务(addService)
  2. Python 元组遍历排序操作方法
  3. 手机APP测试几个要点
  4. python 中的序列
  5. Mac废纸篓批量还原的方法
  6. 服务器系统给U盘盘符,五大步骤解决U盘插入电脑盘符不显示问题
  7. Maven 单元测试报错:错误: 找不到或无法加载主类 @{failsafeArgLine}
  8. nutch2.3.1 scoring-opic 插件url评分为0 问题
  9. Script:收集Exadata诊断信息
  10. linux编辑文件命令 vi_Linux的vi编辑器
  11. java关于map用来筛选的用法
  12. 如何安装mgn-mqt82.exe以及qtp10下载
  13. matlab运算放大器概述,运算放大器概述
  14. Python批量复制文件夹(以及所有子文件夹)下的某类型文件
  15. vue项目中使用思维导图mindmap
  16. alios是安卓吗_揭秘:阿里云OS和Android的主要区别是什么
  17. Zedboard 运行linaro操作系统
  18. Python 3 《dictionary》入门练习
  19. Spark 持久化(cache和persist的区别)
  20. SAXReader的主要用法(XML)

热门文章

  1. Round 2—算法的复杂度
  2. python实现复联4影评数据词云可视化分析
  3. Python的学习笔记案例4--52周存钱挑战5.0
  4. 小程序分享功能实现php,微信小程序转发好友的功能
  5. 互联网的寒冬是否只是以讹传讹?
  6. DayDayUp:计算机技术与软件专业技术资格证书之《系统集成项目管理工程师》课程讲解之十大知识领域之4辅助—项目沟通管理
  7. 进销存小程序(一)项目整体功能和设计
  8. PaddlePadlle2.3实现人脸关键点检测
  9. 阿里云手机验证码注册(可以使用阿里云提供的测试模板,不用个人申请)
  10. 基于SpringBoot实现邮箱验证码注册