APUE 终端I/O

《UNIX环境高级编程》第18章 终端I/O 笔记

综述

终端I/O有两种不同的工作模式:

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

如果不做特殊处理,则默认模式是规范模式

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

有以下几点说明:

  • 如果打开了回显功能,则在输入队列和输出队列之间有一个隐含的连接
  • 输入队列的长度MAX_INPUT是有限值。当一个特定设备的输入队列已经填满时,系统的行为将依赖于实现。这种情况发生时大多数UNIX系统回显响铃字符。
  • 图中没有显示另一个输入限制MAX_CANON。这个限制是一个规范输入行的最大字节数
  • 虽然输出队列的长度通常也是有限的,但是程序并不能获得这个定义其长度的常量为当输出队列将要填满时,内核便直接使写进程休眠,直至写队列中有可用的空间。

大多数UNIX系统在一个称为终端行规程(terminal linediscipline)的模块中进行全部的规范处理。可以将这个模块设想成一个盒子,位于内核通用读、写函数和实际设备驱动程序之间

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

所有可以检测和更改的终端设备特性都包含在termios结构中。该结构定义在头文件<termios.h>中,本章使用这一头文件。

各个标志位的作用:

  • 输入标志通过终端设备驱动程序控制字符的输入(例如,剥除输入字节的第8位,允许输入奇偶校验)
  • 输出标志则控制驱动程序输出(例如,执行输出处理、将换行符转换为CRLF)
  • 控制标志影响RS232串行线(例如,忽略调制解调器的状态线、每个字符的一个或两个停止位)
  • 本地标志影响驱动程序和用户之间的接口(例如,回显打开或关闭、可视地擦除字符、允许终端产生的信号以及对后台输出的作业控制停止信号

与终端有关的各个函数之间的关系

特殊输入字符

POSIX.1定义了11个在输入时要特殊处理的字符。实现定义了另外一些特殊字符

在POSIX的11个特殊字符中,其中有9个字符的值可以任意更改。不能更改的两个特殊字符是换行符和回车符(分别是\n和\r),也可能是STOP和START字符(依赖于实现)。为了更改,只需要修改termios结构中c_cc数组的相应项。该数组中的元素都用名字作为下标进行引用,每个名字都以字母V开头。

对各个字符的详细解释可见原书内容

获得和设置终端属性

为了获得和设置 termios结构,可以调用 tcgetattr和 tcsetattr函数。这样就可以检测和修改各种终端选项标志和特殊字符,使终端按我们所希望的方式进行操作。

1
2
3
4
5
6
#include <termios.h> 

int tcgetattr(int fd, struct termios *termptr);
int tcsetattr(int fd, int opt, const struct termios *termptr);

// Both return: 0 if OK, −1 on error

tcsetattr的参数opt使我们可以指定在什么时候新的终端属性才起作用。opt可以指定为下列常量中的一个:

  • TCSANOW 更改立即发生。
  • TCSADRAIN 发送了所有输出后更改才发生。若更改输出参数则应使用此选项。
  • TCSAFLUSH 发送了所有输出后更改才发生。更进一步,在更改发生时未读的所有输入数据都被丢弃(冲洗)。

终端选项标志

包含终端选项标志的所有的解释,具体可查看原书

stty命令

上节说明的所有选项都可以被检查和更改:在程序中用tcgetattr和tcsetattr函数进行检查和更改;在命令行(或shell脚本)中用stty(1)命令进行检查和更改。简单地说,stty(1)命令就是终端I/O所列的前6个函数的接口。

波特率函数

术语波特率(baud rate)是一个历史沿用的术语,现在它指的是“位/秒”(bit per second)。虽然大多数终端设备对输入和输出使用同一波特率,但是只要硬件许可,可以将它们设置为两个不同值。

设置波特率函数:

1
2
3
4
5
6
7
8
9
10
11
#include <termios.h> 

speed_t cfgetispeed(const struct termios *termptr);
speed_t cfgetospeed(const struct termios *termptr);

// Both return: baud rate value

int cfsetispeed(struct termios *termptr, speed_t speed);
int cfsetospeed(struct termios *termptr, speed_t speed);

// Both return: 0 if OK, −1 on error

行控制函数

下列4个函数提供了终端设备的行控制能力:

1
2
3
4
5
6
7
8
9

#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);

// All four return: 0 if OK, −1 on error

tcdrain函数等待所有输出都被传递。
tcflow函数用于对输入和输出流控制进行控制。
tcflush函数冲洗(抛弃)输入缓冲区(其中的数据是终端驱动程序已接收到,但用户程序尚未读取的)或输出缓冲区(其中的数据是用户程序已经写入,但尚未被传递的)。
tcsendbreak函数在一个指定的时间区间内发送连续的0值位流。

终端标识

历史上,在大多数UNIX系统版本中,控制终端的名字一直是/dev/tty。POSIX.1提供了个运行时函数,可用来确定控制终端的名字。

1
2
3
4
5
#include <stdio.h>

char *ctermid(char *ptr);

// Returns: pointer to name of controlling terminal on success, pointer to empty string on error

另外还有两个UNIX系统比较感兴趣的函数: isatty和 ttyname。如果文件描述符引用个终端设备,则 isatty返回真。 ttyname返回的是在该文件描述符上打开的终端设备的路径名

1
2
3
4
5
6
7
8
9
#include <unistd.h>

int isatty(int fd);

// Returns: 1 (true) if terminal device, 0 (false) otherwise

char *ttyname(int fd);

// Returns: pointer to pathname of terminal, NULL on error

规范模式

规范模式很简单:发一个读请求,当一行已经输入后,终端驱动程序即返回。以下几个条件造成读返回。

  1. 所请求的字节数已读到时,读返回。无需读一个完整的行。如果读了部分行,那么也不会丢失任何信息,下一次读从前一次读的停止处开始。
  2. 当读到一个行定界符时,读返回。
  3. 如果捕捉到信号,并且该函数不再自动重启,则读也返回

非规范模式

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

如前所述,规范模式很容易理解:系统每次至多返回一行。但在非规范模式下,系统如何知道在什么时候将数据返回给我们呢?如果它一次返回一个字节,那么系统开销就会过大。在启动读数据之前,往往不知道要读多少数据,所以系统不能总是一次返回多个字节解决方法是,当已读了指定量的数据后,或者已经超过了给定量的时间后,即通知系统返回。

这种技术使用了 termios结构中c_cc数组的两个变量:MIN和TIME。c_cc数组中的这两个元素的下标名为VMIN和VTIM。

MIN指定一个read返回前的最小字节数。TIME指定等待数据到达的分秒数(分秒为秒的1/10)。

终端窗口大小

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

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