美文网首页LinuxLinux学习之路
APUE读书笔记-18终端输入输出(1)

APUE读书笔记-18终端输入输出(1)

作者: QuietHeart | 来源:发表于2020-08-25 17:41 被阅读0次

1、简介

无论什么操作系统,对于终端输入输出的处理都是一个非常杂乱的领域。UNIX系统也是这样。一般在大多数编程手册中,终端输入输出是最长的部分。

对于UNIX系统,在1970s出现了一个分歧,System III开发了一套和version7不一样的终端函数集合。SystemIII形式的终端输入输出在System V中延续下来,而Version 7形式的成为基于BSD系统的标准。和信号处理一样,这两个不同的分歧在POSIX.1中不存在了。本章我们将要看到所有POSIX.1的终端相关函数以及一些平台特定的内容。

终端输入输出之所以很复杂,其一部分原因也是因为人们使用终端输入输出来做许多事情:终端,计算机之间的硬连接线,调制解调(modems),打印机等等。

译者注

原文参考

参考: APUE2/ch18lev1sec1.html

2、概要

模式相关

终端输入输出有两种模式:

  1. 通常模式(后面使用canonical模式来表示)的输入处理。这种模式下,终端的输入被做为一行来进行处理。终端的驱动在每次read请求的时候返回至多一行。
  2. 非通常的模式(后面使用非canonical模式)输入处理。输入的字符并不会积攒为一行来进行处理。

如果我们不做任何的特殊处理,那么canonical模式就是默认的。例如shell将标准输入重新定向到终端,我们使用read和write将标准输入拷贝到标准输出,那么终端处于canonical模式,每次read会返回至多一行。而像vi编辑器这样处理整个屏幕的应用程序,一般都使用非canonical模式,因为其命令可能为单个字符而不以行结束符结尾。这个编辑器也不想系统处理一些特殊字符,因为这些字符可能会与它们本身的一些命令相互冲突,例如,Control-D在终端中一般表示文件的结尾,而它同时在vi中也是一个命令,表示滚动半个屏幕。

version7和原来bsd形式的终端驱动有三种终端输入模式:

  1. cooked模式(输入会被收集成行并且特殊符号会被处理)
  2. raw模式(输入不会被收集成行也不会处理特殊符号)
  3. cbreak模式(输入会被收集成行并且一些特殊符号会被处理)

后面会给出一个函数tty_cbreak,这个函数可以将终端设置成为cbreak或者raw模式。

POSIX.1定义了11个特殊的输入字符,我们可以修改其中的9个。本文中我们已经使用到了其中的一些,例如文件结束符号(一般都是Control-D),以及挂起字符(一般为Control-Z),后面章节对应的参考资料中有相应的列表专门列出这些字符。

设备与驱动

我们可以假设在内核中终端设备被终端驱动所控制。如下图所示,每个终端设备都有一个输入队列和输出队列。

                        终端设备输入队列和输出队列的逻辑图形
     next character written            next character read
          by process                         by process
               |                                  ^
               |                                  |
+--------------v-+ if echo enabled +--------------|-+- - - -
|  output queue  |<----------------|  input queue   |       |
+-|--------------+                 +-^--------------+- - - -
  |                                  |<------MAX_INPUT------>
  v                                  |
next character to                  next character
transmit to device                 read from device

对于上面的图形,有几点需要考虑:

  1. 如果如果echo被激活,那么在输入队列和输出队列之间有一个(隐含的)连接。
  2. 输入队列的大小,MAX_INPUT,是有限的。当某一个特定设备的输入队列被填满的时候,系统行为依赖于它的实现。大多数UNIX系统这个时候会响铃(也就是使用echo显示响铃字符)。
  3. 还有一个输入限制,MAX_CANON,我们这里不展示。这个限制表示通常输入的一行的最大字节数目。
  4. 虽然输出队列的大小是有限的,但是并没有可以让程序中可以访问到的对应常数来定义那个大小,因为当输出队列被填满的时候,内核只是简单地将写入进程设置为睡眠状态,直到有空间可以使用。
  5. 我们将会看到刷新函数tcflush是如何刷新输入和输出队列的。类似地,当我们描述tcsetattr函数的时候,我们将会看到(仅在输出队列空的情况下)我们如何让系统改变终端设备的属性。我们也可以在改变终端属性的时候告诉系统忽略输入队列中的所有内容。(如果我们正在改变输入属性或者在通常模式下进行切换,我们会想要这样做的,这样,之前输入的字符就不会在错误的模式下被解释了)。

