在这里,我们将会讨论如何更好的控制用户终端;也就说是键盘输入与屏幕输出。除了这些,我们还会了解我们编写的程序如何由用户处读取输入,即使是在输入重定向的情况下,以及确保输出到屏幕的正确位置。
这里所提供的一些底层访问也许正是我们所寻找的。我们在这里所涉及的大部分内容也同样可以很好的适用于运行在终端窗口下的程序,例如KDE的Konsole,GNOME的gnome-terminal,或者是标准的X11 xterm。
在这一章,我们将会了解下面一些内容:
终端读取
终端驱动器以及通用终端接口
termios
终端输出与terminfo
检测按键
由终端读取与向终端写入
在第3章,我们了解到当一个程序由命令提示行启动时,shell会将标准的输入与输出流连接到我们的程序。我们可以通过使用getchar与printf例程来读取与写入这些默认流而与用户进行简单的交互。
下面我们要用C语言重写我们的菜单例程,只使用这两个例程,将其命名为menu1.c。
试验--C语言的菜单例程
1 由下面的代码行开始,其中定义要作为菜单使用的数组,以及getchoice函数原型:
#include <stdio.h> char *menu[] = { “a - add new record”, “d - delete record”, “q - quit”, NULL, }; int getchoice(char *greet, char *choices[]);
2 main函数使用例子菜单menu调用getchoice:
int main() { int choice = 0; do { choice = getchoice(“Please select an action”, menu); printf(“You have chosen: %c/n”, choice); } while(choice != ‘q’); exit(0); }
3 下面是重要的代码:打印菜单与读取用户输入的函数:
int getchoice(char *greet, char *choices[]) { int chosen = 0; int selected; char **option; do { printf(“Choice: %s/n”,greet); option = choices; while(*option) { printf(“%s/n”,*option); option++; } selected = getchar(); option = choices; while(*option) { if(selected == *option[0]) { chosen = 1; break; } option++; } if(!chosen) { printf(“Incorrect choice, select again/n”); } } while(!chosen); return selected; }
工作原理
getchoice函数打印程序简介greet与以及例子菜单choices,然后要求用户选择一个初始字符。程序会循环直到getchar函数返回一个与option数组实体的第一个字符匹配的字符。
当我们编译运行这个程序时,我们地发现他并不如我们期望的那样运行。下面是一此终端会活来演示这个程序:
$ ./menu1 Choice: Please select an action a - add new record d - delete record q - quit a You have chosen: a Choice: Please select an action a - add new record d - delete record q - quit Incorrect choice, select again Choice: Please select an action a - add new record d - delete record q - quit q You have chosen: q $
在这里用户必须输入A/Enter/Q/Enter来做出选择。这至少有两个问题:最严重的问题是我们在每次正确的选择之后都会得到Incorrect choise输出;另外,我们必须在程序读取我们的输入之前按下Enter。
典型与非典型模式
这两个问题是紧密相关的。在默认情况下,终端输入直到用户按下Enter下才会为程序可用。在大多数情况下,这是一个好处,因为他可以允许用户使用Backspace或是Delete修正输入错误。只有当他们对他们在屏幕上看到的感到满意以后才会按下Enter使得输入为程序可用。
这种行为称之为典型模式,或是者是标准模式。所有的输入都是以行的方式进行处理的。在一行输入完整(通常是当用户按下Enter时),终端界面管理所有的按键输入,包括Backspace,而且程序不会读取任何字符。
这种模式的相反面为非典型模式,此时程序在输入字符的处理上有更多的控制权。我们会在后面再回来讨论这两种模式。
除了这些之外,Linux终端处理器喜欢将字符转换为信号,并且可以为我们自动执行Backspace与Delete,所以在我们所编写的程序中并不需要重新实现。我们将会在第11章讨论更多关于信号的内容。
那么在我们的程序中发生了什么呢?Linux在用户按下Enter之前会保存输入,然后将选择的字符与后面的Enter发送给程序。所以每次我们输入一个菜单选项时,程序调用getchar,处理字符,然后再次调用getchar,此时会立即返回Enter字符。
程序实际看到的字符并不是一个ASCII码的回车,CR(十进制13,十六进制0D),而换行(十进制10,十六进制0A)。这是因为Linux(类似Unix)内部总是使用换行来结束文本行;也就是说,Unix只使用一个换行来表示新行,而其他的系统,例如MS-DOS,使用回车和换行来表示新行。如果输入或是输出设备也发送或是请求一个回车,Linux终端会小心的进行处理。如果我们习惯于使用MS-DOS或是其他的环境,那就会显得有一些奇怪,但是这样考虑的一个好处是Linux中文本与二进制之间并没有真正的区别。只有当我们向一个终端或是打印机,绘图仪时输入或输出时才会处理回车。
我们可以使用一些代码来忽略额外的换行符来简单的修改我们的菜单例程的主要缺陷,如下所示:
do { selected = getchar(); } while(selected == ‘/n’);
这解决了第一个问题。我们回到需要按下回车的第二个问题,而我们会在后面讨论一个更好的处理换行的方法
处理重定向输出
对于Linux程序,可以很容易的将他们的输入或输出重定向到一个文件或是其他的程序。让我们看一下当我们将输出重定到一个文件时我们的程序是如何处理的:
$ menu1 > file a q $
我们可以认为这是成功的,因为车出重定向到一个文件而不是终端。然而,这里却有我们希望阻止的情况,或者说我们希望分离提示,我们希望用户可以从其他的输出查看,从而可以安全的重定向。
我们可以通过检测是一个底层文件描述符是否与一个终端相关联来区别标准输出是否已被重定向。isatty系统调用可以完成这个工作。我们只需要简单的传递给他们一个可用的文件描述符,他就可以检测出这个文件描述符是否连接到一个终端。
#include <unistd.h> int isatty(int fd);
如果打开的文件描述符fd连接到一个终端,isatty系统调用就会返回1,否则返回0。
在我们的程序中,我们使用文件流,但是isatty只可以操作一个文件描述符。为了提供必要的转换,我们需要结合使用isatty调用与我们在第3章讨论的fileno例程。
如果stdout已经被重定向了我们要怎么办呢?仅是退出是不够的,因为用户并不知道程序为什么会运行失败。在stdout上打印一条信息也没有用,因为他已经被重定向离开终端了。一个解决办法就是写入stderr,此时他并没有被shell命令>file进行重定向。
试验--检测输出重定向
使用我们在前面所编写的程序menu1.c,包含一个新的include,将main改为下面的代码,并将其称为menu2.c.
#include <unistd.h> ... int main() { int choice = 0; if(!isatty(fileno(stdout))) { fprintf(stderr,”You are not a terminal!/n”); exit(1); } do { choice = getchoice(“Please select an action”, menu); printf(“You have chosen: %c/n”, choice); } while(choice != ‘q’); exit(0); }
工作原理
新版本的代码使用isatty函数来测试标准是否连接到一个终端,如果不是则会结束执行。同样也可以使用shell测试来决定是束提供一个提示符。当然可以,而且也比较常见的是同时重定向stdout与stderr,从而使其离开终端。我们可以像如下的样子将错误流重定向到一个不同的文件:
$ menu2 >file 2>file.error $
或者是将两个输出流组合到一个文件中,如下所示:
$ menu2 >file 2>&1 $
在这个例子中,我们需要向控制台发送一条消息。
与终端交互
如果我们需要阻止我们程序中与用户交互的部分被重定向,但是对于其他的输入或是输出我们还是允许发生的,此时我们需要分离与stdout和stderr的交互。我们可以通过直接读写终端来做到。因为Linux是一个多用户系统,通常有许多终端直接相连或是通过网络相连,那么我们如何来确定要使用的正确终端呢?
幸运的,Linux和Unix系统通过提供一个特殊的设备,/dev/tty,从而使得事情变得简单,这个设备通常是当前的终端或是登陆会话。因为Linux将所有的内容都看作文件,我们可以使用通常的文件操作来读写/dev/tty设备。
现在我们要来修改我们的选择程序,从而我们可以向getchoice例程传递参数,来更好的控制输出。我们将命名为menu3.c。
试验--使用/dev/tty
打开menu2.c,将其内容改为下列的样子,这样输入和输出就可以重定向到/dev/tty:
#include <stdio.h> #include <unistd.h> char *menu[] = { “a - add new record”, “d - delete record”, “q - quit”, NULL, }; int getchoice(char *greet, char *choices[], FILE *in, FILE *out); int main() { int choice = 0; FILE *input; FILE *output; if(!isatty(fileno(stdout))) { fprintf(stderr,”You are not a terminal, OK./n”); } input = fopen(“/dev/tty”, “r”); output = fopen(“/dev/tty”, “w”); if(!input || !output) { fprintf(stderr,”Unable to open /dev/tty/n”); exit(1); } do { choice = getchoice(“Please select an action”, menu, input, output); printf(“You have chosen: %c/n”, choice); } while(choice != ‘q’); exit(0); } int getchoice(char *greet, char *choices[], FILE *in, FILE *out) { int chosen = 0; int selected; char **option; do { fprintf(out,”Choice: %s/n”,greet); option = choices; while(*option) { fprintf(out,”%s/n”,*option); option++; } do { selected = fgetc(in); } while(selected == ‘/n’); option = choices; while(*option) { if(selected == *option[0]) { chosen = 1; break; } option++; } if(!chosen) { fprintf(out,”Incorrect choice, select again/n”); } } while(!chosen); return selected; }
现在,当我们使用输出重定向来运行这个程序时,我们可以看到提示符与通常的程序输出是分离的:
$ menu3 > file You are not a terminal, OK. Choice: Please select an action a - add new record d - delete record q - quit d Choice: Please select an action a - add new record d - delete record q - quit q $ cat file You have chosen: d You have chosen: q
终端驱动器与通用终端接口
有时程序需要更好的控制终端而不是使用简单的文件操作来达到。Linux提供了一个可以允许我们控制终端驱动器的接口集合,从而可以使得我们更好的控制终端的输入与输出处理过程。
概览
正如下图所示,我们可以通过一个与用来进行读写操作相分离的函数调用集合来控制终端。这使得数据接口更为清晰,同时又能更好的控制终端的行为。这并不说是说终端I/O接口是清晰的,而是可以处理各种不同的硬件。
在Linux术语中,控制接口设置一个"行规程"(line discipline),从而使得程序在指定终端驱动器的行为方面更为灵活。
我们可以控制的主要特征包括:
行编辑:决定是否允许编辑使用Backspace
缓冲:决定是否立即读取字符,或是在一个延时后读取
回显:允许我们控制回显,例如当我们正读取密码时
CR/LF:决定输入与输出映射,也就是当我们输入一个/n时会发生什么
行速度:很少用在PC控制台上,这些速度对于调制解调器和串口线上的终端非常重要
硬件模型
在我们详细的了解通用终端接口之前,了解一下他所驱动的硬件模型是非常重要的。
下图显示的是一台Unix机器通过一个串口连接到一个调制解调器,然而通过一条电话线和另一个调制解调器连接到远程端。事实上,这就是一些小的网络提供商所采用的配置类型。这就是相对遥远的客户/服务器模式,用于程序在主机上运行而用户在终端上工作的情况。
如果我们在一个运行Linux的PC上工作,这显得似乎是一个过于复杂的模型。然而,如果两个人都有调制解调器,如果我们愿意,我们可以使用一个终端模拟程序,例如minicom,来在彼此的机器上运行一个登陆会话,就如同使用一对调制解调器和一条电话线一样。
使用这样一个硬件模型的好处就是大多数真实世界的情况会形成这种最复杂情况的一个子集。支持他们比失去这个功能更为简单。
termios结构
termios是POSIX所指定的标准接口,与System V接口的termio类似。终端接口是通过在一个termios类型的结构中设置值以及使用一组函数调用来进行控制的。所有这些都定义在头文件termios.h中。
注意:使用定义在termios.h中的函数的程序需要使用一个合适的函数库进行链接。这通常是curses库,所以当编译这一章的程序是,我们需要在编译器命令行的最后加上-lcurses。在一些老的Linux系统上,curses库是由一个所谓的新curses或是ncurses来提供的。在这些情况下,库名字与链接参数就分别变为-lncurses。
可以进行操作来影响终端的值可以分为几种模式:
输入(input)
输出(output)
控制(control)
本地(local)
特殊控制字符(Special control characters)
一个最小的termios结构通常声明如下(X/Open规范允许添加一些其他的域):
#include <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]; };
成员的名字对应上面列表中的五个参数。
我们可以通过调用tcgetattr函数来为终端初始化termios结构,其函数原型如下:
#include <termios.h> int tcgetattr(int fd, struct termios *termios_p);
这个函数调用将终端接口变量的当前值写入由termios_p所指向的结构中。如果这些值被修改了,我们可以使用tcsetattr函数来重新配置终端接口:
#include <termios.h> int tcsetattr(int fd, int actions, const struct termios *termios_p);
tcsetattr函数中的actions域控制如何应用这些修改。三个可能的值分别为:
TCSANOW:立即更改
TCSADRAIN:当前输出完成时更改
TCSAFLUSH:当前输出完成时更改,但是忽略当前可用的输入与read调用中未返回的输入
注意:在程序启动之前保存终端设置是非常重要的。通常程序负责初始保存并且在程序完成时恢复设置。
下面我们会详细的看一下这些模式以及相关的函数调用。某些详细的模式相当特殊并且很少用到,所以在这里我们只讨论主要特征。如果我们要了解更多的内容,我们可以查看我们本地的man手册页或是一份POSIX或X/Open规范的拷贝。
我们首先要了解的最重要的模式的是本地模式。正规与非正规模式是我们第一个程序中第二个问题的解决方法。我们可以指示我们的程序等一行输入或是在输入之后立即声明为输入。
输入模式
输入模式控制输入(终端驱动器在串口或是键盘上接收的字符)在传递给程序之前是如何处理的。我们通过在termios结构的c_iflag成员中设置相应的标记来进行设置。所有这些标记都定义为宏,而且可以使用位或进行组合。对于所有的终端模式都是如此。
可以用于c_iflag的宏为:
BRKINT:在一行中检测到中断(break)条件时产生一个中断
IGNBRK:在一行中忽略中断条件
INCRNL:将接收到的回车转换为换行
IGNCR:忽略接收到的因车
INLCR:将接收到的新行转换为回车
IGNPAR:忽略带有奇偶检验误差的字符
INPCK:在接收到的字符上执行奇偶校验
PARMRK:标记奇偶校验误差
ISTRIP:去除所有的输入字符
IXOFF:在输入上允许软件流控制
IXON:在输出上允许软件流控制
如果没有设置BRKINT与IGNBRK,一行中的break条件将会被读取为NULL(Ox00)字符。
我们并不需要经常的改变输入模式,因为默认值通常是最合适的,所以在这里我们并不会进行更为深入的讨论。
输出模式
这些模式控制输出字符是如何进行处理的;也就是说,程序所发送的字符在传递到串口或是屏幕之前是如何被处理的。正如我们所期望的,许多输出模式都有相对应的输入模式。同时也存在一些其他的标记,这些标记主要关注于允许需要时间处理字符的慢速终端。几乎所有的这些模式都可以使用终端功能的terminfo数据进行很好的处理,这我们会在后面用到。
我们通过设置termios结构的c_flag成员标记来控制输出模式。我们可以在c_oflag使用的标记有:
OPOST:打开输出处理
ONLCR:将输出的新行转换为回车/换行对
OCRNL:将输出的回车转换为新行
ONOCR:在第0列不输出回车
ONLRET:新行也需要一个回车
OFILL:发送填充字符来提供延时
OFDEL:使用DEL作为填充字符,而不是NULL
NLDLY:新行延时选择
CRDLY:回车延时选择
TABDLY:Tab延时选择
BSDLY:Backspace延时选择
VTDLY:垂直Tab延时选择
FFDLY:换页延时选择
如果OPOST没有设置,所有其他的标记都会被忽略。
输出模式也并不经常使用,所以在这里我们也不会进行更深入的讨论。
控制模式
这些模式控制终端的硬件特点。我们可以通过设置termios结构中的c_cflag成员的值来指定控制模式,其可用的值为:
CLOCAL:忽略调制解调器状态行
CREAD:允许字符接收
CS5:在发送或是接收的字符中使用5位(5 bits)
CS6:在发送或是接收的字符中使用6位
CS7:在发送或是接收的字符中使用7位
CS8:在发送或是接收的字符中使用8位
CSTOPB:每个字符使用两个结束位,而不是一个
HUPCL:关闭时挂起调制解调器
PARENB:允许奇偶校验生成与检测
PARODD:使用介校验而不是奇校验
如果设置了HUPCL,当终端驱动器检测到指向终端的最后一个文件描述符已经关闭时,他就会将调制解调器控制行设置为挂起。
控制模式主要用于串口线连接到一个调制解调器上的情况,尽管他们与可以用于与终端交互。通常,使用termios的控制模式改变我们终端的配置要比改变默认行行为简单得多。
本地模式
这些模式控制终端的各种特性。我们可以通过设置termios结构中的c_lflag成员的值来指定本地模式,其可用的宏如下:
ECHO:允许输入字符的本地回显
ECHOE:在接收EPASE时执行Backspace,Space,Backspace组合
ECHOK:在KILL字符上执行清除行
ECHONL:回显新行字符
ICANON:允许正规输入处理
IEXTEN:允许实现特定函数
ISIG:允许信号
NOFLSH:禁止队列flush
TOSTOP:在写尝试上发送后台处理信号
最重要的两个标记为ECHO,这会允许我们抑制输入字符的回显,以及ICANON,他在两个不同的处理接收字符的模式中切换终端。如果设置了ICANON标记,这一行就处理正规模式;如果没有,这一行就处理非正规模式。
特殊控制字符
另外还有一些字符集合,例如Ctrl-C,当用户输入时会以特殊的方式运行。termios结构的c_cc数组成员包含映射到每一个支持函数的字符。每一个字符的位置(在数组中的索引)是由一个宏定义的,但是他们必须控制的字符并没有限制。
依据于终端是否设置为正规模式(例如,在termios的c_lfalg成员设置ICANON标记),c_cc数组以两种不同的方式来使用。
在这里我们要注意的是,两种不同模式的数组索引值所使用的方式有一某些重叠。正因为如此,我们绝不要混用这两种模式的值。
对于正规模式,数组索引为:
VEOF:EOF字符
VEOL:EOL字符
VERASE:ERASE字符
VINTR:INTR字符
VKILL:KILL字符
VQUIT:QUIT字符
VSUSP:SUSP字符
VSTART:START字符
VSTOP:STOP字符
对于非正规模式,数组索引为:
VINTR:INTR字符
VMIN:MIN值
VQUIT:QUIT字符
VSUSP:SUSP字符
VTIME:TIME值
VSTART:START字符
VSTOP:STOP字符
字符
因为特殊字符以及非正规字符MIN与TIME值对于高级输入字符的处理是如此重要,我们会在这里进行详细的解释。
字符 描述
INTR 使得终端驱动器向连接到终端的处理操作发送一个SIGINT信号。我们会在第11章更详细的讨论信息。
QUIT 使得终端驱动器向连接到终端的处理操作发送一个SIGQUIT信号。
ERASE 使得终端驱动器删除一行的最后一个字符。
KILL 使得终端驱动器删除整个行
EOF 使得终端驱动器将一行中的所有字符传递给正读取输入的程序。如果此行为空,read调用会返回零个字符,就如同read操作到达文件结尾时一样。
EOL 行结束符,与更为通常的新行字符相类似
SUSP 使得终端驱动器向连接到终端的操作发送一个SIGSUSP信号。如果我们的Unix系统支持工作控制,当前的程序就会被挂起。
STOP 阻止向终端发送更多的输出。他用于支持XON/XOFF流控制,通常设置为ASCII XOFF字符,Ctrl+S。
START 在STOP字符之后重启输出,通常为ASCII XON字符。
TIME与MIN值
TIME与MIN的值只用非正规模式,而且共同作用来控制输入的读取。同时,他们控制当程序试图读取与一个终端相关联的文件描述符时会发生什么。
有四种情况:
MIN=0同时TIME=0:在这种情况下,一个read调用会立即返回。如果某些字符可用,他们就会立即返回;如果没有可用字符,read会返回零并且不会读取任何字符。
MIN=0同时TIME>0:在这种情况下,当有任何可以读取的字符或是TIME的十分之一秒逝去时,read会返回。如果因为时间过期没有读取任何字符,read就会返回零。否则,他会返回读取的字符数。
MIN>0同时TIME=0:在这种情况下,read会等待直到有MIN个字符可以读取,然后返回读取的字符数。在文件结尾时会返回零。
MIN>0同时TIME>0:这是最复杂的情况。当调用read时,他等待接收一个字符。当接收到第一个字符,以及在接下来的时间序列内接收到一个字符时,就会启动一个中间字符(inter-character)计时器(如果他已经在运就重新启动)。当有MIN个字符可以读取或是中间字符计时器的TIME时间值过去十分之一秒时,read会返回。这可以用于区分Escape按键的一下按下与一个函数键值转义序列的启动之间的区别。但要小心,网络通信或是高级处理器会擦除时间信息。
通过设置非正规模式以及使用MIN与TIME值,程序可以执行一个字符一个字符的输入处理。
由Shell访问终端模式
如果我们要查看我们正使用的Shell所使用的termios设置,我们可以使用下面的命令来得到一个列表:
$ styy -a
在我们的Linux系统上,对标准的termios结构进行一些扩展,其输出如下:
speed 38400 baud; rows 44; columns 109; line = 0; intr = ^C; quit = ^/; erase = ^?; kill = ^U; eof = ^D; eol = <undef>; eol2 = <undef>; start = ^Q; stop = ^S; susp = ^Z; rprnt = ^R; werase = ^W; lnext = ^V; flush = ^O; min = 1; time = 0; -parenb -parodd cs8 -hupcl -cstopb cread -clocal -crtscts -ignbrk -brkint -ignpar -parmrk -inpck -istrip -inlcr -igncr icrnl ixon -ixoff - iuclc -ixany -imaxbel opost -olcuc -ocrnl onlcr -onocr -onlret -ofill -ofdel nl0 cr0 tab0 bs0 vt0 ff0 isig icanon iexten echo echoe echok -echonl -noflsh -xcase -tostop -echoprt echoctl echoke
在这些输出之间,我们可以看到EOF字符为Ctrl+D并且允许回显。如果我们试验终端控制,我们可以很容易使得这个终端处理非标准状态,从而会使其使用非常困难。有几个方法可以做到这一点。
第一个方法是,如果我们的stty版本支持,我们可以使用下面的命令:
$ stty sane
如果我们失去了回车键到新行字符的映射,我们需要输入stty sane,但不是按回车,而是按下Ctrl+J(新行字符)。
第二个方法是使用stty -g命令将当前的stty设置存为可以重新读取的格式。在命令行中,我们可以使用下面的命令:
$ stty -g > save_stty .. <experiment with settings> .. $ stty $(cat save_stty)
对于最后一个stty命令我们仍然需要使用Ctrl+J而不是回车。我们要以在shell脚本中使用同样的技术:
save_stty=”$(stty -g)” <alter stty settings> stty $save_stty
第三个方法就是使用另一个不同的终端,使用ps命令来查看我们要使其成为不可用的shell,然后使用kill HUP <process id>来强制结束这个shell。因为stty参数总是在一个登陆提示出现之前进行设置的,所以我们可以正常登陆。
由命令行设置终端模式
我们也可以使用stty命令直接由命令行来设置终端模式。
要设置一个我们的shell脚本可以执行单个字符读取的模式,我们需要关闭正规模式,同时将MIN设置为1,而TIME设置为0。命令如下:
$ stty -icanon min 1 time 0
现在终端被设置为可以立即读取字符,我们可以试着运行我们的第一个程序。我们就会发现其运行情况正如我们所希望的那样。
我们也可以在提示输入密码之前关闭回显来输入密码。其命令如下:
$ stty -echo
一定要记住在我们试验之后一定要用命令stty echo来打开回显。
终端速率
termios结构所提供的最后一个函数可以操作线速率。并没有为终端速率定义成员;相反,他是由函数调用来设置的。输入与输出速率是分别处理的。
四个调用原型为:
#include <termios.h> speed_t cfgetispeed(const struct termios *); speed_t cfgetospeed(const struct termios *); int cfsetispeed(struct termios *, speed_t speed); int cfsetospeed(struct termios *, speed_t speed);
注意,这些函数是作用在termios结构上的,而不是直接作用在端口上。这就意味着要设置一个新的速率,我们必须使用tcgetattr读取当前的设置,使用上面函数调用中的一个来设置速率,然后使用tcsetattr写回termios结构。只有tcsetattr调用之后,线速率才会改变。
在上面的函数调用中允许各种速率值,其中最重要的为如下几个:
B0:挂起终端
B1200:1200波特
B2400:2400波特
B9600:9600波特
B19200:19200波特
B38400:38400波特
标准并没有定义大于38400的速率,对于大于这个速率的串口也没有相应的支持函数。
一些系统,包括Linux,为选择更快的速率定义了B57600,B115200和B230400几个速率。如果我们正使用Linux的某个早期版本,而并不可以使用这些常量,我们可以使用setserial命令来获得57600和115200等非标准速率。在这种情况下,当选择B38400时会使用这些速率。这两个方法都是不可以移植的,所以我们使用时要小心。
其余的函数
对于终端控制还有一些其他的函数。这些函数直接作用在文件描述符上,而不需要读取与设置termios结构。他们的定义如下:
#include <termios.h> int tcdrain(int fd); int tcflow(int fd, int flowtype); int tcflush(int fd, int in_out_selector);
这些函数的目的如下:
tcdrain会使得调用函数在所有的输出队列发送之前等待。
tcflow用于中止或是重启输出。
tcflush可以用于冲刷输入,输出或是两者。
现在我们已经讨论了关于termios结构相当多的主题内容,下面让我们来看一些实际的例子。也许最简单的就是在读取密码时禁止回显了。我们可以通过关闭ECHO标记来做到。
试验--使用termios读取密码
1 我们的密码程序,password.c,以下面的定义开始:
#include <termios.h> #include <stdio.h> #define PASSWORD_LEN 8 int main() { struct termios initialrsettings, newrsettings; char password[PASSWORD_LEN + 1];
2 接下来,添加一行来由当前的标准输入读取当前的设置,并且将其拷贝到我们前面创建的termios结构中:
tcgetattr(fileno(stdin), &initialrsettings);
3 制作一份原始设置的拷贝来替换他们。在newrsettings中关闭ECHO标记,并且询问用户密码:
newrsettings = initialrsettings; newrsettings.c_lflag &= ~ECHO; printf(“Enter password: “);
4 然后将终端属性设置为newrsettings,并且读取密码。最后,将终端属性设置为其原始属性,并且打印密码来验证前面的效果。
if(tcsetattr(fileno(stdin), TCSAFLUSH, &newrsettings) != 0) { fprintf(stderr,”Could not set attributes/n”); } else { fgets(password, PASSWORD_LEN, stdin); tcsetattr(fileno(stdin), TCSANOW, &initialrsettings); fprintf(stdout, “/nYou entered %s/n”, password); } exit(0); }
工作原理
$ ./password Enter password: You entered hello $
在这个例子中,在Enter password:提示之后输入hello,但是输入的字符并没有回显。直到用户按下Enter时才产生输出。
我们很小心的使用语句 X &= ~FLAG(清除相应的FLAG位)来改变我们需要改变的标记位。如果需要,我们可以使用X |= FLAG来设置由FLAG定义的位,虽然在我们上面的这个例子中并不需要这样。
当我们设置属性性时,我们使用TCSAFLUSH来忽略程序准备读取之前用户所输入的字符。这是使得用户在回显关闭之前不要输入密码的一个好办法。同时我们在程序结束之前恢复了先前的设置。
termios结构的另一个通常用法就是可以设置终端为一种我们可以立即读取用户输入字符的状态。我们可以通过关闭正规模式并且设置MIN与TIME的值来做到。
试验--读取每个字符
1 使用我们的新程序,我们可以对我们的菜单程序做出修改。下面的代码与pasword.c相类似,但是需要插入到menu3.c中来生成我们的新程序menu4.c。在开始这前,我们需要在程序顶部包含一个新的头文件:
#include <stdio.h> #include <unistd.h> #include <termios.h>
2 然后我们需要在main函数中定义一些新的变量:
int choice = 0; FILE *input; FILE *output; struct termios initial_settings, new_settings;
3 我们需要在调用getchoice函数之前修改终端特点,这就是我们需要插入代码的地方:
fprintf(stderr, “Unable to open /dev/tty/n”); exit(1); } tcgetattr(fileno(input),&initial_settings); new_settings = initial_settings; new_settings.c_lflag &= ~ICANON; new_settings.c_lflag &= ~ECHO; new_settings.c_cc[VMIN] = 1; new_settings.c_cc[VTIME] = 0; if(tcsetattr(fileno(input), TCSANOW, &new_settings) != 0) { fprintf(stderr,”could not set attributes/n”); } fprintf(stderr, “Unable to open /dev/tty/n”); exit(1); } tcgetattr(fileno(input),&initial_settings); new_settings = initial_settings; new_settings.c_lflag &= ~ICANON; new_settings.c_lflag &= ~ECHO; new_settings.c_cc[VMIN] = 1; new_settings.c_cc[VTIME] = 0; if(tcsetattr(fileno(input), TCSANOW, &new_settings) != 0) { fprintf(stderr,”could not set attributes/n”); }
4 同时我们需要在程序结束之前恢复原始设置:
do { choice = getchoice(“Please select an action”, menu, input, output); printf(“You have chosen: %c/n”, choice); } while (choice != ‘q’); tcsetattr(fileno(input),TCSANOW,&initial_settings); exit(0); }
5 现在我们需要检测回车/r以确保我们在非正规模式,因为不会再执行默认的CR到LF的映射:
do { selected = fgetc(in); } while (selected == ‘/n’ || selected == ‘/r’);
6 不幸的是,如果此时用户在我们程序运行时按下Ctrl+C,程序就会终止。我们可以通过在本地模式中清除ISIG标记来禁止特殊字符的处理。在主函数中添加下面的代码行:
new_settings.c_lflag &= ~ISIG;
如果我们将这些修改加入到我们的程序中,我们现在就会得到一个立即响应并且输入不会回显朱程序:
$ ./menu4 Choice: Please select an action a - add new record d - delete record q - quit You have chosen: a Choice: Please select an action a - add new record d - delete record q - quit You have chosen: q $
如果我们按下Ctrl+C,他就会被直接传递给程序,并且将其看作是不正确的选择。
终端输出
使用termios结构,我们可以控制键盘输入,但是如果在显示在屏幕上的输出上可以进行同样级别的控制也许会更好。在我们本章的开始,我们使用printf来向屏幕输出字符,但是却没有办法将输出定位在屏幕上的某个特定位置上。
终端类型
许多Unix系统使用终端,尽管在今天的许多情况下,终端也许实际上是一个运行终端程序的PC。从历史上来说,不同的生产产商提供了大量的硬件终端。尽管他们都是使用转义序列(以转义字符开始的字符串)来提供对光标与属性的控制,例如粗体与闪烁等,但是他们并没有以标准的方式来提供这些特性。某些老的终端同时还具有不同的滚动功能,当发送backspace滚动条也许会消失。
硬件终端的多样性对于那些希望编写控制屏幕以及运行在多个终端类型上的软件的程序员是一个极大的问题。例如,ANSI标准使用转义序列Escape+[+A来将光标上移一行,然而ADM-3a终端却使用单独的控制字符Ctrl+K。
要编写处理各种不同的连接到Unix系统上的终端类型的程序是一件极其困难的任务。程序也许要为每一个终端类型提供不同的源代码。
这样在一个名为terminfo的包中提供一个解决方案就显得并不惊奇。程序并不会迎合各种终端类型,相反,程序会查找一个终端类型数据库来得到正确的信息。在大多数的现代Unix系统中,包括Linux,这些已经被集成到一个名为curses的软件包中,这就是我们下一章要了解的内容。
在Linux上,我们也许要使用curses的一个名为ncurses的实现,并且要包含ncurses.h文件来提供我们terminfo函数的原型。terminfo函数本身声明在他们自己的头文件term.h中,或者至少以前是这种情况。而在新版本的Linux系统上,在terminfo与ncurses之间有一个模糊的界线,许多需要terminfo函数的程序必须同时包含ncurses头文件。为避免以后的混乱,现代的Linux发行版本同时提供一个与Unix系统更兼容的curses头文件与库。在这些系统上,我们推荐使用curses.h与-lcurses。
标识我们的终端类型
Linux环境包含一个变量,TERM,他被设置为我们正在使用的终端类型。通常他是在系统登陆时由系统自动设置的。系统管理员也许会为每一个直接连接到终端的用户设置一个默认的终端类型,这些用户也许是要提供终端类型的远程或是网络用户。TERM的值可以通过telnet协商,并通过rlogin传递。
用户可以查询shell来确定他正使用的终端类型。
$ echo $TERM xterm $
在这个例子中,shell是由一个名为xterm的程序来运行的,这是一个X Window系统的终端模拟器,或是提供类似功能的程序,例如KDE的Konsole或是Gnome的gnome-terminal。
terminfo软件包包含了一个功能与大量终端的转义序列的数据库,并且为程序员提供了统一的接口。这样编写的程序就可以在数据库扩展时利用未来终端的优点,而不是每一个程序都必须为不同的终端提供支持。
terminfo的功能是通过属性来描述的。这些属性存储在已编译的terminfo文件集合中,并且通常可以在/usr/lib/terminfo或是/usr/share/terminfo中找到。对于每一个终端(也有一些可以在terminfo中指定的打印机),有一个文件来定义其功能以及如何访问这些特性。为了避免创建一个非常大的目录,实际的文件存储在子目录中,而子目录的名字只是简单的终端类型的第一个字符。所以,VT100的定义可以在...terminfo/v/vt100中找到。
对于每一个终端类型都会以可读的源码的格式来编写一个terminfo文件,然后使用tic命令将其编译为应用程序可用的更为紧凑和高效的格式。奇怪的是,X/Open规范谈到源码以及编译的格式定义,但是却没有提到实际编译源码的tic命令。我们可以使用infocmp程序来输出一个已编译的terminfo实体的可读版本信息。
下面是一个VT100终端的terminfo文件的例子:
$ infocmp vt100 vt100|vt100-am|dec vt100 (w/advanced video), am, mir, msgr, xenl, xon, cols#80, it#8, lines#24, vt#3, acsc=``aaffggjjkkllmmnnooppqqrrssttuuvvwwxxyyzz{{||}}~~, bel=^G, blink=/E[5m[ DISCUZ_CODE_2758 ]lt;2>, bold=/E[1m[ DISCUZ_CODE_2758 ]lt;2>, clear=/E[H/E[J[ DISCUZ_CODE_2758 ]lt;50>, cr=/r, csr=/E[%i%p1%d;%p2%dr, cub=/E[%p1%dD, cub1=/b, cud=/E[%p1%dB, cud1=/n, cuf=/E[%p1%dC, cuf1=/E[C[ DISCUZ_CODE_2758 ]lt;2>, cup=/E[%i%p1%d;%p2%dH[ DISCUZ_CODE_2758 ]lt;5>, cuu=/E[%p1%dA, cuu1=/E[A[ DISCUZ_CODE_2758 ]lt;2>, ed=/E[J[ DISCUZ_CODE_2758 ]lt;50>, el=/E[K[ DISCUZ_CODE_2758 ]lt;3>, el1=/E[1K[ DISCUZ_CODE_2758 ]lt;3>, enacs=/E(B/E)0, home=/E[H, ht=/t, hts=/EH, ind=/n, ka1=/EOq, ka3=/EOs, kb2=/EOr, kbs=/b, kc1=/EOp, kc3=/EOn, kcub1=/EOD, kcud1=/EOB, kcuf1=/EOC, kcuu1=/EOA, kent=/EOM, kf0=/EOy, kf1=/EOP, kf10=/EOx, kf2=/EOQ, kf3=/EOR, kf4=/EOS, kf5=/EOt, kf6=/EOu, kf7=/EOv, kf8=/EOl, kf9=/EOw, rc=/E8, rev=/E[7m[ DISCUZ_CODE_2758 ]lt;2>, ri=/EM[ DISCUZ_CODE_2758 ]lt;5>, rmacs=^O, rmkx=/E[?1l/E>, rmso=/E[m[ DISCUZ_CODE_2758 ]lt;2>, rmul=/E[m[ DISCUZ_CODE_2758 ]lt;2>, rs2=/E>/E[?3l/E[?4l/E[?5l/E[?7h/E[?8h, sc=/E7, sgr=/E[0%?%p1%p6%|%t;1%;%?%p2%t;4%;%?%p1%p3%|%t;7%;%?%p4%t;5%;m%?%p9%t^N%e^O%;, sgr0=/E[m^O[ DISCUZ_CODE_2758 ]lt;2>, smacs=^N, smkx=/E[?1h/E=, smso=/E[1;7m[ DISCUZ_CODE_2758 ]lt;2>, smul=/E[4m[ DISCUZ_CODE_2758 ]lt;2>, tbc=/E[3g,
每一个terminfo定义由三种类型的实体构成。每一个实体被称之为capname并且定义了一个终端功能。
布尔功能只是简单的指示一个终端是否支持一个特定的特性。例如,如果终端支持XON/XOFF流控制就会显示xon布尔功能。
数字功能定义了尺寸,例如lines定义了屏幕上的行数,而cols定义了屏幕上的列数。实际的数字是通过#字符与功能相区分的。要定义一个具有80列与24行的终端,我们可以写成cols#80,lines#24。
字符串功能要显得有些复杂。他们用于两种不同的功能:定义访问终端所需要的输出字符串以及定义当用户按下特定的按键时会接收的输入字符串,通常为功能键或是数字键盘上的特殊键。某些字符功能相当简单,例如el,表示"清除直到一行结束"。在一个VT100的终端上,要完成这个任务的转义序列为Esc+[+K。在terminfo的源码格式中则为el=/E[K。
特殊的键的定义与此相类似。例如,VT100上的功能键F1发送的转义序列为Esc+O+P,其定义为kf1=/EOP。
当转义序列需要一些参数时,其定义会显得有些复杂。大多数的终端可以将光标移动到一个特定的行与列位置。对于每一个可能的光标位置都会有一个不 的功能是不切实际的,所有使用一个通用字符串,并且带有参数定义当字符串被使用时要插入的值。例如,VT100终端使用转义序列Esc+[+<row>+;+<col>+H来将光标移到一个特定的位置。在terminfo源码格式中,其定义为cup=/E[%i%p1%d;%p2%dH$<5>。
其含义为:
/E:发送Escape
[:发送[字符
%i:增加参数
%p1:将第一个参数放入堆栈
%d:将堆栈上的数字作为十进制数字输出
;:发送;字符
%p2:将第二个参数放入堆栈
%d:将堆栈上的数字作为十进制数字输出
H:发送H字符。
这看起来似乎有一些复杂,但是却允参数以固定的顺序出现,独立于终端希望他们出现在最终的转义序列中的顺序。增加参数的%i是必须的,因为标准的光标位置是屏幕的左上角(0,0),但是VT100的光标位置为(1,1)。最后的$<5>表明需要等同于输出5个字符的时间来允许终端处理光标移动。
注意,我们将会定义许多许多终端,但是幸运的是,大多数的Unix和Linux系统已经预定义了大多数的终端。如果我们需要添加一个新的终端,我们可以在terminfo手册页中查找到完整的功能列表。一个好的起点就是定位那些与我们的新终端相似的终端,将新终端定义为已存在终端的一个变体。
使用terminfo功能
现在我们知道了如何定义终端功能,我们需要了解如何访问他们。当我们使用terminfo时,我们需要做的第一件事就是通过调用setupterm来设置终端类型。这会为当前的终端类型初始化一个TERMINAL结构。然后我们就可以访问并使用终端功能。setupterm函数原型如下:
#include <term.h> int setupterm(char *term, int fd, int *errret);
setupterm库函数将当前的终端类型设置为参数term所指定的终端类型。如果term为一个空指针,那么就会使用TERM环境变量。写入终端所用的打开的文件描述符必须由参数fd传递。函数的执行结果存储在由errret所指向的整型变量中(如果他不为空)。写入的值可能是:
-1:没有terminfo数据库
0:在terminfo数据库中没有匹配的实体
1:成功
如果成功,setupterm函数会返回常量OK,如果失败则会返回ERR。如果errret设置为一个空指针,函数执行失败时就会输出一个诊断信息并且退出程序,如下面的例子所示:
#include <stdio.h> #include <term.h> #include <ncurses.h> int main() { setupterm(“unlisted”,fileno(stdout),(int *)0); printf(“Done./n”); exit(0); }
运行在我们系统上的程序输出也许并不是这里给出的样子,但是其含义已经足够明显了。在这里并没有打印出Done,因为setupterm函数执行失败从而导致程序退出。
$ cc -o badterm badterm.c -I/usr/include/ncurses -lncurses $ badterm ‘unlisted’: unknown terminal type. $
注意上面例子中的编译命令:在这个Linux系统上,ncurses头文件位于/usr/include/ncurses目录,所以我们必须使用-I选项来指示编译器在这里进行查找。而某些Linux系统也许会将ncurses库可以由标准位置进行访问。在这些系统上,我们只需要简单的包含curses.h头文件,并且为库指定-lcurses选项。
对于我们的菜单选择函数,我们希望可以清屏,在屏幕上移动光标,并且可以屏幕上的任意位置写入。一旦我们调用了setupterm函数,我们就可以使用不同的函数来访问terminfo功能,功能类型如下:
#include <term.h> int tigetflag(char *capname); int tigetnum(char *capname); char *tigetstr(char *capname);
函数tigetflag,tigetnum,tigetstr分别返回布尔,数字值以及字符串terminfo功能。如果失败,tigetflag会返回-1,tigetnum会返回-2,而tigetstr会返回(char *)-1。
下面我们使用程序sizeterm.c程序取得cols与lines功能来确定终端尺寸:
#include <stdio.h> #include <term.h> #include <ncurses.h> int main() { int nrows, ncolumns; setupterm(NULL, fileno(stdout), (int *)0); nrows = tigetnum(“lines”); ncolumns = tigetnum(“cols”); printf(“This terminal has %d columns and %d rows/n”, ncolumns, nrows); exit(0); } $ echo $TERM vt100 $ sizeterm This terminal has 80 columns and 24 rows $
如果我们在工作站的一个窗口内运行这个程序,我们会得到反映当前窗口尺寸的答案:
$ echo $TERM xterm $ sizeterm This terminal has 88 columns and 40 rows $
如果我们使用tigetstr来取得xterm终端类型的光标移动功能(cup),我们会得到一个参数化的答案: /E[%p1%d;%p2%dH。
这个功能需要两个参数:光标要移动到的行与列。这两个坐标都是由屏幕左上角的零点处开始计量的。
我们可以使用tparm函数用实际的值来代替功能中的参数。最多可以替换九个参数,并且会返回一个可用的转义序列:
#include <term.h> char *tparm(char *cap, long p1, long p2, ..., long p9);
一旦我们使用tparm来组织终端转义序列,我们必须将其发送到终端。要正确的处理,我们不应使用printf来向终端发送字符串,相反,我们要使用特殊的函数,这些函数为终端完成一个操作的正确处理提供了必要的延时。这些函数为:
#include <term.h> int putp(char *const str); int tputs(char *const str, int affcnt, int (*putfunc)(int));
如果成功,putp返回OK,如果失败,则会返回ERR。putp函数将终端控制字符串作为参数并且将其发送到标准输出。
所以要移动到屏幕的第5行,第30列,我们可以使用下面的代码块:
char *cursor; char *esc_sequence; cursor = tigetstr(“cup”); esc_sequence = tparm(cursor,5,30); putp(esc_sequence);
tputs函数是为那些不可以通过stdout访问终端并且允许我们指定输出字符所使用的函数的情况而提供的。他会返回用户指定的函数putfunc的结要。affcnt参数用来指明更改会影响到的行数,通常将其设置为1。用于输出字符串的函数必须与putchar函数具有相同的参数与返回结果。事实上,putp(string)等同于调用tputs(string,1,putchar)。我们将会在下面的例子中的看到用用户指定的输出函数来使用tputs函数。
要小心,一些老的Linux发行版本将tputs函数的最后一个参数定义为int (*putfunc)(char),这会强制我们修改在我们的下一个试验中所定的char_to_terminal函数。
注意,如果我们查看tparm与终端功能的信息手册页,我们也许会遇到一个tgoto函数。他为移动光标提供了一个更为简单的方案,但是我们不使用这个函数的原因是因为X/Open规范并没有将其包含在1997版本中。所以我们推荐不要在新程序中使用这些函数。
现在我们准备好为我们的菜单选择功能添加屏幕处理了。还有一件需要做的事就是简单的使用clear来清除屏幕。某些终端并不支持clear功能,从而会使得光标停留在屏幕的左上角。在这种情况下,我们可以将光标放置在左上角,并且使用"删除直到显示结尾"命令ed。
将所有这些信息结合在一起,我们可以编写我们例子菜单程序的最终版本,screen-menu.c,在这里我们将会在屏幕上"画"出选项,从而供用户选择。
试验--完全终端控制
我们可以重新编写menu4.c的getchoice函数从而为我们提供完全的终端控制。在这个列表中,省略了main函数,因为他并没有改变。
#include <stdio.h> #include <unistd.h> #include <termios.h> #include <term.h> #include <curses.h> static FILE *output_stream = (FILE *)0; char *menu[] = { “a - add new record”, “d - delete record”, “q - quit”, NULL, }; int getchoice(char *greet, char *choices[], FILE *in, FILE *out); int char_to_terminal(int char_to_write); int main() { ... } int getchoice(char *greet, char *choices[], FILE *in, FILE *out) { int chosen = 0; int selected; int screenrow, screencol = 10; char **option; char *cursor, *clear; output_stream = out; setupterm(NULL,fileno(out), (int *)0); cursor = tigetstr(“cup”); clear = tigetstr(“clear”); screenrow = 4; tputs(clear, 1, (int *) char_to_terminal); tputs(tparm(cursor, screenrow, screencol), 1, char_to_terminal); fprintf(out, “Choice: %s, greet); screenrow += 2; option = choices; while(*option) { tputs(tparm(cursor, screenrow, screencol), 1, char_to_terminal); fprintf(out,”%s”, *option); screenrow++; option++; } fprintf(out, “/n”); do { fflush(out); selected = fgetc(in); option = choices; while(*option) { if(selected == *option[0]) { chosen = 1; break; } option++; } if(!chosen) { tputs(tparm(cursor, screenrow, screencol), 1, char_to_terminal); fprintf(out,”Incorrect choice, select again/n”); } } while(!chosen); tputs(clear, 1, char_to_terminal); return selected; } int char_to_terminal(int char_to_write) { if (output_stream) putc(char_to_write, output_stream); return 0; }
工作原理
重写的getchoice函数实现了与我们前面的例子中相同的菜单,但是输出函数进行了修改从而来使用terminfo功能。如果我们希望在屏幕被清除之前看到You have chosen:信息停留一会,可以使用下面的选择,在main函数中添加一个sleep调用:
do { choice = getchoice(“Please select an action”, menu, input, output); printf(“/nYou have chosen: %c/n”, choice); sleep(1); } while (choice != ‘q’);
这个程序中的最后一个函数,char_to_terminal,包含了一个我们在第3章提到的putc函数调用。
检测按键
编写过MS-DOS程序的人通常都会查找Linux下等同于kbhit的函数,这个函数会检测一个按键是否被按下而并不实际的读取。不幸的是他们并没有找到这样的函数,因为并没有直接等同的函数。Unix程序员并不会注意到这个遗漏,因为Unix的编程方式通常为程序应准备好等待事件的发生。因为这就是通常的kbhit的用法,所以Unix和Linux将其忽略了。
然而,当我们要由MS-DOS移植程序时,通常需要模拟kbhit,此时我们可以用非正规输入模式来做到。
试验--我们自己的kbhit
1 首先我们需要定义标准的头文件并且为终端设置声明了一个结构。peek_character用于测试一个按键是否被按下。然后我们定义了我们将会用到的函数的原型。
#include <stdio.h> #include <termios.h> #include <term.h> #include <curses.h> #include <unistd.h> static struct termios initial_settings, new_settings; static int peek_character = -1; void init_keyboard(); void close_keyboard(); int kbhit(); int readch();
2 main函数调用init_keyboard函数来配置终端,然后一秒循环一次,调用kbhit函数。如果按键检测为q,close_keyboard函数会返回正常行为并且退出程序。
int main() { int ch = 0; init_keyboard(); while(ch != ‘q’) { printf(“looping/n”); sleep(1); if(kbhit()) { ch = readch(); printf(“you hit %c/n”,ch); } } close_keyboard(); exit(0); }
3 init_keyboard与close_keyboard在程序的开始和结束配置终端。
void init_keyboard() { tcgetattr(0,&initial_settings); new_settings = initial_settings; new_settings.c_lflag &= ~ICANON; new_settings.c_lflag &= ~ECHO; new_settings.c_lflag &= ~ISIG; new_settings.c_cc[VMIN] = 1; new_settings.c_cc[VTIME] = 0; tcsetattr(0, TCSANOW, &new_settings); } void close_keyboard() { tcsetattr(0, TCSANOW, &initial_settings); }
4 下面是检测键盘按键的函数:
int kbhit() { char ch; int nread; if(peek_character != -1) return 1; new_settings.c_cc[VMIN]=0; tcsetattr(0, TCSANOW, &new_settings); nread = read(0,&ch,1); new_settings.c_cc[VMIN]=1; tcsetattr(0, TCSANOW, &new_settings); if(nread == 1) { peek_character = ch; return 1; } return 0; }
5 按下的按键是由下一个函数,readch,读取的,然后将peek_character为下一次循环设置为-1。
int readch() { char ch; if(peek_character != -1) { ch = peek_character; peek_character = -1; return ch; } read(0,&ch,1); return ch; }
当我们运行这个程序时,我们会得到下面的输出:
$ ./kbhit looping looping looping you hit h looping looping looping you hit d looping you hit q $
工作原理
在init_keyboard中配置终端在返回之前(MIN=1,TIME=0)读取一个字符。kbhit将其改变为检测输入并且立即返回(MIN=0,TIME=0),然后在程序退出前恢复原始设置。
注意,我们必须读取被按下的键,但是却是在局部存储,从而可以在需要的时候返回。
虚拟控制台
Linux提供了一个称之为虚拟控制台的特性。有许多的终端设备可用,所有的设备都共享PC的屏幕,键盘以及鼠标。通常,一个Linux的安装配置12个这样的虚拟控制台。
虚拟控制台是通过字符设备/dev/ttyN来访问的,在这里N是一个数字,由1开始。
如果我们的Linux系统使用文本登陆,那么在Linux启动运行时我们就会得到一个登陆提示。然后我们可以使用用户名与密码进行登陆。此时我们使用的设备就是第一个虚拟控制台,终端设备/dev/tty1。
使用who与ps命令,我们可以看到登陆的用户,shell与在这个虚拟控制台上正运行的程序:
$ who neil tty1 Mar 8 18:27 $ ps -e PID TTY TIME CMD 1092 tty1 00:00:00 login 1414 tty1 00:00:00 bash 1431 tty1 00:00:00 emacs
从这里我们可以看出登陆的用户为neil,并且在控制台设备/dev/tty1上运行Emacs。
Linux通常启动一个getty进程运行在前六个虚拟控制台上,这样就可以使用相同的屏幕,键盘与鼠标登陆六次。我们可以使用ps看到这些进程:
$ ps -e PID TTY TIME CMD 1092 tty1 00:00:00 login 1093 tty2 00:00:00 mingetty 1094 tty3 00:00:00 mingetty 1095 tty4 00:00:00 mingetty 1096 tty5 00:00:00 mingetty 1097 tty6 00:00:00 mingetty
在这里我们可以看到SuSE默认的getty程序,mingetty,运行在另外五个虚拟控制台上,等待用户登陆。
我们可以使用一个特殊的按键组合Ctrl+Alt+F<N>来在虚拟控制台之间进行切换,在这里N为我们要切换到的虚拟控制台号。所以要切换到第2个虚拟控制台,我们需要按下Alt+Ctrl+F2,而Ctrl+Alt+F1会返回到第一个控制台。(当由字符登陆而不是图形登陆切换时,Ctrl+F<N>组合也可以起作用)
如果Linux启动一个图形登陆,或者是通过startx或者是通过一个显示管理器,例如xdm,X Window系统将会使用第一个空闲的虚拟控制台来启动登陆,通常为/dev/tty7。当使用X Window系统时,我们可以使用Ctrl+Alt+F<N>切换到文本控制台,并使用Ctrl+Alt+F7返回到图形控制台。
通常在Linux上会运行多个会活。如果我们这样做,例如,使用命令
$ startx - :1
Linux会在下一个空闲的虚拟控制台上启动X服务器,在这种情况下,通常为/dev/tty8,然后我们可以使用Ctrl+Alt+F8与Ctrl+Alt+F7在他们之间进行切换。
在所有其他方面,虚拟控制台的行为与终端类似,正如我们在这一章所描述的。如果一个进程具有正确的权限,虚拟控制台可以使用与通常的终端相的方式执行打开,读取与写入等操作。
伪终端
许类Unix系统,包括Linux,具有一个名为伪终端的特性。这些设备的行为与我们在这一章所使用的终端相类似,所不同的是他们并没有与其相关联的硬件。他们可以用来为其他的程序提供一个类终端接口。
例如,使用两个伪终端就可以使得两个象棋程序彼此交战,尽管程序本身是设置用来与人在终端进行交互的。作为中介的程序将一个程序的动作传递给另一个。可以使得伪终端可以使得程序在不提供终端的情况下像通常一样运行。
伪终端曾经是以特定系统的方式实现的。现在他们已经进入Single Unix规范的Unix98伪终或是PTY标准中。
总结
我们了解了控制终端的三个不同方面。在这一章的第一部分,我们了解了重定向检测以及如何与一个终端交互,尽管标准的文件描述符已经进行重定向。我们讨论的硬件模型及其历史。然后我们了解了通用终端接口以及在Linux终端处理上提供详细控制的termios结构。我们也看到了如何使用termios数据库以及以独立于终端的方式管理屏幕的相关函数,同时我们也了解了立即检测按键。最后,我们讨论了Linux虚拟控制台以及伪终端。
本文整体自:http://blog.csdn.net/mylxiaoyi/article/details/2949286
Linux终端那件事儿