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

2023-10-27

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.非规范模式输入处理:输入字符不装配成行。

默认模式是规范模式,如用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数组的相应项,该数组中的元素都用上图中的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);
    }

    // 获取禁用字符的值,该值可赋值给termios.c_cc数组中的某个元素,代表禁用该字符
    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 */

    // TCSAFLUSH:发送了所有输出后才更改termios结构中的配置,且更改发生时所有未读的输入数据都被冲洗
    if (tcsetattr(STDIN_FILENO, TCSAFLUSH, &term) < 0) {
        printf("tcsetattr error\n");
		exit(1);
    }

    exit(0);
}

执行以上程序:
在这里插入图片描述
可见执行完以上进程后,当前终端Ctrl+C已被禁用,第二个sleep命令50s后才结束。

对于以上程序要说明以下几点:
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标志,则在输入中识别此字符(Ctrl+z)。它产生SIGTSTP信号,该信号被送至前台进程组的所有进程。此字符在处理后被丢弃,不传送给读进程。

19.WERASE:字擦除字符。以扩充规范模式进行输入时识别此字符。它使前一个字被擦除。首先,它向前跳过任意一个空白字符(空格或制表符),然后向前跳过一个标记,使光标位于前一个标记的第一个字符位置上。通常一个标记碰到空白字符终止,可通过设置ALTWERASE标志改变此行为,ALTWERASE标志使前一个记号在碰到第一个非字母、非数字字符时终止。此字符在处理后被丢弃,不传送给读进程。

BREAK字符实际不是一个字符,而是在异步串行数据传送时发生的一种状况,根据串行接口的不同,通知设备驱动程序发生BREAK状况的方式也不同。

大多早期串行终端上有一个BREAK键,用其可产生BREAK状况,这就是为什么很多人认为BREAK是一个字符。PC上的BREAK键(PB或PauseBreak键)可能有其他用途,如Ctrl+BREAK可中断Windows cmd,相当于向进程发送SIGBREAK信号,此信号是Window独有的,它类似于Ctrl+C产生的SIGINT信号,目的是中断进程。

对于异步串行数据传送,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的波特率。B38400表示每秒传输38400个比特的波特率。

5.CCAR_OFLOW(c_cflag):使用RS-232调制解调器的DCD(Data-Carrier-Detect,数据载波检测)信号来开启输出的硬件流控制。这与早期的MDMBUF标志相同。硬件流控制是一种数据传输中的流控制机制,用于控制数据的发送和接收速率,以确保数据传输的可靠性和完整性。RS-232(Recommended Standard 232)是一种常见的串行通信接口标准,用于在计算机和外部设备之间进行串行数据传输。

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):若设置,则忽略调制解调器状态线,这通常意味着该设备是直接连接的,表示串口处于本地连接模式。本地连接模式(Local Mode)是串口通信中的一种工作模式,它指示串口设备不需要检测和控制调制解调器状态的变化。当串口设置为本地连接模式时,它将不会监视调制解调器状态的变化,例如 DCD(Data Carrier Detect)信号的变化。这意味着串口将不受调制解调器的状态影响,无论调制解调器处于何种状态,串口都将进行数据传输。如未设置此标志,则打开一个终端设备常常会遭遇阻塞,直到调制解调器回应呼叫并建立连接。调制解调器(Modem)是一种设备,用于将数字信号转换为模拟信号以进行远程通信,并将模拟信号转换回数字信号。

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)。带内硬件流控制(In-Band Hardware Flow Control)是一种通过数据信号本身来实现的硬件流控制方式,它使用数据通信中的特定控制字符或数据信号来控制数据的传输和流量控制。带外硬件流控制(Out-of-Band Hardware Flow Control)是一种通过与数据信号分离的独立信号线来实现的硬件流控制方式,它使用额外的控制信号线来传输流量控制信号,而不是使用数据信号本身来进行流控制。

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.ECHOK(c_lflag):若设置,且设置了ICANON,KILL字符会擦除当前行所有内容。

24.ECHOKE(c_cflag):若设置,且也设置了ICANON,则回显KILL字符的方式是擦除行中的每个字符,擦除每个字符的方式由ECHOE和ECHOPRT标志共同决定。

25.ECHONL(c_lflag):若设置,且也设置了ICANON,即使没有设置ECHO,也回显NL字符。