模块 terminal line discipline

多数UNIX系统在一个被叫做 terminal line discipline 的模块下面执行通常模式处理。我们可以将这个模块想像成一个位于内核通用的read、write函数与实际设备驱动之间的盒子。参见下图。

       Terminal line discipline

          user process
               ^
               |
+--------------|-------------+
|              |             |
|    +---------v--------+    |
|    |  read and write  |    |
|    |     functions    |    |
|    +--------------^---+    |
|          |        |        |
|          |        |        |
|          |        |        |
|    +-----v------------+    |
|    |     terminal     |    | kernel
|    |  line discepline |    |
|    +--------------^---+    |
|          |        |        |
|          |        |        |
|          |        |        |
|    +-----v------------+    |
|    |     terminal     |    |
|    |  device driver   |    |
|    +---------^--------+    |
|              |             |
+--------------|-------------+
               |
               v
         actual device

注意这个图形和前面"A stream with a processing module"的类似之处。我们将会在后面我们讨论伪终端时回到这个图形。

终端设备属性

我们能够检查以及改变的所有终端设备特性,都存放在一个termios结构中。这个结构定义在头文件<termios.h>里面,我们这一章都会使用这个结构。

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 */
};

大体来说,

  • input flags 控制终端设备驱动的输入字符(去掉输入的第8位,输入等值检测等)。
  • output flags 控制驱动输出(进行输出处理,将换行符号映射成CR/LF等等)。
  • control flags 影响RS-232串口(忽略modem状态行,每个字符有一或两个停止位等)。
  • local flags 影响驱动和用户之间的接口(echo与否,擦除字符,终端是否发起信号,用于后台输出的作业控制停止信号等)。

以上标记对应的 tcflag_t 类型应该足够大,以容纳每个flag值,并且它经常被定义成unsigned int或者unsigned long。

  • 数组 c_cc 包含所有我们能够改变的特殊字符。

    数组数目NCCS是这个数组中的元素数目,大概为15到20之间(因为大多数UNIX系统的实现支持11个以上的POSIX定义的特殊字符)。

cc_t 类型有足够的空间可以容纳每个特殊字符,一般为unsigned char类型。

SystemV比POSIX标准要早,它已经有一个头文件,名字叫<termio.h>,以及一个结构termio。所以,POSIX.1给这个名字添加了一个s,用以区分。

这里,给出了几个表用来列出所有我们可以改变的终端标记,通过修改这些标记,我们可以影响一个终端设备的特性。注意,尽管Single UNIX Specification定义了适用所有平台的公共子集,所有的实现也都有他们自己的额外的标记。所有这些额外的标记来自各自系统之间不同的历史。

列出的表格包含了

tcflag_t  c_iflag;    /* input flags */
tcflag_t  c_oflag;    /* output flags */
tcflag_t  c_cflag;    /* control flags */
tcflag_t  c_lflag;    /* local flags */

这四个标记可能的取值以及对应的含义。本书在后面详细地对这些标记进行讨论。表格内容太多,这里限于篇幅就不列出来了。

修改与查询终端设备特性

假设上面所有的选项都是可用的,我们如何检查和改变终端设备的这些特性呢?下表中就列出了Single UNIX Specification定义的用于操作这些终端设备的不同的函数。(所有的函数都是基本的POSIX标准的一部分。tcgetsid函数除外,这个函数是一个XSI扩展标准,我们在前面章节中讨论过tcgetpgrp, tcgetsid, 和 tcsetpgrp这三个函数)

                                                终端I/O函数

