终端I/O.
终端I/O
- 一、综述
- 二、特殊输入字符
- 三、获得和设置终端属性
- 四、stty命令
- 五、波特率函数
- 六、行控制函数
- 七、终端标识
- 八、规范模式与非规范模式
- 1.加工模式、cbreak模式以及原始模式
- 九、终端窗口大小
- 十、termcap、terminfo和curses
一、综述
终端I/O有两种不同的工作模式:
-
规范模式输入处理。在这种模式中,对终端输入以行为单位进行处理。对于每个读请求,终端驱动程序最多返回一行。
-
非规范模式输入处理。输入字符不装配成行。
POSIX.1定义了11个特殊字符,其中9个可以更改。例如文件结束符(Ctrl+D)和挂起字符(Ctrl+Z)。
终端设备是由通常位于内核中的终端驱动程序控制的。每个终端设备都有一个输入队列和一个输出队列,如下图所示:
许多UNIX系统在一个称为终端行规程的模块 中进行全部的规范处理。可以将这个模块设想成一个盒子,位于内核通用读、 写函数和实际设备驱动程序之间,如下图所示:
终端设备特性包含在头文件为<termios.h>
如下结构中:
struct termios{
tcflag_t c_iflag;
tcflag_t c_oflag;
tcflag_t c_cflag;
tcflag_t c_lflag;
cc_t c_cc[NCCS];
}
//c_cc数组包含了所有可以更改的特殊字符,NCCS是该数组中元素的数量(15~20)
//cc_t类型的长度足以保存每个特殊字符,典型的是unsigned char。
//类型tcflag_t的长度足以保存每个标志值,它经常被定义为unsigned int或者 unsigned long。
输入标志通过终端设备驱动程序控制字符的输入,输出标志则控制驱动程序输出,控制标志影响RS-232串行线,本地标志影响驱动程序和用户之间的接口。
更改影响终端设备特性的标志如下所示:
对终端设备进行操作的各个函数:
函数关系示意图:
二、特殊输入字符
POSIX.1定义了11个在输入时要特殊处理的字符,如下图所示:
-
9个字符的值可以任意更改。
-
不能更改的两个特殊字符是换行符和回车符(分别是\n和\r),也可能是STOP和 START字符(依赖于实现)。
-
更改只需要修改
termios
结构中c_cc
数组的相应项。该数组中的元素都用名字作为下标进行引用,每个名字都以字母V 开头。 -
禁止使用这些字符,只虚将
c_cc
数组中的某项设置为_POSIX_VDISABLE
的值,则禁止使用相应特殊字符。
三、获得和设置终端属性
获得和设置termios
结构,调用tcgetattr和tcsetattr
函数,可以 检测和修改各种终端选项标志和特殊字符,使终端按我们所希望的方式进行操作。
#include <termios.h>
int tcgetattr(int fd, struct termios *termptr);
int tcsetattr(int fd, int opt, const struct termios *termptr);
//termptr:指向termios结构的指针,返回当前终端的属性,或者设置该终端的属性。
//若fd没有引用终端设备则出错返回-1,errno设置为ENOTTY。
//两个函数的返回值:若成功,返回0;若出错,返回-1
opt
指定什么时候新的终端属性才起作用:
-
TCSANOW
更改立即发生。 -
TCSADRAIN
发送了所有输出后更改才发生。若更改输出参数则应使用此 选项。 -
TCSAFLUSH
发送了所有输出后更改才发生。更进一步,在更改发生时未 读的所有输入数据都被丢弃(冲洗)。
-
tcsetattr函数执行了任意一种所要求的动作,即使未能执行所有要求的动作,也返回OK(表示成功。若该函数返回OK,需要检查该函数是否执行了所有要求的动作,意味着,在调用
tcsetattr
设置所希望的属性后,需调用tcgetattr
,然后将实际终端属性与所希望的属性相比较,以检测两者是否有区别。-
终端第一次被打开时,其属性视具体情况而定。一些系统可能会将终端 属性初始化为具体实现所定义的值,另一些系统可能会保留并使用最后一次使 用终端时的属性值。
- 通过打开一个带有
O_TTY_INIT
标志的驱动设 备,可以确认终端的行为是否遵循标准,在调用tcgetattr
时,确保初始化termios结构中的任何非标准部分,使得在修改属性和调用tcgetattr时,终端的 表现符合预期。
- 通过打开一个带有
-
四、stty命令
使用stty命令进行检查和更改所有属性,stty -a
显示终端的所有选项:
若在选项名前有一个连字符,表示该选项禁用。最后4行显示各终端特殊字符的当前设置。第1行显示当前终端窗口的行数和列数。
五、波特率函数
波特率指的是“位/ 秒”
,大多数终端设备对输入和输出使用同一波特率,但是只要硬件许可,可以将它们设置为两个不同值。
#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
,则调制解调器的控制线就不再起作用。 -
大多数系统定义了另外的波特率值,如
B57600以及B115250
。 -
使用这些函数时,必须认识到输入、输出波特率是存储在设备的termios结构中的。在调用两个cfget函数中的任意一个之前,要先用 tcgetattr获得设备的termios结构。
-
在调用两个cfset函数中的任意一个 之后,要做的就是在termios结构中设置波特率。
更改影响到设备,应 当调用tcsetattr
函数。即使所设置的两个波特率中的任意一个出错,在调用 tcsetattr
之前可能也不会发现这个错误。
六、行控制函数
下列函数提供了终端设备的行控制能力。
#include <termios.h>
int tcdrain(int fd);
int tcflow(int fd, int action);
int tcflush(int fd, int queue);
int tcsendbreak(int fd, int duration);
//要求参数fd引用一 个终端设备,否则出错返回-1,errno设置为ENOTTY。
// 4个函数的返回值:若成功,返回0;若出错,返回-1
- tcdrain 函数等待所有输出都被传递。
-
tcflow 函数用于对输入和输出流控制 进行控制。
action参数必定是下列4个值之一:
1. TCOOFF 输出被挂起。
2. TCOON 重新启动以前被挂起的输出。
3. TCIOFF 系统发送一个STOP字符,这将使终端设备停止发送数据。
4. TCION 系统发送一个START字符,这将使终端设备恢复发送数据。-
tcflush函数冲洗(抛弃)输入缓冲区(其中的数据是终端驱动程序已接收 到,但用户程序尚未读取的)或输出缓冲区(其中的数据是用户程序已经写 入,但尚未被传递的)。
queue参数必定是下列3个常量之一:
1. TCIFLUSH 冲洗输入队列。
2. TCOFLUSH 冲洗输出队列。
3. TCIOFLUSH 冲洗输入队列和输出队列。 -
tcsendbreak
函数在指定的时间区间内发送连续的0值位流。若duration
参数为0,则此种传递延续0.25~0.5
秒。POSIX.1说明若duration非0,则传递时间 依赖于实现。
-
-
七、终端标识
控制终端的名字一直是/dev/tty
。POSIX.1提供了一个运行时函数,可用来确定控制终端的名字。
#include <stdio.h>
char *ctermid(char *ptr);
//返回值:若成功,返回指向控制终端名的指针;若出错,返回指向空字符串的指针
-
若ptr非空,代表个指针,指向长度至少为
L_ctermid
(stdio.h定义)字节的数组,进程的控制终端名存储在该数组中。 -
若ptr是空指针,则该函数为数组(通常作为静态变量)分配空间,进程的控制终端名存储在该数组中。
-
这两种情况中,该数组的起始地址都被作为函数值返回。
-
无法确定调用者的缓冲区大小,所以也就不能防止过度使 用该缓冲区。
#include <unistd.h>
//如果文件描 述符引用一个终端设备,则isatty返回真。
int isatty(int fd);
//返回值:若为终端设备,返回1(真);否则,返回0(假)
//ttyname返回的是在该文件描述符上打开的终端设备的路径名。
char *ttyname(int fd);
//返回值:指向终端路径名的指针;若出错,返回NULL
八、规范模式与非规范模式
规范模式:
-
通过设定
ICANON
标志打开规范模式输入。- 发一个读请求,当一行已经输入后,终端驱动程序即返
区分规范描述符输入:
- 发一个读请求,当一行已经输入后,终端驱动程序即返
-
输入被装配成行,通过如下几种行结束符终结:
NL、EOL、EOL2、EOF或者CR
。 -
打开了行编辑功能。可以修改当前行中的输入。像ERASE、KILL可用。设定IEXTEN标志,WEASE也可用。
-
若设定IEXTEN标志,则REPRINT和LNEXT字符都可用。
-
规范模式存在一行完整输入,终端的read()才返回。
非规范模式:
-
应用程序在用户没有提供终止符时也需从终端中读取字符,非规范模式用于此。
-
通过关闭
termios
结构中c_lflag
字段的ICANON
标志来指定非规范模式。- 非规范模式不会处理特殊处理。
-
TIME(十分之一秒为单位指定超时时间)和MIN(指定被读取字节数的最小值),若read满足下列要求会返回可用字节数和所请求的字节数中较小的哪个值。
-
MIN0,TIME0(轮询读取)。
-
MIN>0,TIME==0(阻塞式读取)。
-
MIN==0,TIME>0(带有超时机制的读操作)。
-
MIN>0,TIME>0(既有超时机制又有最小读取字节数的要求)。
1.加工模式、cbreak模式以及原始模式
中断驱动程序能够以这3三种方式处理输入,如图所示三种区别。
九、终端窗口大小
大多数UNIX系统都提供了一种跟踪当前终端窗口大小的方法,在窗口大小 发生变化时,使内核通知前台进程组。内核为每个终端和伪终端都维护了一个 winsize结构:
struct winsize {
unsigned short ws_row;
unsigned short ws_col;
unsigned short ws_xpixel;
unsigned short ws_ypixel;
};
此结构的规则如下:
-
用
ioctl
的TIOCGWINSZ
命令可以取此结构的当前值。 -
用
ioctl
的TIOCSWINSZ
命令可以将此结构的新值存储到内核中。果此 新值与存储在内核中的当前值不同,则前台进程组会收到SIGWINCH
信号。 -
除了存储此结构的当前值以及在此值改变时产生一个信号以外,内核对该 结构不进行任何其他操作。对结构中的值进行解释完全是应用程序的工作。
提供这种功能的目的是,当窗口大小发生变化时应用程序能够得到通知 (如vi编辑器)。应用程序接收此信号后,可以获取窗口大小的新值,然后重 绘屏幕。
例:打印当前窗口大小,然后休眠。窗口改变时,程序捕捉SIGWINCH信号,然后打印新的大小。
#include "apue.h"
#include <termios.h>
#ifndef TIOCGWINSZ
#include <sys/ioctl.h>
#endif
static void pr_winsize(int fd)
{
struct winsize size;
if (ioctl(fd, TIOCGWINSZ, (char *) &size) < 0)
err_sys("TIOCGWINSZ error");
printf("%d rows, %d columns\n", size.ws_row, size.ws_col);
}
static void sig_winch(int signo)
{
printf("SIGWINCH received\n");
pr_winsize(STDIN_FILENO);
}
int main(void)
{
if (isatty(STDIN_FILENO) == 0)
exit(1);
if (signal(SIGWINCH, sig_winch) == SIG_ERR)
err_sys("signal error");
pr_winsize(STDIN_FILENO); /* print initial size */
for ( ; ; ) /* and sleep forever */
pause();
}
十、termcap、terminfo和curses
-
termcap
:是终端能力,它涉及文本文件/etc/termcap
和一套读此文件的例程,包含对各种终端说明。 -
terminfo
:terminfo库中,终端说明基本上都是文本说明 的编译版本,在运行时易于被快速定位。 -
curses
:curses库提供了很多函数,用来设置 原始模式、设置cbreak模式、打开和关闭回显等。