Duangw

终端I/O

索引:

  1. 终端I/O的工作方式
  2. termios结构
  3. 终端特殊输入字符
  4. POSIX终端I/O函数
  5. tcgetattr和tcsetattr
  6. 波特率函数
  7. 行控制函数
  8. 终端标识函数
  9. 规范方式
  10. 非规范方式
  11. break方式和raw方式
  12. 终端窗口

1.终端I/O的工作方式

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

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

如果不作特殊处理,则默认方式是规范方式。

V7和BSD类的终端驱动程序支持三种终端输入方式:

  1. 精细加工方式(输入装配成行,并对特殊字符进行处理);
  2. 原始方式(输入不装配成行,也不对特殊字符进行处理);
  3. cbreak方式(输入不装配成行,但对某些特殊字符进行处理)。

 

2.termios结构

struct termios {
    tcflag_t c_iflag; /* Input modes */
    tcflag_t c_oflag; /* Output modes */
    tcflag_t c_cflag; /* Control modes */
    tcflag_t c_lflag; /* Local modes */
    cc_t c_cc[NCCS]; /* Control characters */
};

各个字段的选项如下(不是所有UNIX系统都支持):

c_iflag:
c_oflag:
c_cflag:
c_lflag:

所有列出的选择标志(除屏蔽标志外)都用一或多位表示,而屏蔽标志则定义多位。屏蔽标志有一个定义名,每个值也有一个名字。例如,为了设置字符长度,首先用字符长度屏蔽标志CSIZE将表示字符长度的位清0,然后设置下列值之一:CS5、CS6、CS7或CS8。由SVR4支持的6个延迟值也有屏蔽标志:BSDLY、CRDLY、FFDLY、NLDLY、TABDLY和VTDLY。

各个标志的含义如下:

 

3.终端特殊输入字符

POSIX.1定义了11个在输入时作特殊处理的字符。SVR4另外加了6个特殊字符,4.3+BSD则加了7个。见下表:

字符 说明 c_cc下标 起作用,由: 典型值 POSIX.1 SVR4 4.3+BSD
字段 标志 扩充
CR 回车 不能更改 c_lflag ICANON \r YES
DISCARD 擦除输出 VDISCARD c_lflag IEXTEN ^O YES YES
DSUSP 延迟挂起(SIGTSTP) VDUSP c_lflag ISIG ^Y YES YES
EOF 文件结束 VEOF c_lflag ICANON ^D YES
EOL 行结束 VEOL c_lflag ICANON YES
EOL2 替换的行结束 VEOL2 c_lflag ICANON YES YES
ERASE 擦除字符 VERASE c_lflag ICANON ^H YES
INTR 中断信号(SIGINT) VINTR c_lflag ISIG ^?, ^C YES
KILL 擦行 VKILL c_lflag ICANON ^U YES
LNEXT 下一个字列字符 VLNEXT c_lflag IEXTEN ^V YES YES
NL 新行 不能更改 c_lflag ICANON \n YES
QUIT 退出信号(SIGQUIT) VQUIT c_lflag ISIG ^\ YES
REPRINT 再打印全部输入 VREPRINT c_lflag ICANON ^R YES YES
START 恢复输出 VSTART c_lflag IXON/IXOFF ^Q YES
STATUS 状态要求 VSTATUS c_lflag ICANON ^T YES
STOP 停止输出 VSTOP c_lflag IXON/IXOFF ^S YES
SUSP 挂起信号(SIGTSTP) VSUSP c_lflag ISIG ^Z YES
WERASE 擦除字 VWERASE c_lflag ICANON ^W YES YES

我们称这些字符为特殊输入字符,但是其中有两个字符,STOP和START(Ctrl-S和Ctrl-Q)在输出时也对它们进行特殊处理。这些字符中的大多数在被终端驱动程序识别并进行特殊处理后都被丢弃,并不将它们传送给执行读终端操作的进程。例外的字符是新行符(NL,EOL,EOL2)和回车符(CR)。详细说明如下:

需要为终端设备定义的另一个“字符”是BREAK。BREAK实际上并不是一个字符,而是在异步串行数据传送时发生的一个条件。

在POSIX.1的11个特殊字符中,可将其中9个更改为几乎任何值。不能更改的两个特殊字符是新行符和回车符(\n和\r),有些实现也不允许更改STOP和START字符。为了进行修改,只要更改termios结构中c_cc数组的相应项,该数组中的元素都用名字作为下标进行引用,每个名字都以字母V开头。

POSIX.1可选地允许禁止使用这些字符。若_POSIX_VDISABLE有效,则_POSIX_VDISABLE的值可存放在c_cc数组的相应项中以禁止使用该特殊字符。可以用pathconf和fpathconf函数查询此特征。

 

4.POSIX终端I/O函数

tcgetattr 取属性(termios结构);
tcsetattr 设置属性(termios结构);
cfgetispeed     得到输入速度;
cfgetospeed 得到输出速度;
cfsetispeed 设置输入速度;
cfsetospeed 设置输出速度;
tcdrain 等待所有输出都被传输;
tcflow 挂起传输或接收;
tcflush 刷清未决输入和/或输出;
tcsendbreak 送BREAK字符;
tcgetpgrp 得到前台进程组ID;
tcsetpgrp 设置前台进程组ID;

 

5.tcgetattr和tcsetattr

#include <termios.h>
int tcgetattr(int filedes, struct termios *termptr);
int tcsetattr(int filedes, int opt, const struct termios *termptr);
两个函数返回:若成功则为0,若出错则为-1

