操作系统中键盘驱动的讲解
在这一讲中,我将为大家讲解键盘相关内容 。从上一讲开始,我们进入了操作系统第四个部分的学习,也就是操作系统对设备的驱动与管理。
上一讲我们探讨的是显示器,并且提到,一个终端设备是由显示器和键盘共同组成的,显示器属于终端设备的输出部分,我们在显示器上看到的内容就是其输出结果。而键盘,是我们进行最基本输入操作的设备 ,所以这一节,我要为大家讲解的就是键盘是如何被驱动的,以及操作系统是怎样使用键盘的。
外设工作基本原理回顾
要讲述键盘的故事,我们仍然需要从回顾上次讲的外设工作的基本原理开始。从上次课到今天的内容,再到下次课要讲的磁盘的使用和管理,都离不开那张外设工作的基本原理图。
上节课我讲过,外设工作的基本原理非常简单。外设的工作流程是:CPU发出一条读或写的指令,当外设工作完成后,会向CPU返回一个中断。无论是终端设备还是其他任何外设,基本思路都是如此。
想要实现外设的驱动,需要做三件事:
- 核心部分:向设备控制器的某些寄存器端口或存储区域发出读写指令,这是计算机使用外设的核心,通常表现为几条指令。但要写出这些指令,必须对硬件有深入了解,清楚硬件的端口、指令格式及含义等,这些细节处理起来很麻烦,虽然原理简单,但实际操作复杂。
- 统一文件视图:操作系统为了隐蔽硬件细节,将所有外设都以文件的形式呈现。根据文件名或文件对应的结构信息,决定最终执行哪一段与硬件交互的代码,将底层与硬件交互的代码通过文件方式进行封装,这是操作系统进行外设管理的第二个重要方面。
- 中断处理:通常在设备完成读写操作后,进行后续相关工作,这是外设驱动的第三个部分。
将这三个部分弄明白,对于任何外设的管理原理也就基本掌握了 。简单总结一下,第一部分是通过out
指令向外设发送命令;第二部分是形成统一的文件视图;第三部分是进行中断处理 。掌握了这三个部分,整个外设工作原理就能清晰明了。
键盘驱动与操作系统对键盘的使用
对于键盘而言,它正好对应着中断处理这一部分。因为键盘的操作是由用户发起的,用户按下按键后就会产生中断,然后进行中断处理,最终将信息传递回上层的文件系统。这也再次印证了,只要理解了外设驱动的三个核心部分,学习任何设备的驱动原理都是相通的。
讲完今天关于键盘的内容,我们就能弄清楚计算机的输入输出原理。上一讲的显示器相当于输出设备,今天的键盘则是输入设备,当这两个设备的原理都搞清楚后,一台基本的计算机就能够正常使用了,即便没有驱动磁盘,只要有CPU、内存、键盘和显示器,计算机也可以运行。
那么,关于键盘的故事该从哪里开始讲起呢?用户敲下按键后会产生什么结果,又会触发哪些操作呢?答案是中断,按下按键后就会触发中断,所以整个故事应该从中断处理开始 。
从硬件参与计算的角度来看,有两条路径:一条是从CPU发起,通过out
指令向外设发送指令;另一条是从硬件产生中断信号,向CPU进行中断请求 。而这两条路径最终都会回归到文件系统的统一视图上。今天我们要讲的就是第二条路,从中断开始,因为设备(键盘)一旦有操作就会产生中断。当然,从中断开始,首先得了解中断初始化,即键盘中断被初始化成了什么函数,这个函数又负责做什么事情。
通过查阅手册可知,键盘中断对应21号中断。通过设置,将21号中断关联到特定的处理程序,当发生中断时,就会执行这个中断处理函数,键盘的故事也就由此展开。
中断处理函数的工作
接下来,我们看看中断处理函数具体做了哪些事情。中断处理函数的第一句是in
指令,有out
指令用于输出字节,相对应的,in
指令就是读入一个字节。通过这条核心指令,我们可以将键盘控制器中60端口的内容读取到AL
寄存器中。要理解这一步,需要具备一定的硬件知识,因为in
和out
这类与CPU和设备控制器寄存器交互的指令,都需要参考硬件手册,明确端口对应的含义。在这里,60端口存放的是扫描码,通过in
指令,扫描码就被存储到了AL
(在AX
寄存器中)。
获取扫描码后,根据不同的扫描码,需要调用相应的table
来执行对应的操作。这很好理解,不同的按键对应不同的扫描码,也就需要执行不同的操作。比如,如果扫描码对应的是字母"a",那就产生字符"a"的ASCII码,然后向上传递;如果是其他按键,如大小写切换键,就执行相应的功能操作。
现在,我们已经有了扫描码,接下来就要根据扫描码获取对应的ASCII码,这就需要用到keymap
。keymap
其实就是一个存储着各种可显示字符ASCII码的表,例如数字、字母,以及按下Shift
键时对应的特殊字符(如叹号、@、#等)的ASCII码 。将keymap
表的起始地址存入EBX
寄存器,再结合刚才获取的扫描码(作为表的偏移量),就可以找到按下按键所对应的ASCII码,并将其赋值给AL
寄存器。至此,我们就成功得到了按键对应的ASCII码。
ASCII码的处理与存储
得到ASCII码后,下一步就是将其放置到合适的位置。显然,ASCII码需要被放到缓冲队列中,就像上一次讲显示器输出时,数据会被写入缓冲队列等待处理一样。在这里,键盘输入的ASCII码会通过put_queue
操作,放入read_q
队列中。put_queue
的操作过程也很简单,就是获取read_q
队列的头部指针,然后将ASCII码存储到队列头部。
到这一步,似乎键盘输入的处理已经完成了,将得到的ASCII码放入队列中,上层程序(如执行scanf
的进程)在需要时就可以从队列中获取数据。这部分与显示器输出时数据写入队列,以及上层程序通过printf
等函数从队列获取数据的原理是一样的,只是这里是输入操作,将write
相关操作替换为read
操作即可。
键盘输入的"回显"处理
不过,我们还需要考虑一个环节,那就是"回显"。"回显"其实就是对输入字符进行一些处理后,将其再次进行输出显示等操作 。在将字符放入队列时,对于一些可显示字符,通常需要进行"回显"操作,也就是将其显示在屏幕上。
"回显"的过程和前面将ASCII码放入read_q
队列类似,同样是将字符数据写入相关队列(这里仍然是read_q
队列),然后在"回显"操作中,通过调用console_write
函数,将字符显示在屏幕上。
总结与拓展思考
我们可以总结一下键盘处理的整个流程:从硬件产生中断开始,中断处理程序获取扫描码,并将其转换为ASCII码,然后将ASCII码放入read_q
队列中,在这个过程中可能会涉及一些中间处理和"回显"操作,最终上层程序从队列中获取数据进行后续处理 。
将键盘的处理和上次显示器的处理结合起来看,我们会发现它们都通过文件接口进行操作。输出时使用tty_write
,输入时使用tty_read
,分别操作write_q
和read_q
队列 。这再次印证了操作系统进行设备处理的三个核心部分:通过out
指令与硬件交互、形成统一的文件视图、进行中断处理。无论是输入设备还是输出设备,其驱动原理都围绕这三个方面展开。
最后,我再给大家留下一个思考和实践的小任务。在上次的内容中,我们提到按下F12键要输出星号。从今天讲解的键盘驱动原理出发,实现这个功能其实并不难。当前按下F12键时,是由func
函数进行处理的,我们只需要将其修改为自定义的函数(如my_func
)。在my_func
函数中,我们可以设置一个标志位flag
,按下一次F12键,将flag
置为1,此时输出全为星号;再按一次F12键,将flag
置为0,恢复正常输出。同时,在tty_write
函数中,根据flag
的值判断是否将输出字符替换为星号 。大家可以在实验中尝试实现这个功能,通过实践,对操作系统设备驱动的理解将会上升到一个新的层次。