UNIX环境高级编程(18-终端I/O)
本章主要介绍终端的相关概念,及一些修改终端操作的函数。
概念
工作模式
主要有以下两种工作模式:
- 规范模式(Canonical mode)输入处理。在此模式下,对于终端的输入以行为单位进行处理。每次读取最多返回一行。这是默认的模式。
- 非规范模式(Noncanonical mode)输入处理。输入字符不装配成行,一些特殊字符(如Ctrl+D)以不会进行处理。
这两种模式在后面会有详细解释。
终端特性
在结构termios
中定义了终端设备全部特性的标志,在其中又将各种标志进行分类,其结构大体如下:
struct termios {
tcflag_t c_iflag; /* input flags */
tcflag_t c_oflag; /* output flags */
tcflag_t c_cflag; /* control flags */
tcflag_t c_lflag; /* local flags */
cc_t c_cc[NCCS]; /* control characters */
};
输入标志通过终端驱动程序控制字符的输入(如剥除输入字节的第8位,允许输入奇偶校验),输出标志控制驱动程序输出(如将换行符转换为CR/LF),控制标志影响RS-232串行线(如忽略调制解调器的状态线),本地标志影响驱动程序和用户之间的接口(如开关回显)。
c_cc
数组则包含了所有可以更改的特殊字符。
相关标志及其说明见如下各图:
配置终端
主要有13个函数可以对终端进行操作,其中的tcgetattr
和tcsetattr
函数用于读取/设置上一节列出的各个标志,因此实际上终端有非常多的配置选项,各个函数之间的关系可以参考下图:
获得和设置终端属性
#include <termios.h>
// Both return: 0 if OK, −1 on error
int tcgetattr(int fd, struct termios *termptr);
int tcsetattr(int fd, int opt, const struct termios *termptr);
这两个函数用于检测和修改各种终端选项标志和特殊字符,它们都使用了前述的termios
结构用于获取和设置终端属性。另外,这两个函数只针对终端进行操作,因此fd
没有引用终端设备就会出错返回-1,且将errno设置为ENOTTY。
set函数的opt
参数指定新的属性的起作用时间,有如下几个选项:
- TCSANOW:立即改变
- TCSADRAIN:发送所有输出后才发生更改
- TCSAFLUSH:与TCSADRAIN相似,但是所有未读的输入都会被丢弃
注意:
set函数只要执行了一种所要求的动作就会返回成功,因此有必要在后面通过get函数检查是否所有的设置都生效了。
特殊输入字符
上一节提到c_cc
数组包含了特殊字符,下面就是这些特殊字符:
其中,c_cc subscript
列表示该字符对应的数组下标值,有了它可以方便地修改对应的特殊字符,如果想要禁止使用某个特殊字符,只需将其设置为fpathconf(fd, _PC_VDISABLE)
的返回值即可。示例代码如下:
#include "apue.h"
#include <termios.h>
int main()
{
struct termios term;
long vdisable;
if (isatty(STDIN_FILENO) == 0) {
err_quit("standard input is not a terminal device");
}
if ((vdisable = fpathconf(STDIN_FILENO, _PC_VDISABLE)) < 0) {
err_quit("fpathconf error");
}
if (tcgetattr(STDIN_FILENO, &term) < 0) {
err_sys("tcgetattr error");
}
term.c_cc[VINTR] = vdisable; /* 禁用INTR */
term.c_cc[VEOF] = 2; /* ctrl+B -> EOF */
if (tcsetattr(STDIN_FILENO, TCSAFLUSH, &term) < 0) {
err_sys("tcsetattr error");
}
return 0;
}
波特率函数
// Both return: baud rate value
speed_t cfgetispeed(const struct termios *termptr); /* 输入 */
speed_t cfgetospeed(const struct termios *termptr); /* 输出 */
// Both return: 0 if OK, −1 on error
int cfsetispeed(struct termios *termptr, speed_t speed);
int cfsetospeed(struct termios *termptr, speed_t speed);
这几个函数用于获取和设置输入/输出的波特率(位/秒)。速度值是形如B0、B50、B75……的常量值。
需要注意的是,速率值保存在termios
结构中,但是并没有规定其使用的字段,所以无法通过该结构直接获取或设置速率,而只能通过以上几个函数。
如果想使用get函数,在这之前需要先调用tcgetattr
获取当前的termios
结构变量,然后传入get函数中以获得速率。同理,在设置速率时,调用完set函数后,需要调用tcsetattr
函数以使该改变生效。
行控制函数
// All four return: 0 if OK, −1 on error
int tcdrain(int fd);
int tcflow(int fd, int action);
int tcflush(int fd, int queue);
int tcsendbreak(int fd, int duration);
这4个函数要求fd
引用的是终端设备,否则出错返回-1,且errno设置为ENOTTY。
tcdrain
函数等待所有输出都被传递.
tcflow
函数控制输入和输出流。由action
参数进行控制:
- TCOOFF:暂停输出(输出被挂起)
- TCOON:重启被挂起的输出
- TCIOFF:发送STOP字符,使终端停止向系统发送数据
- TCION:发送STRAT字符,使终端恢复发送数据
tcflush
函数冲洗(丢弃)输入缓冲区(终端驱动程序已收到但用户程序未读取的数据)或输出缓冲区(用户程序写入但未被传递的数据)。queue
参数决定哪个缓冲区中的数据被冲洗:
- TCIFLUSH:冲洗输入队列
- TCOFLUSH:冲洗输出队列
- TCIOFLUSH:冲洗两者
tcsendbreak
函数会在指定的时间内持续发送0值的位流。如果duration
参数为0,则持续0.25~0.5秒,否则持续时间根据实现的不同而不同。
终端标识
参考代码:https://gitee.com/maxiaowei/Linux/blob/master/apue/ch18/term_ctermid.c
#include <stdio.h>
// Returns: pointer to name of controlling terminal on success,
// pointer to empty string on error
char *ctermid(char *ptr);
该函数用于确定终端的名字(一般都是/dev/tty)。
当ptr
非空时,控制终端名会存放在该参数指向的数组中,数组的长度至少为L_ctermid
字节。无论ptr
是否为空,函数成功执行后都会返回指向终端名的指针(如果ptr
为空,则函数自己分配空间)。
#include <unistd.h>
// Returns: 1 (true) if terminal device, 0 (false) otherwise
int isatty(int fd);
// Returns: pointer to pathname of terminal, NULL on error
char *ttyname(int fd);
isatty
用于检查描述符是否引用的是终端设备。
ttyname
返回的是描述符打开的终端设备的路径名。
窗口大小
参考代码:https://gitee.com/maxiaowei/Linux/blob/master/apue/ch18/term_windowSize.c
内核为每个终端和伪终端都维护一个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的TIOCGWINSZ
和TIOCSWINSZ
命令,可以分别获取和设置该值。
当窗口大小改变时,前台进程组会收到SIGWINCH
信号。
两种模式
规范模式
这种模式比较简单,也是比较常用的模式。工作过程为:发送读请求,输入一行后,终端驱动程序返回。
该模式中,NL
、EOL
、EOL2
和EOF
被解释为行结束。另外,如果设置了终端标志ICRNL
且未设置IGNCR
,则CR
会被转化为NL
,从而造成读返回。
除此以外,还有一些情况会造成读返回:
- 读取到请求的字节数,则即使没有读取完整的一行,也会马上返回。下次读取会从前一次停止的地方继续读。
- 捕捉到信号,且函数不再自动重启(自动重启的详细介绍参考书10.5节)。
非规范模式
参考代码:https://gitee.com/maxiaowei/Linux/blob/master/apue/ch18/term_noncanonicalMode.c
通过关闭c_lflag
字段的ICANON
标志,可以使终端运行于非规范模式下。在该模式下,输入数据不装配成行,也不处理以下几个特殊字符:RASE
,KILL
,EOF
,NL
, EOL
, EOL2
, CR
, REPRINT
,STATUS
和 WERASE
。
由于不是以行为单位返回数据,因此需要指定一些参数来告诉系统何时返回数据。除了读取指定量的数据自动返回以外,通过设置c_cc数组中的MIN
和TIME
变量(下标分别为VMIN和VTIME),使得系统在超过给定时间后也会返回。
MIN
用于指定read返回前的最小字节数,TIME
指定等待数据到达的分秒数(分秒为0.1秒)。两者组合有如下4中情形:
- MIN>0,TIME>0:在第一个字节被接收时启动时长为TIME的定时器。若在超时前接收到了MIN个字节,则read返回MIN个字节,否则返回已接收到的字节数。如果在调用read之前已经有数据可用,那么调用read后返回的字节数就可能会>MIN。
- MIN>0,TIME==0:read在接收到MIN个字节前不会返回。
- MIN==0,TIME>0:调用read后立即启动定时器(注意与第一种情况的启动时机不同),在接到一个字节或定时器超时后,立即返回。因此read可能返回0(定时器超时)。
- MIN0,TIME0:有数据可用则返回要求的字节数,否则立即返回0。
4种情形总结如上表所示,nbytes
为read的第三个参数,即要求读取的字节数。