26.ECHOPRT(c_lflag):若设置,且也设置了ICANON和ECHO,则ERASE和WERASE(若支持WERASE字符)字符使所有要被擦除的字符在擦除后打印出来,这在硬拷贝终端上很有用,我们可以确切看到哪个字符被删除。硬拷贝终端(Hardcopy Terminal)是一种计算机终端设备,它能够将文本或图形数据以打印形式输出到纸张上,实现实时的打印输出功能。

27.EXTPROC(c_lflag):若设置,字符处理可在操作系统外执行,如果串行通信外设卡能通过执行某种行规程减轻主机处理器负载,就可以这样设置。使用伪终端时也可以设置此标志。

28.FFDLY(c_oflag):FFDLY是换页延迟(Form Feed Delay),在传统的打印机和终端设备中,Form Feed(换页符)用于指示打印机或终端在纸上进行换页操作,当设置此标志时,系统在遇到换页符后可以根据需要进行相应的处理,例如暂停输出,等待打印机或终端设备完成页面切换后再继续输出。此屏蔽字对应的值为FF0、FF1。

29.FLUSHO(c_lflag):若设置,则表示输出都会被冲洗。键入DISCARD字符时设置此标志,键入另一个DISCARD字符时清除此标志。我们也可以通过tcsetattr函数设置或清除此标志。

30.HUPCL(c_cflag):若设置,则当最后一个进程关闭设备时,调制解调器控制线将至低电平(即断开与调制解调器的连接)。

31.ICANON(c_lflag):若设置,则按规范模式工作,以下字符会起作用:EOF、EOL、EOL2、ERASE、KILL、REPRINT、STATUS、WERASE。输入字符被装配成行。

32.ICRNL(c_iflag):若设置,且未设置IGNCR,则将接收到的CR字符转换成NL字符。

33.IEXTEN(c_lflag):若设置,则识别由实现定义的扩展字符。

34.IGNBRK(c_iflag):若设置,忽略输入中的BREAK条件。BREAK条件是产生SIGINT信号还是作为数据读取,见BRKINT。

35.IGNCR(c_iflag):若设置,则忽略接收到的CR字符,若未设置,且设置了ICRNL标志,则可能将(如果在规范模式下)接收到的CR字符转换为NL字符。

36.IGNPAR(c_iflag):若设置,忽略带有结构出错(非BREAK)或奇偶出错的输入字节。

37.IMAXBEL(c_iflag):当输入队列满时响铃。

38.INLCR(c_iflag):若设置,将接收到的NL字符转换成CR字符。

39.INPCK(c_iflag):若设置,使输入奇偶校验起作用。奇偶位的“产生和检测”和“输入奇偶校验”是不同的,奇偶位的产生和检测由PARENB标志控制,设置后通常会使串行接口的设备驱动程序对输出字符产生奇偶位且对输入字符验证奇偶性;PARODD标志决定奇偶性是奇还是偶。如果一个奇偶性错误的字符到来,才检查INPCK标志的状态,若设置,则检查IGNPAR标志(是否忽略奇偶出错的输入字节),如果不忽略奇偶出错的输入字节,则检查PARMRK标志来决定应该向读进程传送哪些字符。

40.ISIG(c_flag):若设置,则判别输入字符是否是要产生终端信号的特殊字符(INTR、QUIT、SUSP、DSUSP),若是,则产生相应信号。

41.ISTRIP(c_iflag):若设置,有效输入字节被剥除为7位,未设置时,处理全部8位。

42.IUCLC(c_iflag):将输入的大写字符转换成小写字符。

43.IUTF8(c_iflag):使字符擦除对UTF-8多字节字符生效。

44.IXANY(c_iflag):任何字符都能重启输出。

45.IXOFF(c_iflag):若设置,当终端驱动程序发现输入队列将要填满时,输出一个STOP字符,此字符应由发送数据的设备识别,并使该设备停止,之后输入队列中的字符处理完毕后,终端驱动程序将输出一个START字符,使该设备恢复数据发送。

46.IXON(c_iflag):若设置,当终端驱动程序接收到STOP字符时,输出停止,在输出停止时,下一个START字符恢复输出。若未设置,STOP和START作为一般字符被进程读取。

47.MDMBUF(c_cflag):按调制解调器的载波标志进行输出流控制。是CCAR_OFLOW的曾用名。

48.NLDLY(c_oflag):换行延迟屏蔽字,该屏蔽字对应的值为NL0、NL1。

49.NOFLSH(c_lflag):按系统默认,当终端驱动程序产生SIGINT、SIGQUIT信号时,输入和输出队列都被清空;当它产生SIGSUSP信号时,输入队列被清空。若设置此标志,则产生这些信号时,不对输入输出队列进行常规清空。

