我们的操作系统虽然已经实现了键盘驱动,但其功能仅限于在屏幕上打印输入的字符,任务并不能读取到这些字符。本章将要实现读取键盘输入的系统调用。
16.1 读取键盘输入的原理
想要让任务读取到键盘输入,最简单的方法是构造一个数组,当键盘中断发生时,将键盘输入的字符保存在这个数组中。然而,这个方案有一个无法解决的问题:如果一个任务想要读取键盘输入,但此时数组是空的,该怎么办?
想要解决这个问题,就需要一个具有等待功能的数组。当任务无法在数组中读取到字符时,使任务等待;当数组中又有字符时,将任务唤醒。处于等待中的任务不能运行,所以其不能出现在任务队列中,而是应该出现在与这个数组配套使用的等待队列中;当数组中又有字符时,将此任务从等待队列中取出,并重新添加到任务队列中。这样,就实现了任务的等待与唤醒。在我们的操作系统中,这种具有等待功能的数组被称为IO队列。
16.2 读取键盘输入的实现
16.2.1 IO队列的实现
现在,请看本章代码16/IOQueue.h
。
第6\~12行,定义了IOQueue
结构体。IO队列的实现使用了双指针算法,其内部包含一个16字节的字符缓冲区,以及两个索引值。__leftIdx
是慢指针,用于读取字符;__rightIdx
是快指针,用于存储字符。此外,IO队列还带有一个专用的等待队列,用于存储在IO队列中等待的任务。
第15\~19行,声明了IO队列的各种函数。
在实现IO队列之前需要先思考一个问题:当任务需要在IO队列上等待时,就需要主动发起0x20
中断以切换到一个新任务。考察Int.s
中的intTimer
函数可以发现,其核心逻辑如下:
- 将当前任务添加到任务队列中
- 从任务队列中取出一个新任务并切换
在此之前,由于任务要么出现在任务队列中,要么出现在退出队列中,所以上述逻辑没有任何问题。但引入了等待队列后,对于需要等待的任务,其任务切换应该是这样的:
- 将当前任务添加到等待队列中
- 从任务队列中取出一个新任务并切换
也就是说,现在的intTimer
函数已经不能硬编码"将当前任务添加到任务队列中"了,TCB应负责提供这一信息。
请看本章代码16/Task.h
。
第12行,新增了数据成员taskQueue
,其用于控制:当任务切换时,当前任务应该被添加到哪个队列中。
接下来,请看本章代码16/Task.hpp
。
对于内核任务以及新任务来说,taskQueue
数据成员的初始值都应该是任务队列。因此,第43行,第93行,第136行,分别添加了对taskQueue
数据成员的初始化。
接下来,请看本章代码16/Int.s
。
第128行,将原先的push taskQueue
修改为push dword [eax + 16]
,[eax + 16]
即为TCB中新增的taskQueue
数据成员。
接下来,请看本章代码16/IOQueue.hpp
。
ioqueueInit
函数用于初始化IO队列。当this->__leftIdx == this->__rightIdx
时,表示IO队列为空。
ioqueueEmpty
函数用于判断IO队列是否为空。
ioqueueFull
函数用于判断IO队列是否已满。如果快指针已经紧跟在慢指针后面,就说明IO队列已满。
ioqueuePush
函数用于向IO队列添加字符。
第31行,判断IO队列是否已满。如果IO队列已满,则放弃此次添加。
第33\~34行,将新添加的字符写入快指针处,然后将快指针向右移动一格。
将一个字符写入IO队列后,不管写入是否成功,IO队列都一定非空。所以,如果先前有在IO队列上等待的任务,此时就是唤醒它的时机。
第37\~42行的代码需要配合ioqueuePop
函数的实现阅读。
第37行,判断是否有任务在IO队列上等待。
第39行,从IO队列的等待队列中取出一个TCB。
第40行,将取出的TCB中的taskQueue
数据成员重新恢复为任务队列。
第41行,将取出的TCB重新添加到任务队列。
ioqueuePop
函数用于从IO队列中取出一个字符。
第48行,判断IO队列是否为空。如果IO队列为空,就说明暂时还无法取出字符。
第50\~52行,当前任务的TCB中的taskQueue
数据成员修改为IO队列的等待队列,然后主动发起任务切换。这样一来,当前任务就会被添加到IO队列的等待队列中,任务队列中的下一个任务将开始执行。
第48行为什么要用while
而不是if
呢?这是因为,当唤醒任务时(第39\~41行),只是将任务重新添加到任务队列中,而不是使任务瞬间开始继续执行。这就意味着,如果这个任务运气不好的话,IO队列中新添加的字符又会被其他任务先行抢走。此时,如果不使用while
,任务就会在被唤醒后直接向下执行,而此时的IO队列是空的,这就造成了错误。这就是并行编程领域非常有名的伪唤醒(Spurious wakeup)问题。
16.2.2 读取字符串的系统调用的实现
请看本章代码Keyboard.h
。
第6行,声明了keyboardInit
函数。
第8行,声明了inputStr
函数。
接下来,请看本章代码Keyboard.hpp
。
第8行,定义了供键盘驱动使用的IO队列。
keyboardInit
函数是本章新增的函数,其用于初始化键盘IO队列。
IO队列的实现属于典型的生产者-消费者模型。生产者向IO队列添加字符,并唤醒在IO队列上等待的消费者;消费者从IO队列中读取字符,并在无法读取时在IO队列上等待。
具体来说,IO队列的生产者是键盘。当键盘中断发生时,键盘上按下的键被添加到IO队列中。
第48行,将键盘上按下的键添加到IO队列中。这个字符应该如何使用,由取出它的任务决定。
IO队列的消费者是inputStr
函数,这是一个系统调用函数。
第56行,循环strLen - 1
次,这是因为输入字符串的最后一个字符应强制设为0。
第58行,从IO队列中取出一个字符。这步操作可能导致任务等待。
第60行,将取出的字符写入结果字符串。
第62\~67行,判断当前字符是否是\n
,如果是,则意味着输入字符串已经结束了,此行为和C语言标准库的gets
函数是一致的。此时,应将结果字符串的下一个字符置0,以终止字符串;然后打印换行符并终止循环。
第68\~75行,判断当前字符是否是\b
。\b
的处理需要注意溢出问题:仅当idx
大于0时,才能进行一次退格。
第76\~80行,处理普通字符。
第83行,将输入字符串的最后一个字符置0。
接下来,请看本章代码16/Int.s
。
第6行,声明了外部链接的inputStr
函数。
第269行,在系统调用表中安装inputStr
函数,其系统调用号为1。
接下来,请看本章代码16/Kernel.c
。
第19行,调用keyboardInit
函数,完成键盘驱动的初始化。
16.3 测试
本章使用的测试任务是16/Test.c
。其先读取字符串,再打印读取到的字符串。
**粗体** _斜体_ [链接](http://example.com) `代码` - 列表 > 引用
。你还可以使用@
来通知其他用户。