编写过MS-DOS程序的人通常都会查找Linux下等同于kbhit的函数,这个函数会检测一个按键是否被按下而并不实际的读取。不幸的是他们并没有找到这样的函数,因为并没有直接等同的函数。Unix程序员并不会注意到这个遗漏,因为Unix的编程方式通常为程序应准备好等待事件的发生。因为这就是通常的kbhit的用法,所以Unix和Linux将其忽略了。

然而,当我们要由MS-DOS移植程序时,通常需要模拟kbhit,此时我们可以用非正规输入模式来做到。

试验--我们自己的kbhit

1 首先我们需要定义标准的头文件并且为终端设置声明了一个结构。peek_character用于测试一个按键是否被按下。然后我们定义了我们将会用到的函数的原型。

#include 
#include 
#include 
#include 
#include 
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为我们要切换到的虚拟控制台号。所以要切换到第2个虚拟控制台,我们需要按下Alt+Ctrl+F2,而Ctrl+Alt+F1会返回到第一个控制台。(当由字符登陆而不是图形登陆切换时,Ctrl+F组合也可以起作用)

如果Linux启动一个图形登陆,或者是通过startx或者是通过一个显示管理器,例如xdm,X Window系统将会使用第一个空闲的虚拟控制台来启动登陆,通常为/dev/tty7。当使用X Window系统时,我们可以使用Ctrl+Alt+F切换到文本控制台,并使用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虚拟控制台以及伪终端。
Logo

更多推荐