50.NOKERNINFO(c_lflag):若设置,阻止STATUS字符打印前台进程组的信息。但无论是否设置此标志,STATUS字符都会使SIGINFO信号发送到前台进程组。

51.OCRNL(c_oflag):若设置,将输出的CR字符转换成NL字符。

52.OFDEL(c_oflag):若设置,则输出的填充字符是ASCII DEL,否则是ASCII NUL。

53.OFILL(c_oflag):若设置,则传递填充字符(填充字符是什么取决于OFDEL)以实现延迟来取代时间延迟。6个延迟标志字分别为BSDLY、CRDLY、FFDLY、NLDLY、TABDLY、VTDLY。

54.OLCUC(c_oflag):若设置,将小写字符转换为大写字符。

55.ONLCR(c_oflag):若设置,输出的NL字符会转换为CR-NL字符。

56.ONLRET(c_oflag):若设置,则输出的NL字符转换为CR字符。

57.ONOCR(c_oflag):若设置,在0列不输出CR。

58.ONOEOT(c_oflag):若设置,在输出中丢弃EOT(^D)字符。在某些将Ctrl+D解释为挂断的终端上,此标志是必需的。在POSIX系统中,Ctrl+D不会产生EOT,而是立即交付当前的行缓冲。

58.OPOST(c_oflag):若设置,使用实现定义的输出处理。

59.OXTABS(c_oflag):若设置,则制表符在输出中被扩展为空格,其效果与TABDLY被设为XTABS或TAB3所产生的效果相同。

60.PARENB(c_cflag):若设置,对输出字符产生奇偶校验位,对输入字符执行奇偶校验。若设置PARODD,则奇偶校验是奇校验,否则是偶校验。另外影响奇偶校验的标志:INPCK、IGNPAR、PARMRK。

61.PAREXT(c_cflag):标记或置空奇偶校验位。若设置了PAREXT,且设置了PARODD,则奇偶位总是1(即标记奇偶位);若设置PAREXT,且没设置PAROOD,则奇偶位总是0(即置空奇偶位)。

62.PARMRK(c_iflag):若设置,且未设置IGNPAR,则带有结构出错的非BREAK字符或奇偶出错的字节被进程读作一个3字符序列\377、\0、X(X指接收到的出错字节)。若设置,且未设置ISTRIP,则一个有效的\377字节被传送给进程时为\377、\377。若未设置IGNPAR,且未设置PARMRK,则带结构出错的非BREAK字符或奇偶出错的字节被读作字符\0。

63.PARODD(c_cflag):若设置,则输出和输入字符的奇偶性都是奇,否则为偶。PARENB标志控制奇偶性的产生和检测。

64.PENDIN(c_lflag):若设置,在下个字符输入时,尚未读的任何输入都由系统重新打印。此标志与键入REPRINT字符时的作用相似。

65.TABDLY(c_oflag):水平制表符延迟屏蔽字,此屏蔽字对应的值为TAB0、TAB1、TAB2、TAB3。XTABS的值等于TAB3,此值使系统将制表符扩展成空格。系统假定制表符长度占8个空格,且不能被更改。

66.TOSTOP(c_lflag):若设置,且支持作业控制,则将信号SIGTTOU送到试图写控制终端的后台进程的进程组。默认,SIGTTOU暂停进程组中所有进程,如果写控制终端的后台进程忽略或阻塞此信号,则终端驱动程序不产生此信号。

67.VTDLY(c_oflag):垂直制表延迟屏蔽字,此屏蔽字对应的值是VT0、VT1。

68.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派生的平台趋向于以位屏蔽方式编码波特率。

以下函数提供终端设备的行控制能力,它们都要求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.
     * /dev/fd是一个特殊的目录,在Unix和类Unix系统中,它是文件描述符的符号链接目录。每个打开的文件描述符都可以通过/dev/fd/<file descriptor>的路径进行访问。
     */
    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;
    }
    // strdup函数分配内存,然后创建一个字符串的副本并返回该副本
    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;
		}
		// The st_ino and st_dev fields taken together uniquely identify the file within the system.
		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节点编号的文件。每个文件系统中的文件都有一个该文件系统内唯一的i节点编号(st_ino),而设备号(st_dev)用于标识文件所在的设备。以上我们假定在找到一个匹配的设备号和匹配的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字符(Ctrl+H):

abcd[ERASE字符]efghij

则会输出:

abcefghi

