5.4 termios结构
termios是在POSIX规范中定义的标准接口,它类似于系统V中的termio接口。通过设置termios类型的数据结构中的值和使用一小组函数调用,你就可以对终端接口进行控制。termios数据结构和相关函数调用都定义在头文件termios.h中。
如果程序需要调用定义在termios.h头文件中的函数,它就需要与一个正确的函数库进行链接,这个函数库可能是标准的C函数库或者curses函数库(取决于你的安装情况)。如果需要,在编译本章中的示例程序时,在编译命令的末尾加上-lcurses。在一些老版本的Linux系统中,curses库被命名为new curses。在这种情况下,库名和链接参数就需要相应地改为ncurses和-lncurses。
可以被调整来影响终端的值按照不同的模式被分成如下几组:
❑ 输入模式
❑ 输出模式
❑ 控制模式
❑ 本地模式
❑ 特殊控制字符
最小的termios结构的典型定义如下(X/Open规范允许包含附加字段):
结构成员的名称与上面列出的5种参数类型相对应。
你可以调用函数tcgetattr来初始化与一个终端对应的termios结构,该函数的原型如下:
这个函数调用把当前终端接口变量的值写入termios_p参数指向的结构。如果这些值其后被修改了,你可通过调用函数tcsetattr来重新配置终端接口,该函数的原型如下:
参数actions控制修改方式,共有3种修改方式,如下所示。
❑ TCSANOW:立刻对值进行修改。
❑ TCSADRAIN:等当前的输出完成后再对值进行修改。
❑ TCSAFLUSH:等当前的输出完成后再对值进行修改,但丢弃还未从read调用返回的当前可用的任何输入。
注意,程序有责任将终端设置恢复到程序开始运行之前的状态,这一点是非常重要的。首先保存这些值,然后在程序结束时恢复它们,这永远是程序的职责。
接下来,我们将仔细分析各种模式和相关的函数调用。一些模式的细节非常晦涩、专业,而且很少使用,所以我们在此只介绍主要的功能。如果读者需要了解更多内容,请查阅man帮助手册或POSIX、X/Open的规范文档。
你首先应该了解的是本地模式,它也是最重要的一种模式。我们在本章中编写的第一个应用程序出现了两个问题,其中第二个问题(用户必须按下回车键才能让程序读取输入)的解决方法是使用标准模式或非标准模式,即你可以让程序等待一行输入完毕后再进行处理,或让它一有字符键入就立刻处理。
5.4.1 输入模式
输入模式控制输入数据(终端驱动程序从串行口或键盘接收到的字符)在被传递给程序之前的处理方式。你通过设置termios结构中c_iflag成员的标志对它们进行控制。所有的标志都被定义为宏,并可通过按位或的方式结合起来。这也是所有终端模式都采用的方法。
可用于c_iflag成员的宏如下所示。
❑ BRKINT:当在输入行中检测到一个终止状态(连接丢失)时,产生一个中断。
❑ IGNBRK:忽略输入行中的终止状态。
❑ ICRNL:将接收到的回车符转换为新行符。
❑ IGNCR:忽略接收到的回车符。
❑ INLCR:将接收到的新行符转换为回车符。
❑ IGNPAR:忽略奇偶校验错误的字符。
❑ INPCK:对接收到的字符执行奇偶校验。
❑ PARMRK:对奇偶校验错误做出标记。
❑ ISTRIP:将所有接收到的字符裁减为7比特。
❑ IXOFF:对输入启用软件流控。
❑ IXON:对输出启用软件流控。
如果BRKINT和IGNBRK标志都未被设置,则输入行中的终止状态就被读取为NULL(0x00)字符。
用户一般无需频繁修改输入模式,因为它的默认值通常就是最合适的,所以我们在这里就不过多讨论了。
5.4.2 输出模式
输出模式控制输出字符的处理方式,即由程序发送出去的字符在传递到串行口或屏幕之前是如何处理的。正如你预料的那样,许多处理方式正好与输入模式对应。它还有几个其他标志,主要用于慢速终端,因为这些终端在处理回车符等字符时需要花费一定的时间。几乎所有这些标志不是多余的(因为现在的终端速度比以前要快得多),就是用具有终端处理能力的terminfo数据库处理会更有效(在本章的后面你会用到该数据库)。
你通过设置termios结构中c_oflag成员的标志对输出模式进行控制。可用于c_oflag成员的宏如下所示。
❑ OPOST:打开输出处理功能。
❑ ONLCR:将输出中的换行符转换为回车/换行符。
❑ OCRNL:将输出中的回车符转换为新行符。
❑ ONOCR:在第0列不输出回车符。
❑ ONLRET:不输出回车符。(2)
❑ OFILL:发送填充字符以提供延时。
❑ OFDEL:用DEL而不是NULL字符作为填充字符。
❑ NLDLY:新行符延时选择。
❑ CRDLY:回车符延时选择。
❑ TABDLY:制表符延时选择。
❑ BSDLY:退格符延时选择。
❑ VTDLY:垂直制表符延时选择。
❑ FFDLY:换页符延时选择。
如果没有设置OPOST,则所有其他标志都被忽略。
由于输出模式用得也不多,所以我们在此也不做过多的讨论。
5.4.3 控制模式
控制模式控制终端的硬件特性。你通过设置termios结构中c_cflag成员的标志对控制模式进行配置。可用于c_cflag成员的宏如下所示。
❑ CLOCAL:忽略所有调制解调器的状态行。
❑ CREAD:启用字符接收器。
❑ CS5:发送或接收字符时使用5比特。
❑ CS6:发送或接收字符时使用6比特。
❑ CS7:发送或接收字符时使用7比特。
❑ CS8:发送或接收字符时使用8比特。
❑ CSTOPB:每个字符使用两个停止位而不是一个。
❑ HUPCL:关闭时挂断调制解调器。
❑ PARENB:启用奇偶校验码的生成和检测功能。
❑ PARODD:使用奇校验而不是偶校验。
如果设置了HUPCL标志,当终端驱动程序检测到与终端对应的最后一个文件描述符被关闭时,它将通过设置调制解调器的控制线来挂断电话线路。
控制模式主要用于串行线连接调制解调器的情况,虽然它也可用来和终端进行“对话”。但与通过使用termios的控制模式来修改默认的线路行为相比,直接修改终端配置文件通常更加容易一些。
5.4.4 本地模式
本地模式控制终端的各种特性。你通过设置termios结构中c_lflag成员的标志对本地模式进行配置。可用于c_lflag成员的宏如下所示。
❑ ECHO:启用输入字符的本地回显功能。
❑ ECHOE:接收到ERASE时执行退格、空格、退格的动作组合。
❑ ECHOK:接收到KILL字符时执行行删除操作。
❑ ECHONL:回显新行符。
❑ ICANON:启用标准输入处理(参见下面的说明)。
❑ IEXTEN:启用基于特定实现的函数。
❑ ISIG:启用信号。
❑ NOFLSH:禁止清空队列。
❑ TOSTOP:在试图进行写操作之前给后台进程发送一个信号。
这里最重要的两个标志是ECHO和ICANON。前者的作用是抑制键入字符的回显,而后者是将终端在两个截然不同的接收字符处理模式间进行切换。如果设置了ICANON标志,就启用标准输入行处理模式,否则,就启用非标准模式。
5.4.5 特殊控制字符
特殊控制字符是一些字符组合,如Ctrl+C,当用户键入这样的组合键时,终端会采取一些特殊的处理方式。termios结构中的c_cc数组成员将各种特殊控制字符映射到对应的支持函数。每个字符的位置(它在数组中的下标)是由一个宏定义的,但并不限制这些字符必须是控制字符。
根据终端是否被设置为标准模式(即termios结构中c_lflag成员是否设置了ICANON标志),c_cc数组有两种差别很大的用法。
要特别注意的一点是,在两种不同的模式下,数组下标值有一部分是重叠的。出于这个原因,你一定要注意不要将两种模式各自的下标值混用。
下面是在标准模式中可以使用的数组下标。
❑ VEOF:EOF字符。
❑ VEOL:EOL字符。
❑ VERASE:ERASE字符。
❑ VINTR:INTR字符。
❑ VKILL:KILL字符。
❑ VQUIT:QUIT字符。
❑ VSUSP:SUSP字符。
❑ VSTART:START字符。
❑ VSTOP:STOP字符。
下面是在非标准模式中可以使用的数组下标。
❑ VINTR:INTR字符。
❑ VMIN:MIN值。
❑ VQUIT:QUIT字符。
❑ VSUSP:SUSP字符。
❑ VTIME:TIME值。
❑ VSTART:START字符。
❑ VSTOP:STOP字符。
1.字符
由于这些特殊字符和非标准值对于输入字符的高级处理非常重要,所以我们在这里对它们进行详细的解释,如表5-1所示。
表 5-1
2.TIME和MIN值
TIME和MIN的值只能用于非标准模式,两者结合起来共同控制对输入的读取。此外,两者的结合使用还能控制在一个程序试图读取与一个终端关联的文件描述符时将发生的情况。
两者的结合分为如下4种情况。
❑ MIN = 0和TIME = 0:在这种情况下,read调用总是立刻返回。如果有等待处理的字符,它们就会被返回;如果没有字符等待处理,read调用返回0,并且不读取任何字符。
❑ MIN = 0和TIME > 0:在这种情况下,只要有字符可以处理或者是经过TIME个十分之一秒的时间间隔,read调用就返回。如果因为超时而未读到任何字符,read返回0,否则read返回读取的字符数目。
❑ MIN > 0和TIME = 0:在这种情况下,read调用将一直等待,直到有MIN个字符可以读取时才返回,返回值是读取的字符数量。到达文件尾时返回0。
❑ MIN > 0和TIME > 0:这是最复杂的一种情况。当read被调用时,它会等待接收一个字符。在接收到第一个字符及后续的每个字符后,一个字符间隔定时器被启动(如果定时器已在运行,则重启它)。当有MIN个字符可读或两个字符之间的时间间隔超过TIME个十分之一秒时,read调用返回。这个功能可用于区分是单独按下了Escape键还是按下一个以Escape键开始的功能组合键。但要注意的是,网络通信或处理器的高负载将使得类似这样的定时器失去作用。
通过设置非标准模式与使用MIN和TIME值,程序可以逐个字符地处理输入。
3.通过shell访问终端模式
如果在使用shell时想查看当前的termios设置情况,可以使用下面的命令:
在我的Linux系统上(它对标准termios结构进行了一些扩展),这个命令的输出如下:
从上面的命令输出中,你可以看到,EOF字符是Ctrl+D并且启用了本地回显。当在做终端控制的练习时,一不小心就会将终端设置为非标准状态,这将使得终端的使用非常困难。下面几种方法可以帮你摆脱这种困境。
❑ 第一种方法是使用如下命令(这要求你的stty版本支持这种用法):
❑ 如果回车键和新行符(用于终止输入行)的映射关系丢失了,你可能就需要输入命令stty sane,然后按下Ctrl+J(它对应新行符),而不是按下回车键Enter。
❑ 第二种方法是用命令stty -g将当前的stty设置保存到某种可以重新读取的形式中。使用的命令如下:
❑ 注意,对最后一个stty命令,你可能还需要使用Ctrl+J的组合键来代替回车键Enter。你也可以在shell脚本中使用相同的方法:
❑ 如果上面两种方法都不能解决问题,还有第三种方法,就是从另一个终端登录,用ps命令查找不能使用的那个shell的进程号,然后用命令kill HUP <进程号>强制中止该shell。因为系统总是在给出登录提示符之前重置stty参数,所以你就可以正常地登录系统了。
4.在命令提示符下设置终端模式
你还可以在命令提示符下用stty命令直接设置终端模式。
比如说,如果想让shell脚本可以读取单字符,你就需要关闭终端的标准模式,同时将MIN设为1,TIME设为0。使用的命令如下:
现在终端已被设置为可立刻读取字符了。如果重新运行第一个程序menu1,你会发现它将按照设计的要求正常工作。
你还可以对第2章的密码检查程序加以改进,在程序提示输入密码前将回显功能关闭。使用的命令如下:
注意,在使用上面命令之后要记住用命令stty echo将回显功能再次恢复启用。
5.4.6 终端速度
termios结构提供的最后一个功能是控制终端速度,但termios结构中并没有与终端速度对应的成员,它是通过函数调用来进行设置的。要注意的是,输入速度和输出速度是分开处理的。
4个函数调用的原型如下:
注意,这些函数作用于termios结构,而不是直接作用于端口。这意味着,要想设置新的终端速度,你就必须首先用函数tcgetattr获取当前终端设置,然后使用上述函数之一设置终端速度,最后使用函数tcsetattr写回termios结构。只有在调用了函数tcsetattr之后,终端速度才会改变。
上面函数调用中speed参数可设置的值很多,下面是最重要的。
❑ B0:挂起终端。
❑ B1200:1200波特。
❑ B2400:2400波特。
❑ B9600:9600波特。
❑ B19200:19200波特。
❑ B38400:38400波特。
标准中没有定义大于38400波特的速度,也无标准方法用来支持串行口的速度大于它。
包括Linux在内的一些操作系统,为了支持更高的速度,补充定义了值B57600、B115200和B230400。如果使用的Linux版本比较低,它可能没有定义这些值,但可以通过命令setserial来获取57600和115200这样的非标准速度。要注意的是,在这种情况下,只有当先设置了B38400后,才能使用这些速度。这两种方法都不具备可移植性,所以你在使用它们时要考虑清楚。
5.4.7 其他函数
在控制终端方面还有一些其他的函数。它们直接对文件描述符进行操作,不需要读写termios结构。它们的定义如下:
这些函数的功能如下所示。
❑ 函数tcdrain的作用是让调用程序一直等待,直到所有排队的输出都已发送完毕。
❑ 函数tcflow用于暂停或重新开始输出。
❑ 函数tcflush用于清空输入、输出或者两者都清空。
我们已介绍完了termios结构,下面来看几个实用的例子。其中最简单的大概要算读取密码时禁止回显了。通过关闭ECHO标志即可做到这一点。
实 验 使用termios结构的密码程序
(1)密码程序password.c以下面的定义开始:
(2)接下来,增加一行语句来获取标准输入的当前设置,并把这些值保存到刚才创建的termios结构中:
(3)对原始的设置值做一份副本以便在程序结束时还原设置。在termios结构变量newrsettings中关闭ECHO标志,然后提示用户输入密码:
(4)接下来,用newrsettings变量中的值设置终端属性并读取用户输入的密码。最后,将终端属性还原到原来的样子并输出刚才读取的密码,但这让刚才的努力都“白费”了(这只是为了说明回显功能恢复了,在实际程序中不要输出密码)。
运行这个程序,你将看到如下的输出:
实验解析
在这个例子中,用户输入hello,但在Enter password:提示符后并不显示用户输入的内容,直到用户按下回车键后程序才有输出。
请注意只修改你需要修改的标志,使用的语法结构是X &= ~FLAG(它的作用是清除变量X中由FLAG标志定义的比特)。如果需要,你可以用语法结构X|= FLAG对由FLAG标志定义的单个比特进行置位,虽然在上面的例子中并不需要这样做。
在设置终端属性时,你用TCSAFLUSH丢弃用户在程序准备好读取数据之前输入的任何内容。这样的处理方式是为了培养用户的一个好习惯,即在回显功能关闭之前不要试图输入自己的密码。在程序结束之前,你还恢复了终端的原始设置。
termios结构的另一种常见用法是,将终端设置为这样一种状态:一旦输入字符,程序就立刻读取它。这是通过关闭标准模式并结合使用MIN和TIME设置来实现的。
实 验 读取每个字符
利用新学到的知识,你可以对菜单程序做一些修改。下面的程序menu4.c基于menu3.c,它在后者中插入了许多来自password.c中的代码。修改的内容以阴影显示,并在下面的步骤中进行了解释。
(1)在程序的开始,必须包含一个新的头文件:
(2)接下来,需要在main函数中声明一些新变量:
(3)在调用getchoice函数之前,需要改变终端的特性,插入下面这些语句:
(4)在退出程序之前,还需要将终端属性还原为原来的值:
(5)由于在非标准模式下,默认的回车和换行符之间的映射已不存在了,所以需要对回车符\r进行检查。
除非你做出安排,否则,当用户按下Ctrl+C组合键时,程序将终止。你可以通过在本地模式下清除ISIG标志来禁用对这些特殊字符的处理。要做到这一点,你需要在main函数中增加如下一条语句,如前面的步骤所示:
如果将这些修改放入菜单程序,则只要用户一键入字符就会立刻得到程序的响应,而且用户键入的字符不会回显。
如果按下组合键Ctrl+C,它将被直接传递给程序,并被程序认为是一个不正确的菜单选择。