+-----------------------------------------------------------------------+
|  Function   |                       Description                       |
|-------------+---------------------------------------------------------|
| tcgetattr   | 获取属性 (termios 结构)                                 |
|-------------+---------------------------------------------------------|
| tcsetattr   | 设置属性 (termios 结构)                                 |
|-------------+---------------------------------------------------------|
| cfgetispeed | 获得输入速度                                            |
|-------------+---------------------------------------------------------|
| cfgetospeed | 获得输出速度                                            |
|-------------+---------------------------------------------------------|
| cfsetispeed | 设置输入速度                                            |
|-------------+---------------------------------------------------------|
| cfsetospeed | 设置输出速度                                            |
|-------------+---------------------------------------------------------|
| tcdrain     | 等待所有的输出被传输                                    |
|-------------+---------------------------------------------------------|
| tcflow      | 挂起传输或者接收                                        |
|-------------+---------------------------------------------------------|
| tcflush     | 刷新挂起的输入和/或输出                                |
|-------------+---------------------------------------------------------|
| tcsendbreak | 发送 BREAK 字符                                         |
|-------------+---------------------------------------------------------|
| tcgetpgrp   | 获取前台进程组ID                                        |
|-------------+---------------------------------------------------------|
| tcsetpgrp   | 设置前台进程组ID                                        |
|-------------+---------------------------------------------------------|
| tcgetsid    | 获取用于控制TTY(XSI扩展)的session leader的进程组ID    |
+-----------------------------------------------------------------------+

要注意的是,Single UNIX Specification没有使用ioctl来操作终端设备。相反,它使用以上的13个函数。原因是,用于终端设备的ioctl函数使用不同的数据结构用于它的最终参数,这取决于被执行的动作。这使得参数的类型检查,变得不太可能。

虽然只有13个函数用来操作终端设备,但是前面的两个函数(tcgetattr和tcsetattr)操作了几乎70多种不同的标记,处理终端设备的复杂之处在于终端设备中大量的可用选项以及确定对于一个特定的设备,哪个选项是需要的(是一个终端,还是modem,是打印机,还是别的什么)。

前面列出来的13个函数之间的关系,参见下面的图形。

                          终端设备相关函数之间的关系图
                      ^                ^
         cfsetispeed  |  cfsetospeed   |
             |   cfgetispeed  |   cfgetospeed
             |        |       |        |
        /+---v------------+---v------------+
struct / |input baud rate |output baud rate|
 termios +----------------+----------------+                                        foreground
       \ |                                 |                                    process group ID
        \+-------------------------^-------+      line control functions         ^      ^
                 |                 |             |      |       |      |         |      |      |
            tcsetattr              |      tcsendbreak   |   tcflush    |         |      |  tcsetpgrp
                 |                 |             |  tcdrain     |   tcflow       |  tcgetpgrp  |
                 |             tcgetattr         |      |       |      |     tcgetsid   |      |
                 |                 |             |      |       |      |         |      |      |
         +-------v-------------------------------v------v-------v------v-----------------------v----+
         |                  terminal line displine / terminal device driver                         |
         +------------------------------------------------------------------------------------------+

POSIX.1没有指定将波特率(baud rate)信息存放在termios结构中的哪里:因为这是一个实现的细节。有些系统,例如Linux和Solaris,会将这个信息存放在c_cflag域。基于BSD的系统,例如FreeBSD和Mac OS X在这个结构中有两个独立的域:一个用来存放输入速度,一个用来存放输出速度。

译者注

原文参考

参考: APUE2/ch18lev1sec2.html

相关文章

网友评论

    本文标题:APUE读书笔记-18终端输入输出(1)

    本文链接:https://www.haomeiwen.com/subject/lbpnjktx.html