总述
一个操作系统必需要具有交互性。所谓交互性就是指人与计算机之间的一种交流,有两种最基本的交互设备,一是显示器,一是键盘。显示器是计算机用来与人进行交流的,计算机通过显示器来告诉人一些信息,而键盘是人用来与计算机进行交流的,人通过键盘将命令发送给计算机,计算机接受人通过键盘发来的命令,然后进行处理,最后将处理的结果通过显示器告诉人,这样就完成了一次最简单最基本的交互。在本篇中,笔才将从一个编写操作系统的角度去描述操作系统应当怎样处理键盘,让人可以通过键盘与计算机进行交互。在本篇中,我们同样将以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是一个很简单的系统。J
从上面的描述中我们可以看到,键盘有许多属性比如说响应时间,重复频率等需要进行设置,不过比较幸运的是,在计算机被引导后,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_Bufferstruct_pyos_Message KernelMessageQueue ;
/* 指向当前接收消息的队列的指针 */
class_template_pyos_Bufferstruct_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 i ){
??????????? buffer[ i ] = 0 ;
????????? }
????????? buffer_count = 0 ;
????????? /* 显示新的提示符 */
????????? class_pyos_Video::PrintMessage( "\n" ) ;
????????? class_pyos_Video::PrintMessage( "pyos" ) ;?????????
??????? }
??????? else{
????????? /* 如是可打印字符则直接打印 */
????????? if( ch= 32 && ch
??????????? 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的键盘中断读到键盘扫描码后,它不再构造一条消息放入内核的消息队列之中,而是直接调用当前处理键盘事件的进程的函数进行处理。
当然,目前这只是一个想法,还有很多细节问题没有想明白,比如说这个事件处理函数怎么获得?参数又通过什么方式传递?如果同时有多个事件,又怎么响应,怎么排队,怎么处理?非常欢迎各位朋友来信交流,指教:)
下面是本实验的一些载图
740)this.width=740" border=undefined原文:http://purec.binghua.com/Article/ShowArticle.asp?ArticleID=104