1.终端I/O的工作方式
终端I/O有两种不同的工作方式:
规范方式输入处理。在这种方式中,终端输入以行为单位进行处理。对于每个读要求,终端驱动程序最多返回一行。
非规范方式输入处理。输入字符不以行为单位进行装配。
如果不作特殊处理,则默认方式是规范方式。
V7和BSD类的终端驱动程序支持三种终端输入方式:
精细加工方式(输入装配成行,并对特殊字符进行处理);
原始方式(输入不装配成行,也不对特殊字符进行处理);
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:
BRKINT:接到BREAK时产生SIGINT;
ICRNL:将输入的CR转换为NL;
IGNBRK:忽略BREAK条件;
IGNCR:忽略CR;
IGNPAR:忽略奇偶错字符;
IMAXBEL:在输入队列空时振铃;
INLCR:将输入的NL转换为CR;
INPCK:打开输入奇偶校验;
ISTRIP:剥除输入字符的第8位;
IUCLC:将输入的大写字符转换成小写字符(仅SVR4);
IXANY:使任一字符都重新起动输出;
IXOFF:使起动/停止输入控制流起作用;
IXON:使起动/停止输出控制流起作用;
PARMRK:标记奇偶错;
c_oflag:
BSDLY:退格延迟屏蔽(仅SVR4);
CRDLY:CR延迟屏蔽(仅SVR4);
FFDLY:换页延迟屏蔽(仅SVR4);
NLDLY:NL延迟屏蔽(仅SVR4);
OCRNL:将输出的CR转换为NL(仅SVR4);
OFDEL:填充符为DEL,否则为NUL(仅SVR4);
OFILL:对于延迟使用填充符(仅SVR4);
OLCUC:将输出的小写字符转换为大写字符(仅SVR4);
ONLCR:将NL转换为CR-NL;
ONLRET:NL执行CR功能(仅SVR4);
ONOCR:在0列不输出CR(仅SVR4);
ONOEOT:在输出中删除EOT字符(仅4.3+BSD);
OPOST:执行输出处理;
OXTABS:将制表符扩充为空格(仅4.3+BSD);
TABDLY:水平制表符延迟屏蔽(仅SVR4);
VTDLY:垂直制表符延迟屏蔽(仅SVR4);
c_cflag:
CCTS_OFLOW:输出的CTS流控制(仅4.3+BSD);
CIGNORE:忽略控制标志(仅4.3+BSD);
CLOCAL:忽略解制解调器状态行;
CREAD:启用接收装置;
CRTS_IFLOW:输入的RTS流控制(仅4.3+BSD);
CSIZE:字符大小屏蔽;
CSTOPB:送两个停止位,否则为1位;
HUPCL:最后关闭时断开;
MDMBUF:经载波的流控输出(仅4.3+BSD);
PARENB:进行奇偶校;
PARODD:奇校,否则为偶校;
c_lflag:
ALTWERASE:使用替换WERASE算法(仅4.3+BSD);
ECHO:进行回送;
ECHOCTL:回送控制字符为^(char);
ECHOE:可见擦除符;
ECHOK:回送kill符;
ECHOKE:kill的可见擦除;
ECHONL:回送NL;
ECHOPRT:硬拷贝的可见擦除方式;
FLUSHO:刷清输出;
ICANON:规范输入;
IEXTEN:使扩充的输入字符处理起作用;
ISIG:使终端产生的信号起作用;
NOFLSH:在中断或退出键后不刷清;
NOKERNINFO:STATUS不使内核输出(仅4.3+BSD);
PENDIN:重新打印;
TOSTOP:对于后台输出发送SIGTTOU;
XCASE:规范大/小写表示(仅SVR4);
所有列出的选择标志(除屏蔽标志外)都用一或多位表示,而屏蔽标志则定义多位。屏蔽标志有一个定义名,每个值也有一个名字。例如,为了设置字符长度,首先用字符长度屏蔽标志CSIZE将表示字符长度的位清0,然后设置下列值之一:CS5、CS6、CS7或CS8。由SVR4支持的6个延迟值也有屏蔽标志:BSDLY、CRDLY、FFDLY、NLDLY、TABDLY和VTDLY。
各个标志的含义参考: https://www.gwduan.com/web/computer/history/unix-prog/terminal.html#special-char
4.POSIX终端I/O函数:
Fcn | Description |
---|---|
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可以指定为下列常数中的一个:
TCSANOW:更改立即发生;
TCSADRAIN:发送了所有输出后更改才发生。若更改输出参数则应使用此选择项。
TCSAFLUSH:发送了所有输出后更改才发生。更进一步,在更改发生时未读的所有输入数据都被删除(刷清)。
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参数应当是下列四个值之一:
TCOOFF:输出被挂起;
TCOON:以前被挂起的输出被重新起动;
TCIOFF:系统发送一个STOP字符。这将使终端设备暂停发送数据;
TCION:系统发送一个START字符。这将使终端恢复发送数据。
tcflush函数刷清(抛弃)输入缓存(终端驱动程序已接收到,但用户程序尚未读)或输出缓存(用户程序已经写,但尚未发送)。queue参数应当是下列三个常数之一:
TCIFLUSH:刷清输入队列;
TCOFLUSH:刷清输出队列;
TCIOFLUSH:刷清输入、输出队列;
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.规范方式
规范方式发一个读请求,当一行已经输入后,终端驱动程序即返回。许多条件造成读返回:
所要求的字节数已读到时读即返回。无需读一个完整的行。如果读了部分行,那么也不会丢失任何信息―下一次读从前一次读的停止处开始。
当读到一个行定界符时,读返回。在规范方式中,下列字符被解释为“行结束”:NL、EOL、EOL2和EOF。另外,如若已设置ICRNL,但未设置IGNCR,则CR字符的作用与NL字符一样,所以它也终止一行。在这五个行定界符中,其中只有一个EOF符在终端驱动程序对其进行处理后即被删除。其他四个字符则作为该行的最后一个字符返回调用者。
如果捕捉到信号而且该函数并不自动再起动,则读也返回。
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为分秒)。有下列四种情形:
情形A:MIN > 0, TIME > 0。TIME说明一个字节间的计时器,在接到第一个字节时才起动它。在该计时器超时之前,若已接到MIN个字节,则read返回MIN个字节。如果在接到MIN个字节之前,该计时器已超时,则read返回已接收到的字节(因为只有在接到第一个字节时才起动,所以在计时器超时时,至少返回1个字节)。在这种情形中,在接到第一个字节之前,调用者阻塞。如果在调用read时数据已经可用,则这如同在read后,数据立即被接收到一样。
情形B:MIN > 0 , TIME = = 0。已经接到了MIN个字节时,read才返回。这可以造成read无限期的阻塞。
情形C:MIN = = 0,TIME > 0。TIME指定了一个调用read时起动的读计时器。(与情形A相比较,两者是不同的)。在接到1个字节或者该计时器超时时,read即返回。如果是计时器超时,则read返回0。
情形D :MIN = = 0,TIME = = 0。如果有数据可用,则read最多返回所要求的字节数。如果无数据可用,则read立即返回0。
在所有这些情形中,MIN只是最小值。如果程序要求的数据多于MIN个字节,那么它可能能接收到所要求的字节数。这也适用于MIN = = 0的情形A和B。
11.break方式和raw方式
对cbreak方式的定义是:
非规范方式。这种方式不对某些输入特殊字符进行处理。这种方式仍对信号进行处理,所以用户可以键入任一终端产生的信号。调用者应当捕捉这些信号,否则这种信号就可能终止程序,并且终端将仍处于cbreak方式。作为一般规则,在编写更改终端方式的程序时,应当捕捉大多数信号,以便在程序终止前恢复终端方式;
关闭回送(ECHO)标志;
每次输入一个字节。为此将MIN设置为1,将TIME设置为0。至少有一个字节可用时,read再返回。
对原始方式的定义是:
非规范方式。另外,还关闭了对信号产生字符(ISIG)和扩充输入字符的处理(IEXTEN)。关闭BRKINT,这样就使BREAK字符不再产生信号;
关闭回送(ECHO)标志;
关闭ICRNL、INPCK、ISTRIP和IXON标志。于是:不再将输入的CR字符变换为NL(ICRNL)、使输入奇偶校验不起作用(INPCK)、不再剥离输入字节的第8位(ISTRIP)、不进行输出流控制(IXON);
8位字符(CS8),不产生奇偶位,不进行奇偶性检测(PARENB);
禁止所有输出处理(OPOST);
每次输入一个字节(MIN = 1,TIME = 0)。
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 */
};
此结构的作用是:
用ioctl的TIOCGWINSZ命令可以取此结构的当前值。
用ioctl的TIOCSWINSZ命令可以将此结构的新值存放到内核中。如果此新值与存放在内核中的当前值不同,则向前台进程组发送SIGWINCH信号。此信号的系统默认动作是忽略。
除了存放此结构的当前值以及在此值改变时产生一个信号以外,内核对该结构不进行任何其他操作。对结构中的值进行解释完全是应用程序的工作。
提供这种功能的目的是,当窗口大小发生变化时通知应用程序(例如vi编辑程序)。应用程序接到此信号后,它可以取得窗口大小的新值,然后重绘屏幕。
网友评论