这两个函数都有一个指向termios结构的指针作为其参数,它们返回当前终端的属性,或者设置该终端的属性。因为这两个函数只对终端设备进行操作,所以若filedes并不引用一个终端设备则出错返回,errno设置为ENOTTY。

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

tcsetattr函数的返回值易于产生混淆。如果它执行了任意一种所要求的动作,即使未能执行所有要求的动作,它也返回0(表示成功)。如果该函数返回0,则我们有责任检查该函数是否执行了所有要求的动作。这就意味着,在调用tcsetattr设置所希望的属性后,需调用tcgetattr,然后将实际终端属性与所希望的属性相比较,以检测两者是否有区别。

 

6.波特率函数

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

#include <termios.h>
speed_t cfgetispeed(const struct termios *termptr);
speed_t cfgetospeed(const struct termios *termptr);
两个函数返回:波特率值

int cfsetispeed(struct termios *termptr, speed_t speed);
int cfsetospeed(struct termios *termptr, speed_t speed);
两个函数返回:若成功为0,出错为-1

两个cfget函数的返回值,以及两个cfset函数的speed参数都是下列常数之一:B50、B75、B110、B134、B150、B200、B300、B600、B1200、B1800、B2400、B4800、B9600、B19200或B38400。常数B0表示“挂断”。在调用tcsetattr时将输出波特率指定为B0,则调制解调器的控制线就不再起作用。

使用这些函数时,应当理解输入、输出波特率是存放在termios结构中的。在调用任一cfget函数之前,先要用tcgetattr获得设备的termios结构。与此类似,在调用任一cfset函数后,应将波特率设置到termios结构中。为使这种更改影响到设备,应当调用tcsetattr函数。如果所设置的波特率有错,则在调用tcsetattr之前,不会发现这种错误。

 

7.行控制函数

#include <termios.h>
int tcdrain(int filedes);
int tcflow(int filedes, int action);
int tcflush(int filedes, int queue);
int tcsendbreak(int filedes, int duration);
四个函数返回:若成功则为0,若出错则为-1

其中,参数filedes引用一个终端设备,否则出错返回,errno设置为ENOTTY。

tcdrain函数等待所有输出都被发送。

tcflow用于对输入和输出流控制进行控制。action参数应当是下列四个值之一:

tcflush函数刷清(抛弃)输入缓存(终端驱动程序已接收到,但用户程序尚未读)或输出缓存(用户程序已经写,但尚未发送)。queue参数应当是下列三个常数之一:

tcsendbreak函数在一个指定的时间区间内发送连续的0位流。若duration参数为0,则此种发送延续0.25~ 0.5秒之间。POSIX.1说明若duration非0,则发送时间依赖于实现。

 

8.终端标识函数

POSIX.1提供了一个运行时函数,可被调用来决定控制终端的名字:

#include <stdio.h>
char * ctermid(char *ptr);

如果ptr是非空,则它被认为是一个指针,指向长度至少为L_ctermid字节的数组,进程的控制终端名存放在该数组中。常数L_ctermid定义在<stdio.h>中。若ptr是一个空指针,则该函数为数组(通常作为静态变量)分配空间。同样,进程的控制终端名存放在该数组中。

在这两种情况中,该数组的起始地址被作为函数值返回。因为大多数UNIX系统都使用/dev/tty作为控制终端名,所以此函数的主要作用是帮助提高向其他操作系统的可移植性。

其他终端标识函数还有:

#include <unistd.h>
int isatty(int filedes);
返回:若为终端设备则为1(真),否则为0(假)

char *ttyname(int filedes);
返回:指向终端路径名的指针,若出错则为NULL

如果文件描述符引用一个终端设备,则isatty返回真,而ttyname则返回在该文件描述符上打开的终端设备的路径名。

 

9.规范方式

规范方式发一个读请求,当一行已经输入后,终端驱动程序即返回。许多条件造成读返回:

 

10.非规范方式

将termios结构中c_lflag字段的ICANON标志关闭就使终端处于非规范方式。在非规范方式中,输入数据不装配成行,不处理下列特殊字符:ERASE、KILL、EOF、NL、EOL、EOL2、CR、REPRINT、STATUS和WERASE。

在非规范方式下,由于不是每次返回一行,解决读的方法是:当已读了指定量的数据后,或者已经过了给定量的时间后,即通知系统返回。

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

MIN说明一个read返回前的最小字节数。TIME说明等待数据到达的分秒数(秒的1/10为分秒)。有下列四种情形:

在所有这些情形中,MIN只是最小值。如果程序要求的数据多于MIN个字节,那么它可能能接收到所要求的字节数。这也适用于MIN = = 0的情形A和B。

 

11.break方式和raw方式

对cbreak方式的定义是:

对原始方式的定义是:

 

12.终端窗口

内核为每个终端和伪终端保存一个winsize结构:

struct winsize {
    unsigned short ws_row; /* rows, in characters */
    unsigned short ws_col; /* columns, in character */
    unsigned short ws_xpixel; /* horizontal size, pixels */
    unsigned short ws_ypixel; /* vertical size, pixels */
};

此结构的作用是:

  1. 用ioctl的TIOCGWINSZ命令可以取此结构的当前值。
  2. 用ioctl的TIOCSWINSZ命令可以将此结构的新值存放到内核中。如果此新值与存放在内核中的当前值不同,则向前台进程组发送SIGWINCH信号。此信号的系统默认动作是忽略。
  3. 除了存放此结构的当前值以及在此值改变时产生一个信号以外,内核对该结构不进行任何其他操作。对结构中的值进行解释完全是应用程序的工作。

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