编写操作系统之键盘交互的实现

来源:互联网 发布:java下载文件到服务器 编辑:程序博客网 时间:2024/06/06 12:27



编写操作系统之键盘交互的实现
Version 0.01
谢煜波
xieyubo@126.com
http://www.binghua.com
QQ:13916830
总述
一个操作系统必需要具有交互性。所谓交互性就是指人与计算机之间的一种交流,有
两种最基本的交互设备,一是显示器,一是键盘。显示器是计算机用来与人进行交流的,计
算机通过显示器来告诉人一些信息,而键盘是人用来与计算机进行交流的,人通过键盘将命
令发送给计算机,计算机接受人通过键盘发来的命令,然后进行处理,最后将处理的结果通
过显示器告诉人,这样就完成了一次最简单最基本的交互。在本篇中,笔才将从一个编写操
作系统的角度去描述操作系统应当怎样处理键盘,让人可以通过键盘与计算机进行交互。在
本篇中,我们同样将以pyos 做为我们的实验平台,在pyos 上实验操作系统对键盘的控制,
为了进行本次实验,笔者专门编写了一人非常简单的“推箱子”游戏,当pyos 启动后,你
可以输入“game(回车)”命令,然后系统就会调入这个游戏,这时,你可以通过上下左右
四个方向键进行游戏,游戏结束后,按“ESC”键,可以返回系统内核。所有的这一切你都
会从本篇中了解到去怎样实现它。
在本篇中,pyos 实现了类似windows 的一个消息系统,换句话说,此处的pyos 是由消
息驱动的,如果你能windows 的消息驱动很好奇,但又不知它是怎样实现的,那么,本篇
中所介绍的pyos 的消息驱动或许能对你理解windows 的消息戏动有些许帮助:)
本篇与前几篇实验报告是一脉相承的,特别是同《保护模式下中断编程》一篇有较为
紧密的相关性,你可以在“纯C 论坛”(http://purec.binghua.com)上的“操作系统实验”专
区找到它们,上面的内容对理解本篇会具有较大帮助。
在pyos 的编写过程中,得到了许多朋友的大力支持与帮助,中国安天实验室的张栗炜
老师指出了pyos 中存在的一些问题,并给出了怎样完全在windows 环境下进行pyos 开发的
方法,哈尔滨工业大学的kylix,sun 为本实现中曾经出现的奇怪现象提出了非常好的意见,
极大的扩展了笔者的思路及眼界,pineapple,lizhenguo 为本实验的调试花费了许多精力……
还有许多朋友给我发来电子邮件及在QQ 上留言,帮助并支持笔者进行实验,无法一一列举,
仅在此对上述关心pyos 的朋友致以最真诚的谢意与问候!
Ok,言归正传,下面就让我们开始我们的实验,Let’s Go!~~
键盘驱动简介
CPU 对外部设备的管理是通过中断程序进行的,键盘也是一种外部设备,因此,CPU
对键盘的管理也是通过中断进行的。当你击打键盘的时候,键盘控制器会向CPU 提出中断
申请,CPU 响应此中断进行处理,这就完成了一次很简单与人之间通过键盘进行的交互。
有关键盘的很详情的硬件控制说明,大家可以在纯C 论坛上找到一篇名为《PS/2 键盘
控制》的文章,这篇文章里非常详细描述了有关键盘的硬件控制。由于本文旨在从操作系统
的角度描述操作系统怎样通过键盘与人进行交互,因此,并不打算详细而完整的描述对键盘
控制器的控制方法,如果你想了解对键盘的更多控制细节,比如设定键盘响应时间,重复键
频率,键盘自检等,你会在《PS/2 键盘控制》中找到所有的内容。这里,仅就pyos 所用到
的一些细节进行简单的介绍,因为,pyos 是一个很简单的系统。?
从上面的描述中我们可以看到,键盘有许多属性比如说响应时间,重复频率等需要进
行设置,不过比较幸运的是,在计算机被引导后,BIOS 已经将这一切为我们做好了,如果
你不需要很特别的应用,使用BIOS 为我们设定的默认值对于pyos 这样的系统来说是足够
了,因此,我很乐意在这里将BIOS 进行的设置称之为键盘初始化,由于BIOS 的运行是在
操作系统运行之前进行的,因此,当操作系统被调入内存并运行时键盘已经完成了初始化,
这个时候键盘就处于等待用户输入的状态。在前面几篇中我们知道,键盘中断是连结在计算
机内部8259A 中断控制器的IRQ1 号线上,当有按键发生时键盘控制器将会在IRQ1 号线上
送出中断信号,8259A 中断控制器将此中断信号与其它外部设备通过其余的IRQ 线送来的
中断信号进行判优、排队,最后,将此信息送给CPU。CPU 在一条指令运行结束后,会查
询一下是否有中断信号送来,如果此时发现有中断信号送来,就会通过此中断信号的中断向
量在中断描述符表中查询应当使用哪一个中断处理程序。当找到中断处理程序后,CPU 将
调用此中断处理程序进行中断处理,完成中断处理后,CPU 再返回到原来被中断的程序处
继续执行。有关8259A 的初始化,中断向量及中断向量表的初始化的问题在上一篇实验报
告〈〈保护模式下的8259A 芯片编程及中断处理探究〉〉中已经详细描述过了,这里就不在
多费口舌了。现在我们只需要知道,CPU 通过8259A 发送来的键盘中断的中断向量号,在
中断向量表中调入了键盘中断程序进行键盘中断的处理。下面我们将集中精力,来看看键盘
中断程序到底都完成了些什么事情。
键盘中断程序概述
键盘中断程序到底要完成些什么事儿,这完全不是固定的。不同的系统对它的各个功
能模块所需要完成的工作有不同的划分,不过对于键盘中断程序,它所必须完成的任务就是
要告诉系统键盘上到底什么键被按下了,这是通过读取键盘控制器的一个端口完成的。
键盘上的每一个键都有两个唯一的数值进行标志。为什么要用两个数值而不是一个数
值呢?这是因为一个键可以被按下,也可以被释放。当一个键按下时,它们产生一个唯一的
数值,当一个键被释放时,它也会产生一个唯一的数值,我们把这些数值都保存在一张表里
面,到时候通过查表就可以知道是哪一个键被敲击,并且可以知道是它是被按下还是被释放
了。这些数值在系统中被称为键盘扫描码,而这张供查询用的表就被称为键盘扫描码集。最
早的键盘键数较少,而现在的键盘键数比以前多了不少,键盘键数的变化也引起了扫描码的
变化,历史上产生过三种键盘扫描码集,分别标记为Set 1,Set 2,Set 3,它们并没有什么
本质的不同,而区别只在于:对于键盘上的同一个键,所带表的扫描码不一样罢了。现代键
盘常常使用Set 2,但是为了兼容以前的老式键盘,BIOS 在启动的时候自动将键盘控制器设
置为工作在Set 1 模式下,因此键盘控制器会自动翻译由键盘数据线送来的扫描码,将它有
Set 2 中的码转换成Set 1 中的码。因此,传给CPU 或说操作系统的就是Set 1 中的扫描码了,
当然我们完全可以给键盘控制器发送命令,让它不进行这样的转换,不过这仿佛没有什么必
要。下面,我们就来看看Set 1 中的键盘扫描码:
Set 1 键盘扫描码
键 按下码 释放码----- 键 按下码释放码----- 键 按下码 释放码
A 1E 9E 9 0A 8A [ 1A 9A
B 30 B0 ` 29 89 INSERT E0,52 E0,D2
C 2E AE - 0C 8C HOME E0,47 E0,97
D 20 A0 = 0D 8D PG UP E0,49 E0,C9
E 12 92 / 2B AB DELETE E0,53 E0,D3
F 21 A1 BKSP 0E 8E END E0,4F E0,CF
G 22 A2 SPACE 39 B9 PG DN E0,51 E0,D1
H 23 A3 TAB 0F 8F U ARROW E0,48 E0,C8
I 17 97 CAPS 3A BA L ARROW E0,4B E0,CB
J 24 A4 L SHFT 2A AA D ARROW E0,50 E0,D0
K 25 A5 L CTRL 1D 9D R ARROW E0,4D E0,CD
L 26 A6 L GUI E0,5B E0,DB NUM 45 C5
M 32 B2 L ALT 38 B8 KP / E0,35 E0,B5
N 31 B1 R SHFT 36 B6 KP * 37 B7
O 18 98 R CTRL E0,1D E0,9D KP - 4A CA
P 19 99 R GUI E0,5C E0,DC KP + 4E CE
Q 10 19 R ALT E0,38 E0,B8 KP EN E0,1C E0,9C
R 13 93 APPS E0,5D E0,DD KP . 53 D3
S 1F 9F ENTER 1C 9C KP 0 52 D2
T 14 94 ESC 01 81 KP 1 4F CF
U 16 96 F1 3B BB KP 2 50 D0
V 2F AF F2 3C BC KP 3 51 D1
W 11 91 F3 3D BD KP 4 4B CB
X 2D AD F4 3E BE KP 5 4C CC
Y 15 95 F5 3F BF KP 6 4D CD
Z 2C AC F6 40 C0 KP 7 47 C7
0 0B 8B F7 41 C1 KP 8 48 C8
1 02 82 F8 42 C2 KP 9 49 C9
2 03 83 F9 43 C3 ] 1B 9B
3 04 84 F10 44 C4 ; 27 A7
4 05 85 F11 57 D7 ' 28 A8
5 06 86 F12 58 D8 , 33 B3
6 07 87
PRNT
SCRN
E0,2A,
E0,37
E0,B7,
E0,AA
. 34 B4
7 08 88 SCROLL 46 C6 / 35 B5
8 09 89 PAUSE
E1,1D,45
E1,9D,C5
当然,上面这张表并不完整,但是它包括了普通键盘上的绝大多数键,完整的Set 1 扫
描码集可以在前面提到的《PS/2 键盘控制》这篇文章中找到。从上面我们可以看出每一个
键的键盘扫描码都是不一样的,而且按下码与弹出码也是不一样的,而且它们只间并没有什
么规律可言。噢,这里所说的规律是只它们同它们所对应键的ASCII 码之间没有什么联系,
并不是说其它联系也一点没有。看一下A,S,D,F 这几个键的键盘扫描码值:1E,1F,
20,21,再看一下A,S,D,F 在键盘上的位置,呵呵,通过上面这张表,我们不难想像,
远古的键盘上的键是怎样排列的。:P
仔细再看看上面这张表,我们还能发现一些其它规律,比如,释放码都大于0x80,且
均为:按下码 + 0x80,有些键的扫描码是多个字节,对于两字节的扫描码,第一字节均是
E0。你也许还会发现其它更多的有意思的现象,不过,对于我们的实验来说,了解这些就
足够了。
这里你也许想问,为什么不直接用ASCII 码,而要用单独的扫描码呢?我想这个问题
一是由于扫描码对于键盘硬件更容易实现(从上面扫描码与键盘上键的排列方式之间的关系
就可以看出);二是由于ASCII 码数量是有限的,而且不能更改,但键盘上的键的个数却是
不定的,随着键盘的发展,键可能增加也可能减少;第三,比如说“Alt”键,在键盘上就
有左右两个,也许以后我们可能在程序中会实现“按左Alt,完成A 功能”,“按右Alt,完
成B 功能”的任务,如果使用ASCII 码,左右Alt 都会使CPU 收到同一个ASCII 码,CPU
也就无法分辩到底是“左Alt”还是“右Al”t 产生的了。
呵呵,言归正转,继续我们的描述。当键盘按下一个键或释放一个键的时候,键盘控
制器都会把这个键的相应的扫描码值放在0x60 这个端口寄存器中,并向CPU提出中断请求,
于是中断请求的主要任务就是读取0x60 端口的键盘扫描码值,而操作系统通过中断服务程
序读得的这个扫描码值,就能知道是哪个键被按下了,于是就可以进行相应的处理,完成与
用户的交互了。
0x60 端口只有一个字节大小,而在上面的扫描码中我们可以发现有些键的扫描码是多
个字节的。事实上,对于这种多个字节的扫描码,键盘控制器会向CPU 发出多个中断请求,
并依次发送它们。比如说,Insert 键的按下码是“E0 52”这两个字节,那么当Insert 键被按
下时,键盘控制器先把“E0”送到0x60 端口,然后申请中断,等待中断服务程序来读取。
当中断服务程序读取之后,键盘控制器会立即把第二个字节“52”送到0x60 端口,再一次
申请中断,让中断服务程序来读取。注意这样一个事实:只有在中断服务程序取走了0x60
端口的数据之后,键盘控制器才会向0x60 端口送入新的数据,才会申请新的中断。
有关中断服务程序的原理概述,描述到这里也就算描述完了,是不是非常的简单?多
说无宜,百闻不如一见,下面我们将用一个实际的例子来看看倒底应当怎么去编程实现,有
很多细节是说不清楚的,只有在代码中才可以窥见。下面就让我们用pyos 来进行这个实验,
完成一个能接收用户键盘输入的操作系统:)。
编写能接收键盘输入的pyos
我们先来简单看看pyos 的内核,也就是系统将pyos 调入内存中后,pyos 都干了些什么。
下面就是pyos 的内核核心代码。
const int KernelMessageMaxLength = 32 ; // 设定消息队列长度
/* 定义一个消息队列所用的缓冲区 */
struct_pyos_Message KernelMessage[ KernelMessageMaxLength ] ;
class_template_pyos_Buffer< struct_pyos_Message > KernelMessageQueue ;
/* 指向当前接收消息的队列的指针 */
class_template_pyos_Buffer< struct_pyos_Message >* ReceiveMessageQueue ;
/* 定义暂存用户输入的缓冲区 */
char buffer[ 4 ] ;
int buffer_count = 0 ;
/* 内核主函数 */
extern "C" void Pyos_Main()
{
/* 系统初始化 */
class_pyos_System::Init() ;
/* 清屏,打印欢迎信息 */
class_pyos_Video::ClearScreen() ;
class_pyos_Video::PrintMessage( "Welcome to pyos, you can input " ) ;
class_pyos_Video::PrintMessage( "game" , clRed , true , clBlue ) ;
class_pyos_Video::PrintMessage( " to play a game~~:):/n" ) ;
class_pyos_Video::PrintMessage( "pyos>" ) ;
/* 初始化 Kernel 所用的消息队列,也是全局中必须存在的一个消息队列 */
KernelMessageQueue.Init( KernelMessage , KernelMessageMaxLength ) ;
/* 定义 Kernel 的消息队列为当前消息接收队列,以接受键盘中断发来的消息 */
ReceiveMessageQueue = &KernelMessageQueue ;
/* 现在可以许可键盘中断了 */
class_pyos_Keyboard::Init() ; // 键盘类初始化
class_pyos_Keyboard::OpenInterrupt() ; // 打开键盘中断
/* 下面进入消息循环 */
struct_pyos_Message message ;
bool ExitMessageLoop = false ;
while( !ExitMessageLoop ){
/* 从消息队列中取出消息,ReadData 返回值表示所取得的消息个数,如果是 0 ,则表示未取到消息
*/
if( KernelMessageQueue.ReadData( message ) ){
if( message.MessageType == pyos_message_type_Keyboard ){ // 键盘消息
// 从消息中取得扫描码,并调用翻译函数翻译此扫描码,取得ascii码
char ch =
class_pyos_Keyboard::TraslateScanCodeToAsciiCode( message.KeyboardScanCode ) ;
/* 如果返回是 0 ,表示这是一个功能键 */
if( !class_pyos_Keyboard::StateKey.Down || ch == 0 ){ // 忽略释放键、功能键的扫描码
continue ;
}
else if( ch == '/n' ){ // 表示表户输入的是回车
/* 检查 buffer 是否是空 */
if( buffer[ 0 ] ){
/* 检查输入是否是 “game” */
if( buffer[ 0 ] == 'g' && buffer[ 1 ] == 'a' && buffer[ 2 ] == 'm' && buffer[ 3 ]
== 'e' ){
// 调入游戏
/* 初始化游戏,主要是初始化游戏的消息队列 */
GameInit() ;
/* 定义游戏为当前接收消息的队列 */
ReceiveMessageQueue = &GameMessageQueue ;
/* 调入游戏进行 */
game_push_box_main() ;
/* 游戏退出,恢复自己为当前接收消息的队列 */
ReceiveMessageQueue = &KernelMessageQueue ;
/* 清屏,打印信息 */
class_pyos_Video::ClearScreen() ;
class_pyos_Video::PrintMessage( "GameOver~~:P/n" ) ;
}
else{
/* 输出错误信息 */
class_pyos_Video::PrintMessage( "/nyour input is not a command :(" , clBrown ) ;
}
}
/* 因为是回车键,所以清空用户的输入缓冲区 */
for( int i = 0 ; i < 4 ; ++i ){
buffer[ i ] = 0 ;
}
buffer_count = 0 ;
/* 显示新的提示符 */
class_pyos_Video::PrintMessage( "/n" ) ;
class_pyos_Video::PrintMessage( "pyos>" ) ;
}
else{
/* 如是可打印字符则直接打印 */
if( ch>= 32 && ch <= 126 ){
buffer[ buffer_count++ % 4 ] = ch ;
class_pyos_Video::PrintMessage( ch ) ;
}
}
}
}
__asm__( "hlt" ) ; // 停机,释放cpu的占用率
}
}
代码虽然长了一些,但是逻辑上非常清楚,是比较易读的。我们这就来一步步的看看。
首先进入Pyos_Main() 函数之后,系统调用了class_pyos_System::Init() ; 来进行内核
的初始化,它完成了全局描述符表及中断向量表的初始化工作,这部份工作已经再前几篇实
验报告中有详细描述,这里就不在多说了,你可以看看它的源代码以了解它们都完成了什么
任务。
内核初始化完成以后,系统调用了class_pyos_Video::PrintMessage() 打印一些欢迎信息,
之后,系统为内核建立了一个消息队列,并定义了一个指向当前接受消息的消息队列的指针,
使它指向系统的消息队列,以表明当前是由系统内核的消息队列接受消息,这是由
/* 初始化 Kernel 所用的消息队列,也是全局中必须存在的一个消息队列 */
KernelMessageQueue.Init( KernelMessage , KernelMessageMaxLength ) ;
/* 定义 Kernel 的消息队列为当前消息接收队列,以接受键盘中断发来的消息 */
ReceiveMessageQueue = &KernelMessageQueue ;
这两个语句完成的。对于消息队列,由于是在此实验中新实现的,故我下面将详细介绍一下。
这个实验的pyos 是采用的消息驱动,简单来说,就是系统完成初始化工作之后就进入
一个消息循环,然后不停的在探险测消息循环中是否有消息在存,如果有消息则取出。消息
是一个结构体,它有一个成员被用着标志消息类别,另外一些成员被用着消息的参数,消息
类别用来告诉消息的接受者这是一个什么消息,消息的参数用来向消息的接收者传递数据。
当应用程序取得消息后,则可以根据消息的类别进行一些处理。
Pyos 的消息队列是用一个循队列实现的,这个循环队列的定义在buffer.h 文件中,它定
义了两个最重要的函数,一个是PutData(),用于把消息放入队列,还有一个是GetData()用
于从消息队列中取出消息。有关循环队列的原理及实现,任何一本讲数据结构的书上都有很
详细的描述,大家对此想必也非常熟悉,这里就不详细论述了,有兴趣的朋友可以看看
buffer.h 中的源代码,上面有很详细的注释。
Pyos 的键盘中断程序主要工作就是从键盘控制器的0x60 端口读取键盘扫描码,然后,
它构造一条消息,让这个消息的消息类型是键盘事件,让这个消息的参数是读到的键盘扫描
码,然后把这条消息放入到内核的消息队列中,它的工作就完成了。
当键盘中断程序把一条消息放入内核的消息队列中后,主程序在下一次循环中检测消
息队列时就会发现消息队列中有消息,然后内核调用GetData()函数取得消息,然后对此
消息进行处理,这点在上面的代码中是非常清晰的。
下面,我们将去看看本实验的核心内容,pyos 的键盘处理程序。
Pyos 的键盘处理程序
在上面的程序中我们可以看见,当内核进行消息循环之前,还执行了下面两条语句:
class_pyos_Keyboard::Init() ; // 键盘类初始化
class_pyos_Keyboard::OpenInterrupt() ; // 打开键盘中断
这两条语句的作用就在于初始化键盘中断,并打开键盘中断,即允许键盘向cpu 提出中
断请求(在pyos 刚引导的时候,是屏蔽了所有中断的)。下面我们来看看这两个函数倒底完
成了些什么工作,首先来看第一个函数:
/* 初始化键盘类 */
void class_pyos_Keyboard::Init()
{
/* 初始化状态键 */
StateKey.Down = false ;
StateKey.AltDown = false ;
StateKey.CtrlDown = false ;
StateKey.ShiftDown = false ;
/* 安装键盘中断,中断号为 0x21 */
class_pyos_Interrupt::InstallInterrupt( 0x21 , ( void
* )pyos_asm_interrupt_handle_for_keyboard ) ;
/* 这里不许可键盘中断,因为有可能在许可键盘中断前主调用程序会准备一些其它资源,因此当主调用
程序把资料
** 准备好之后,自行调用 OpenInterrupt 函数许可中断
*/
}
程序中的注释已经非常详尽了,需要另外说明的是pyos 用了一个StateKey 的结构体来
保存键盘状态,比如说按下的是哪一个功能键,Shift,Alt,Ctrl 是否是被按下,这是一个按
下键还是释放键等。在函数开头是将各种状态设为初始值,之后,它调用了一个
InstallInterrupt()函数来安装键盘中断。
Pyos 的中断是通过安装方式进行的。在前几篇实验报告中我们已经知道操作统在内存
中建立了一张中断向量表,为一个表项都存放着一个中断描述符,而每一个中断描述符都含
有一个相应的中断处理函数的函数指针。当发生中断时就通过该中断的中断向量在这个中断
向量表中取得相应的中断描述符,进而取得了相应的中断处理函数的函数指针,于是就可以
调用中断处理程序进行中断处理。在pyos 最初初始化的时候, 中断向量表是与
class_pyos_Interrupt::Init()函数中建立的,当时系统并不知道会有哪些中断,以及它们的中断
处理函数的函数指针是什么,于是系统统一将每一个中断(cpu 共支持256 个中断)的中断
描述符都置成一个默认的中断描述符,而在需要的时候在由各个中断服务程序自行安装。所
谓安装,其实就是用一个新的中断描述符去替换中断向量表中的旧的中断描述符,这就是
InstallInterrupt()函数所完成的工作,当中断安装完成以后,系统只是更新了中断向量表中的
相应表项,因此紧接着,系统调用了OpenInterrupt()函数来打开键盘中断,所谓打开中断,
其实就是重新设置中断控制器8259A 的屏蔽寄存器,使之不再屏蔽键盘中断而矣,下面就
是OpenInterrupt()函数的代码:
/* 打开键盘中断 */
void class_pyos_Keyboard::OpenInterrupt()
{
/* 许可键盘中断 */
class_pyos_System::ToPort( 0x21 , 0xfd ) ;
}
非常简单,下面我想我们可以来看看我们最重的的一个部份,实际被cpu 所调用的中断
处理函数:
/* 中断实际处理函数 */
void class_pyos_Keyboard::HandleInterrupt()
{
// 从键盘控制器的0x60端口读入扫描码,否则键盘不会第二次中断,因为键盘在等待用户从0x60口读走
数据
unsigned char ch = class_pyos_System::FromPort( 0x60 ) ;
// 先过滤掉特殊字节
if( ch == 0x0 ){ // 0x0 表示按键产生错误
return ;
}
else if( ch == 0xe0 ){ // 检测是否是扩展码的前缀
// 设置 扩展码标记位
ex = true ;
// 直接返回,等待下一个中断送来真实的扫描码
return ;
}
else{
// 构造一个键盘消息
struct_pyos_Message message ;
message.MessageType = pyos_message_type_Keyboard ;
if( ex == true ){
message.KeyboardScanCode.KeyboardScanCodeHighChar = 0xe0 ; // 让高字节为扩展码
}
else{
message.KeyboardScanCode.KeyboardScanCodeHighChar = 0 ;
}
ex = 0 ; // 清空扩展码标志
message.KeyboardScanCode.KeyboardScanCodeLowChar = ch ; // 让低字节为送来的真实扫描码
/* 发送消息给应用程序 , ReceiveMessageQueue指向的是接收消息的队列 */
ReceiveMessageQueue->PutData( message ) ;
}
}
有了前面所介绍的背景,再来看这个函数,就非常简单了,因此,也就不在此处多费
口舌了,直接进入下一个环节。
Pyos 对用户输入的处理
其实写到这里,本篇所要描述的主要问题基本上是已经描述完成了,虽然还有一些小
细节,比如pyos 是怎样识别及保存用户输入的。
首先来看看pyos 是怎样识别用户输入的。当内核从消息队列中取得消息后,就从这个
消息的参数中取得了扫描码,然后,系统调用了TranslateScanCodeToAsciiCode() 函数将这
个扫描码翻译为Ascii 码,这其实就是一个查询Set 1 表的过程,有兴趣的朋友可以看看它
的源代码。如果所按下的是一个功能键,TranslateScanCodeToAsciiCode() 会返回 0 ,然后
将功能键状态保存在StateKey 中,否则,就返回这个键的Ascii 码。这样内核就识别出了用
户的输入。
对于pyos 是怎样保存用户输入的这个问题,在本实验中,pyos 用的是一种最简单最笨
最烂最不具扩展性的方法:P,pyos 定义了一个数组 buffer ,用于保存用户输入,我们估
且称这个数组为用户输入缓冲区,它依次将用刻的输入放入缓冲区,然后当用户输入回车的
时候,就检测buffer 中存的值是不是“game”,如果是,则认为用户在输入“game”会按
下了回车,也即用户输入了“game”命令,于是就调入游戏程序,如果不是则认为用户输
入了一个不可识别的命令,于是输出一个错误信息,并清空了缓冲区,等待用户下一次输入。
这里有个细节需要提一下,当调入游戏程序后,用户键盘消息就应当被游戏程序而不
是内核接收了,于是此时,游戏程序的消息队列成为了当前接收消息的队列,因此,在正式
调入游戏程序之前,系统还执行了下面两条语句:
/* 初始化游戏,主要是初始化游戏的消息队列 */
GameInit() ;
/* 定义游戏为当前接收消息的队列 */
ReceiveMessageQueue = &GameMessageQueue ;
由于游戏只是一个演示,而并不是我们实验的主角,因此对游戏是怎样实现的就不在
此处详细描述了,但其所用的原理完全与本篇报告介绍的相同,源代码中亦有很详尽的注释,
如果感兴趣可以看看,呵呵,非常欢迎你为pyos 编写游戏^_^
对pyos 消息驱动机制的思考
我没有机会阅读到windows 的源码,因此不知道windows 中消息驱动机制到底是怎样
实现的,或者说它做了什么优化,总之,我在实现pyos 的消息机制时候,产生了一些疑问
与想法,写在这里,希望能与大家一起讨论。
我个人认为消息机制有一些弊端,比如我们来看看pyos 中的消息机制,首先,内核中
有一个大循环,此循环不停的去消息队列中取出消息,然后通过switch() 函数检测消息的类
型,然后转到相应的处理语句进行处理。
switch() 语句经过编译器编译后,会产生许多的cmp,jne,jmp 之类的指令,jmp 之类
的跳转指令会极大的破坏cpu 的流水机制,使cpu 预取的指令报废,并且使cache 的失效几
率增大,因此会使系统的效率下降,试想一下,如果你有上万种的消息,而把一个会被频繁
触发的消息放在了switch() 的最后一个case 语句中,那么这个语句在被执行之前会首先执
行一万次的cmp 与jmp,这种效率应当是相当低下的!(注:对于cpu 的流水机制问题,我
目前是根据所学的教科书上的cpu 组成原理所描述的情形进行推断的,具kylix 指教,现在
的cpu 对于流水机制有了一些新的特性,Intel 在这方面还做了很多优化,但我没有此方面的
资料,亦不知道Intel 倒底做了些什么优化,因此这里仍按所学知识进行推断,如果您对此
方面深有研究,非常希望您能指点指点我)
第二,消息是一个预先定义好的结构体,它的参数数量及类型是固定的。比如说windows
中,消息就只有两个参数。但两个参数够不够呢?有些消息也许不需要参数,有些消息只需
要一个参数,而有的消息会需要更多的参数,因为它们或许有更多的数据需要传递给消息的
接受者。对于这种情况,就只能采用一些其它的办法处理,比如说把一个参数较多的消息拆
分成多个参数较少的消息分别发送,或者用一些很复杂的位操作,把许多数据集成到消息参
数的一个字节里面,然后再通过一些翻译函数取出这些数据,比如说我们常常在windows
下见到的TranslateMessage() 之类的翻译消息的函数,而这显然加重了编程人员的负担,并
且程序的易读性,简洁性都受到了影响。
那么怎么解决上面的问题呢?我目前的想法是在以后的pyos 系统中,把pyos 改造成一
个以事件驱动的内核,而不是消息驱动。所谓事件驱动,我的理解是,由事件的触发者调用
事件的处理函数进行处理,而不是发送一条消息给这个处理函数。举个简单的例子,当pyos
的键盘中断读到键盘扫描码后,它不再构造一条消息放入内核的消息队列之中,而是直接调
用当前处理键盘事件的进程的函数进行处理。
当然,目前这只是一个想法,还有很多细节问题没有想明白,比如说这个事件处理函
数怎么获得?参数又通过什么方式传递?如果同时有多个事件,又怎么响应,怎么排队,怎
么处理?非常欢迎各位朋友来信交流,指教:)
 
原创粉丝点击