• 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的第三个参数,即要求读取的字节数。

  • 相关阅读:
    Pick-up sticks
    The Doors
    Intersecting Lines
    Segments
    TOYS
    Palindrome
    Distinct Substrings
    Milk Patterns
    Musical Theme
    JavaScript基于时间的动画算法
  • 原文地址:https://www.cnblogs.com/maxiaowei0216/p/14250344.html
Copyright © 2020-2023  润新知