终端IO部分整体上读了两遍,感觉这一部分的内容又乱又碎,不太好理解。读完了之后,仍然感觉什么也没有学到。先做一个肤浅的记录,等到以后要用到的时候,再回来补充。
1.终端IO的工作方式
终端IO有两种不同的工作方式,规范方式输入处理和非规范方式输入处理:
(1)规范方式输入处理:终端以行为单位进行处理,对于每个读要求,终端驱动程序最多返回一行。 (2)非规范方式输入处理:输入字符不以行为单位进行处理。
终端设备是由位于内核中的终端驱动程序所控制的,每个终端设备有一个输入队列和一个输出队列。
2.对终端设备进行操作
关于终端IO的属性存放在一个 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]; /* special characters */
通过对这些数据成员的设置,可以来控制终端的属性。c_iflag 用来控制终端的输入属性,c_oflag 用来控制输出的属性,c_cflag 用来控制一些其他属性,c_lflag 用来控制驱动程序和用户之间的界面。c_cc 数组包含了所有可以更改的特殊字符,这在稍后会介绍到。
对终端设备的操作函数基本如下:
int tcgetattr(int fd, struct termios *termios_p); int tcsetattr(int fd, int optional_actions,const struct termios *termios_p); int tcsendbreak(int fd, int duration); int tcdrain(int fd); int tcflush(int fd, int queue_selector); int tcflow(int fd, int action); speed_t cfgetispeed(const struct termios *termios_p); speed_t cfgetospeed(const struct termios *termios_p); int cfsetispeed(struct termios *termios_p, speed_t speed); int cfsetospeed(struct termios *termios_p, speed_t speed);
我们可以通过这几个函数来操作终端的属性。tcgetattr 函数用来获取终端的属性,终端的属性通过第二个参数返回。tcsetattr 函数用来设置终端的属性。这里需要注意的是最后四个用来设置和获得波特率的函数,它们是建立在前面两个函数的基础之上的。也就是说,如果想要获得波特率,需要先调用tcgetattr函数来获得终端的属性,然后在将获得的终端的属性作为参数传递给 cfgetispeed 函数或 cfgetospeed 函数。同样,当需要设置波特率时,需要先将波特率在 termios 结构体中设置好,然后调用 tcsetattr 函数。
下面的一个例子用来禁止终端中的中断字符(即CTRL+C),并且将文件结束的字符更改为CTRL+B:
#include <termios.h> #include <stdio.h> #include <unistd.h> int main(void) { struct termios term; long vdisable; if(isatty(STDIN_FILENO)==0) { printf("standard input is not terminal device\n"); return -1; } if( (vdisable=fpathconf(STDIN_FILENO,_PC_VDISABLE))<0 ) { printf("fpathconf error\n"); return -1; } if(tcgetattr(STDIN_FILENO,&term)<0) { printf("tcgetattr error\n"); return -1; } term.c_cc[VINTR] = vdisable; term .c_cc[VEOF] = 2; if(tcsetattr(STDIN_FILENO,TCSAFLUSH,&term)<0) { printf("tcsetattr error\n"); return -1; } return 0; }
该程序先用 isatty 函数来测试 STDIN_FILENO 描述符所指向的文件是否是终端设备,如果测试的文件描述符是指向一个终端设备的话则返回1,否则返回0。
然后使用函数 fpathconf 获取系统中的 _PC_VDISABLE 的值,将这个值保存在 c_cc 数组中的相应位置就可以禁止使用这个位置所代表的特殊字符。
接着我们调用 tcgetattr 函数用来获取终端IO的属性,然后设置 c_cc 数组的 VINTR 的位置为 _PC_VDISABLE,表示禁止使用中断符号CTRL+C。而将VEOF的位置的值更改为2,即表示将文件的结束符号修改为CTRL+B,同理如果要修改为CTRL+A,则这个地方的值为1。
最后使用 tcsetattr 函数设置修改过的属性,使这些属性生效。tcsetattr 函数的第二个参数用来表示设置属性之后,这些属性生效的时间,这个参数有三种选择:
TCSANOW:更改立即生效 TCSADRAIN:发送了所有输出之后,更改才生效。如果更改输出参数,则应该使用此选项。 TCSAFLUSH:发送了所有输出之后,更改才生效。这里和上面一个的不同是,当更改发生时,未读的所有输入数据都被删除(丢弃)。
3.一些函数
(1)ctermid 函数用来返回控制终端的名字,一般是 /dev/tty。它的函数原型如下:
#include <stdio.h> char *ctermid(char *s);
它将控制终端的名字存放在由参数 s 指示的缓存中,如果 s 为空,则启动一个静态存储区,并将这个静态存储区的首地址返回给调用者。下面是 ctermid 函数的一个实现:
#include <stdio.h> #include <string.h> static char ctermid_name[L_ctermid]; char* ctermid(char* str) { if(str==NULL) { str = ctermid_name; } return strcpy(str,"/dev/tty"); //strcpy returns str }
(2)isatty 函数的一种实现如下:
#include <termios.h> #include <stdio.h> int isatty(int fd) { struct termios term; return (tcgetattr(fd,&term)!=-1); }
可以用下面的程序来进行测试 isatty 函数:
#include <termios.h> #include <stdio.h> int isatty(int fd) { struct termios term; return (tcgetattr(fd,&term)!=-1); } int main(void) { printf("fd 0:%s\n",isatty(0)?"tty":"not a tty"); printf("fd 1:%s\n",isatty(1)?"tty":"not a tty"); printf("fd 2:%s\n",isatty(2)?"tty":"not a tty"); return 0; }
将程序编译为 a.out,运行程序,输出结果:
fd 0:tty fd 1:tty fd 2:tty
如果将标准输入和标准出错重定向,按照下面的方式运行程序:
./a.out</etc/passwd 2>/tmp/t
输出结果如下:
fd 0:not a tty fd 1:tty fd 2:not a tty
(3)ttyname 返回在指定描述符上打开的终端设备的路径名,例如下面的一个程序可以演示一下它的使用:
#include <unistd.h> #include <stdio.h> int main(void) { printf("fd 0:%s\n",isatty(0)?ttyname(0):"not a tty"); printf("fd 1:%s\n",isatty(1)?ttyname(1):"not a tty"); printf("fd 2:%s\n",isatty(2)?ttyname(2):"not a tty"); return 0; }
4.终端窗口的大小
linux 系统提供了一个跟踪终端大小的功能,内核为每个终端或者是伪终端保存了一个 winsize 结构体,这个结构体中保存了当前终端大小的信息,这个结构体如下:
struct winsize { unsigned short ws_row; unsigned short ws_col; unsigned short ws_xpixel; unsigned short ws_ypixel; };
通过 ioctl 函数的 TIOCGWINSZ 命令可以取此结构体的当前值。当然也可以通过 ioctl 的 TIOCSWINSZ 命令可以将此结构体的新值存放到内核中,如果新值与存放在内核中的当前值不同,则会向前台进程组发送SIGWINCH信号,系统对此信号的默认处理是忽略该信号。当前终端窗口大小发生变化时,也会产生 SIGWINCH 信号。下面的程序演示了当终端的大小发生变化时,就打印当前终端的大小:
#include <signal.h> #include <termios.h> #ifndef TIOCGWINSZ #include <sys/ioctl.h> #endif #include <stdio.h> #include <stdlib.h> #include <unistd.h> static void pr_winsize(int fd) { struct winsize size; if(ioctl(fd,TIOCGWINSZ,(char*)&size)<0) { printf("TIOCGWINSZ error\n"); exit(-1); } 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); return; } int main(void) { if(isatty(STDIN_FILENO)==0) { exit(1); } if(signal(SIGWINCH,sig_winch)==SIG_ERR) { printf("signal error\n"); return -1; } pr_winsize(STDIN_FILENO); for(;;) { pause(); } }
将程序编译之后,运行,不断的改变终端的大小,就可以看到输出结果类似与下面:
55 rows,178 columns SIGWINCH received 6 rows,63 columns SIGWINCH received 6 rows,124 columns SIGWINCH received 24 rows,124 columns SIGWINCH received 53 rows,178 columns SIGWINCH received 55 rows,178 columns