如果输入中带KILL字符(Ctrl+U):

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个或read调用所要求的字节数前不返回,这会造成无限期阻塞。

3.MIN=0、TIME>0:调用read时启动一个定时器,接到一个字节或定时器超时时,read调用返回,如果是定时器超时,则read函数返回0。

4.MIN=0、TIME=0:如调用read时无数据可读,立即返回0,如有数据可读,最多返回read函数所要求的字节数。

以上4种情形的总结:
在这里插入图片描述
MIN只是表示最小值,如果程序要求的数据量大于MIN,是可能接收到所要求的字节数的。

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

以下函数tty_cbreak和tty_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 above 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 above 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) == 0177) {    /* 0177(127) = 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),所以每个字符后没有回车符(\r)只有换行符(\n)。在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>
#endif

static 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个字节,即1个字节,应该就不会再等待后续的字节了。

本文内容由网友自发贡献,版权归原作者所有,本站不承担相应法律责任。如您发现有涉嫌抄袭侵权的内容,请联系:hwhale#tublm.com(使用前将#替换为@)

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

  • 缺少 /var/lib/mysql/mysql.sock 文件

    我正在尝试访问 mysql 当我运行 mysql 命令时 我得到以下信息 root ip 10 229 65 166 tpdatabase 1 8 0 28356 mysql 错误 2002 HY000 无法连接到 通过socket本地My
  • 在 fork() 之后寻求有关“文件描述符”的简单描述

    Unix 环境中的高级编程 第二版 作者 W Richard Stevens 第 8 3 节 fork 函数 描述如下 父级和子级共享相同的文件偏移量非常重要 考虑一个分叉子进程 然后等待子进程完成的进程 假设两个进程都写入标准输出作为其正
  • 使用 ffmpeg 从 unix 命令批量将 wav 文件转换为 16 位

    我有一个由许多子文件夹组成的文件夹 每个子文件夹都有其他子文件夹 其中包含 wav 文件 我想像这样转换所有文件 ffmpeg i BmBmGG BmBmBmBm wav acodec pcm s16le ar 44100 BmBmGG B
  • 将多个参数传递给 UNIX shell 脚本

    我有以下 bash shell 脚本 理想情况下我会用它来按名称杀死多个进程 bin bash kill ps A grep awk print 1 然而 虽然此脚本有效 但传递了一个参数 端镀铬 脚本名称为end 如果传递多个参数 则它不
  • WebSockets 监听 UNIX 域套接字?

    是否可以在 nginx 服务器后面设置一个 WebSockets 服务器来处理 UNIX 域套接字上的连接 我目前在同一台计算机上有多个 WebSocket 服务器实例 并且存在端口共享问题 所有实例都必须分配一个唯一的端口 我想避免这种情
  • 如何将文件中的值分配给 UNIX sh shell 中的变量?

    我一直在搜索这个网站 试图找到这个问题的答案 并发现了几个非常好的答案 不幸的是 它们都不适合我 这是我正在使用的脚本 VALUE cat szpfxct tmp export VALUE echo gt gt LGFILE echo te
  • 如何拆分一行并重新排列其元素?

    我在一行中有一些数据 如下所示 abc edf xyz rfg yeg udh 我想呈现如下数据 abc xyz yeg edf rfg udh 以便打印备用字段并用换行符分隔 有没有这样的衬里 下列awk脚本可以做到这一点 gt echo
  • Linux 中 AF_UNIX 数据报消息的最大大小是多少?

    目前我已达到 130688 字节的硬限制 如果我尝试在一条消息中发送更大的内容 我会收到一条消息ENOBUFS error 我已经检查过net core rmem default net core wmem default net core
  • 如何复制每个扩展名为 X 的文件,同时保留原始文件夹结构? (类Unix系统)

    我正在尝试将每个 HTML 文件从 src 文件夹复制到 dist 文件夹 但是 我想保留原始文件夹结构 如果 dist 文件夹不存在 我想创建一个新文件夹 如果文件夹不存在则创建 d dist mkdir dist 复制每个文件 cp R
  • linux下如何获取昨天和前天?

    我想在变量中获取 sysdate 1 和 sysdate 2 并回显它 我正在使用下面的查询 它将今天的日期作为输出 bin bash tm date Y d m echo tm 如何获取昨天和前天的日期 这是另一种方法 对于昨天来说 da
  • 如何在gcc中打印UINT64_t?

    为什么这段代码不起作用 include
  • Python 用静态图像将 mp3 转换为 mp4

    我有x文件包含一个列表mp3我想转换的文件mp3文件至mp4文件带有static png photo 似乎这里唯一的方法是使用ffmpeg但我不知道如何实现它 我编写了脚本来接受输入mp3文件夹和一个 png photo 然后它将创建新文件
  • 进程名称长度的最大允许限制是多少?

    进程名称允许的最大长度是多少 我正在读取进程名称 proc pid stat文件 我想知道我需要的最大缓冲区 我很确定有一个可配置的限制 但就是找不到它在哪里 根据man 2 prctl http man7 org linux man pa
  • 如何在Unix中将相对路径转换为绝对路径[关闭]

    Closed 这个问题不符合堆栈溢出指南 help closed questions 目前不接受答案 我想转换 相对路径 home stevin data APP SERVICE datafile txt to 绝对路径 home stev
  • 在以下程序中将产生多少个进程

    int main fork fork fork fork fork printf forked n return 0 当我们调用 fork 函数时 父进程会得到一个非零 pid而孩子得0分作为回报 基于这个逻辑 在第二个陈述中 我们必须应用
  • 为什么总是./configure;制作;进行安装;作为 3 个单独的步骤?

    每次从源代码编译某些内容时 都会经历相同的 3 个步骤 configure make make install 我明白 将安装过程分为不同的步骤是有意义的 但我不明白 为什么这个星球上的每个编码员都必须一次又一次地编写相同的三个命令才能完成
  • awk: hping: 打印 icmp 发起/接收之间的差异

    我有以下输出hping http ports su net hping在 OpenBSD 上 hping icmp ts www openbsd org HPING www openbsd org re0 129 128 5 194 icm
  • 了解多个进程的并发文件写入

    从这里 UNIX 中文件追加是原子的吗 https stackoverflow com questions 1154446 is file append atomic in unix 考虑多个进程打开同一个文件并向其追加内容的情况 O AP
  • 如何使用 UNIX shell 计算字母在文本文件中出现的次数?

    我有几个文本文件 我想计算每个字母在每个文件中出现的次数 具体来说 我想使用 UNIX shell 来执行此操作 形式为 cat file 做东西 有没有办法让 wc 命令来执行此操作 grep char o filename wc l
  • 抑制 makefile 中命令调用的回显?

    我为一个作业编写了一个程序 该程序应该将其输出打印到标准输出 分配规范需要创建一个 Makefile 当调用它时make run gt outputFile应该运行该程序并将输出写入一个文件 该文件的 SHA1 指纹与规范中给出的指纹相同

随机推荐

  • web开发资源

    1 http www cnblogs com lhb25 archive 2011 05 26 1997341 html 主要进行web开发 2
  • 洛谷P1216 [USACO1.5][IOI1994]数字三角形 Number Triangles题解

    接触的第一道DP题 动态规划入门 题目描述 写一个程序来查找从最高点到底部任意处结束的路径 使路径经过数字的和最大 每一步可以走到左下方的点也可以到达右下方的点 7 3 8 8 1 0 2 7 4 4 4 5 2 6 5 在上面的样例中 从
  • javafx 图片加载不出来,并且文件是可导的情况

    1 检查是不是没有在new imgae 前面加上file 如果是文件的话 或者URL 如果是网址 2 可能是file 中间无意识加了空格 我就是这样怎么查都查不出来 这样的结果就是显示一片空白 没有图片 而且不报错 3 另外 注意如果批量命
  • Unity私有变量在其它脚本的获取

    Unity私有变量在其它脚本的获取 以下是脚本A private int curHealth 5 int value 2 public int Health set curHealth value get return curHealth
  • Spring学习--IOC容器的初始化过程

    IOC容器初始化概述 IOC容器初始化是由refresh 方法来启动的 这个方法标志着IOC容器的正式启动 Spring将IOC容器启动的过程分开 并使用不同的模块来完成 如使用ResourceLoader BeanDefinition等模
  • 解决:"MySQL error code 145 Table was marked as crashed and should be repaired"的问题

    该错误指的是数据库的表损坏需要修复 我的数据库客户端使用的是Navicat 通过查看服务的错误日志我们可以知道具体是哪个表出现了问题 在Navicat上如果试图鼠标双击打开那个被损坏的表也会显示如标题所示的错误信息 MySQL error
  • Ubuntu安装软件时Could not get lock /var/lib/dpkg/lock - open (11: Resource temporarily unavailable)的解决方案

    Ubuntu 19 04 在安装wireshark的时候 sudo apt get install wireshark后遇到报错如下 E Could not get lock var lib dpkg lock open 11 Resour
  • 运维基础知识

    一 简述运维流程 1 接手平台 管理资产 增删 设置平台对资产是扫描策略 2 每天按照规定的巡检周期对资产进行巡检 巡检过程中检测资产的目前状态做记录 查看是否有新增告警事件 将发现的新增告警事件按照规定输出详细的事件工单 工单内要求详细描
  • Error processing condition on org.springframework.cloud.commons.httpclient.HttpClientConfiguration

    背景 创建springboot项目后 导入nacos 配置中心和注册中心依赖后报错 springboot启动类无法正常启动 控制台异常如题 报错源码 java lang IllegalStateException Error process
  • Springboot数据库连接池报错的解决办法

    Springboot数据库连接池报错的解决办法 这个异常通常在Linux服务器上会发生 原因是Linux系统会主动断开一个长时间没有通信的连接 那么我们的问题就是 数据库连接池长时间处于间歇状态 导致Linux系统将其断开了 然后抛出了这个
  • 关于解决win10的 tencent qqmail plugin 卸载不了的问题

    问题出现场景 我也是偶然一次在搜索我电脑里面下载的程序时 发现有个叫做tencent qqmail plugin的程序怎么也删除不掉 经过的我不断的尝试 欸 我终于找到解决方法了 问题描述 我之前使用的方法的是在 设置 应用 应用程序 中卸
  • 在VMware里克隆出来的CentOSLinux。。 ifconfig...没有看到eth0.。然后重启网卡又报下面错误。

    原文地址 http www 51testing com html 90 360490 846295 html 故障现象 service network restart Shutting down loopback insterface OK
  • 流程图中的虚线含义_流程图图形符号标准含义简介

    流程图是我们工作中经常会用到的一种工具 形象直观 便于理解 直观地描述一个工作过程的具体步骤 流程图对准确了解事情是如何进行的 以及决定应如何改进过程极有帮助 这一方法可以用于整个企业 以便直观地跟踪和图解企业的运作方式 流程图中有很多图形
  • 数据挖掘初探(skleran)

    1 使用sklearn进行数据挖掘 1 1 数据挖掘的步骤 数据挖掘通常包括数据采集 数据分析 特征工程 训练模型 模型评估等步骤 我们使用sklearn进行虚线框内的工作 sklearn也可以进行文本特征提取 通过分析sklearn源码
  • RT-Thread记录(十一、I/O 设备模型之UART设备 — 源码解析)

    深入理解 RT Thread I O 设备模型 分析 UART设备源码 目录 前言 一 初识 UART 操作函数 应用程序 二 UART 的初始化 2 1 UART 设备初始化位置 2 2 UART 设备初始化函数分析 stm32 uart
  • 华为IP的考试费要好几千,想问一下这个证书的含金量怎么样?

    虽然华为认证HCIP考试只考笔试 题库稳 运气好的话刷题库就有可能会过 但是其实学的时候还是好好学的 要不然只为了考试而去背题 但是实际操作能力不行的话一样会被企业拒绝的 最重要的还是掌握华为认证HCIP的技能 证书只是找工作的一个敲门砖
  • 服务器系统这么做,服务器怎么做系统

    服务器怎么做系统 内容精选 换一换 无法直接从云备份控制台查看备份中的数据 您可以通过以下几种方式进行查看 云服务器备份使用云服务器备份创建镜像后 再使用镜像创建云服务器 登录云服务器 查看服务器中的数据 云硬盘备份使用云硬盘备份创建新的云
  • 算法题:完全二叉树的权值

    问题描述 给定一棵包含 N 个节点的完全二叉树 树上每个节点都有一个权值 按从 上到下 从左到右的顺序依次是 A1 A2 AN 如下图所示 现在要把相同深度的节点的权值加在一起 他想知道哪个深度的节点 权值之和最大 如果有多个深度的权值和同
  • 代理模式,以及Java的动态代理

    定义 为其他对象提供一种代理以控制对这个对象的访问 可以提供额外不同的操作 UML类图 Subject类 定义了RealSubject和Proxy的共用接口 这样就在任何使用RealSubject的地方都可以使用Proxy RealSubj
  • UNIX环境高级编程 学习笔记 第十八章 终端I/O

    20世纪70年代后期 系统 UNIX System III 发展出一套不同于V7 Version 7 Unix 的终端IO例程 使得UNIX终端IO处理分立为两种不同风格 一种是系统 风格 它延续到了System V 另一种是V7风格 它成