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数组则包含了所有可以更改的特殊字符。

相关标志及其说明见如下各图:

c_cflag terminal flags

c_iflag terminal flags

c_lflag terminal flags

c_oflag terminal flags

配置终端

主要有13个函数可以对终端进行操作,其中的tcgetattrtcsetattr函数用于读取/设置上一节列出的各个标志,因此实际上终端有非常多的配置选项,各个函数之间的关系可以参考下图:

Relationships among the terminal-related functions

获得和设置终端属性

#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数组包含了特殊字符,下面就是这些特殊字符:

Summary of special terminal input characters

其中,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的TIOCGWINSZTIOCSWINSZ命令,可以分别获取和设置该值。

当窗口大小改变时,前台进程组会收到SIGWINCH信号。

两种模式

规范模式

这种模式比较简单,也是比较常用的模式。工作过程为:发送读请求,输入一行后,终端驱动程序返回。

该模式中,NLEOLEOL2EOF被解释为行结束。另外,如果设置了终端标志ICRNL且未设置IGNCR,则CR会被转化为NL,从而造成读返回。

除此以外,还有一些情况会造成读返回:

  • 读取到请求的字节数,则即使没有读取完整的一行,也会马上返回。下次读取会从前一次停止的地方继续读。
  • 捕捉到信号,且函数不再自动重启(自动重启的详细介绍参考书10.5节)。

非规范模式

参考代码:https://gitee.com/maxiaowei/Linux/blob/master/apue/ch18/term_noncanonicalMode.c

通过关闭c_lflag字段的ICANON标志,可以使终端运行于非规范模式下。在该模式下,输入数据不装配成行,也不处理以下几个特殊字符:RASEKILLEOFNLEOLEOL2CRREPRINTSTATUSWERASE

由于不是以行为单位返回数据,因此需要指定一些参数来告诉系统何时返回数据。除了读取指定量的数据自动返回以外,通过设置c_cc数组中的MINTIME变量(下标分别为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。

Four cases for noncanonical input

4种情形总结如上表所示,nbytes为read的第三个参数,即要求读取的字节数。

原文地址:https://www.cnblogs.com/maxiaowei0216/p/14250344.html