《操作系统真象还原》chapter10 笔记与思考
Last Update:
PART 1 <锁Lock>
首先是上一章的地址访问错误问题。首先回忆一下本书的打印字符串功能是如何实现的:
- 读取当前光标值
- 将光标值转换为坐标值
- 向坐标写入字符
- 更新光标值
其中,更新光标的过程如下:
- 通知光标寄存器将要设置高8位
- 设置高8位
- 通知光标寄存器将要设置低8位
- 设置低8位
接下来,当引入多线程以后,设想如下一个执行过程:首先假设存在线程A和线程B
- 线程A尝试打印字符,打印结束以后,线程A进入第四步更新光标
- 更新光标时,当执行到通知设置低8位时,中断发生,切换到线程B
此时,光标的坐标才刚设置了新的高8位,低8位已经被计算出来了,但还没能设置到寄存器中。但如果没有换行等,尚且不会影响到以后的打印,因为高位坐标浮动不大。
- 线程B也需要打印字符,它也走到更新光标的时候。当它通知寄存器接下来要设置高8位时,发生中断,切换到线程A
- 现在,光标寄存器以为接下来要设置高8位,而线程A则继续执行设置步骤,将本应该设置到低8位的值放到高位去了。
低位的浮动极大,诸如0xfc这样的值被放进高位,将直接导致内存访问异常。
那么朴素一点的解决方法就是,别让线程在打印的时候被中断。但这也不太现实,因为不只是打印字符串函数会这样,所有需要访问全局资源的函数都可能出现这个问题。并且这些函数也往往都是些底层函数,这样做对debug来说似乎不太友好,层层封装还有额外消耗,所以针对资源访问,引入一个锁来替代关闭中断。
锁的思路也不复杂:
首先,为全局资源添加一个锁(体现为结构体,其中带有一个value)。
接下来,任何线程尝试访问该资源时,首先查看资源是否已经上锁。若上锁,则直接将自己阻塞(加入该锁本身的阻塞队列),等待直到被唤醒为止;若未上锁,则获得该锁以防其他线程也访问该资源,然后将所有事情做完以后,释放该锁,然后主动去唤醒阻塞队列中的线程。
上面的过程是没有关中断的,显然,它仍然是能够被调度的,那些不需要访问该资源的线程自然就不会因为你需要访问全局资源而被卡脖子了。而那些需要访问本资源的线程则会在尝试访问时因为锁已经被获取了,所以陷入阻塞状态,直到当前拿着锁的线程释放锁以后主动唤醒自己。
具体的代码实现如下:
1 |
|
通过信号量来实现锁的功能。本书的方法如下:
信号量初值为1。当有线程需要获得锁时,先将信号量减一,然后把holder指向自己的PCB;而释放锁则需要先将holder指向NULL,然后将信号量加一。
而检测锁的方式是在需要操作信号量的时候进行一次判断:
减一时:如果信号量为0,则表示锁已经被取走,将自己加入锁的等待队列以后阻塞自己。
加一时:如果等待队列非空,那就唤醒等待队列里的第一个线程,然后把信号量增加
如上两个操作均需要在关闭中断的情况下进行,即原子操作
至于唤醒和阻塞的实现也同样不复杂:
阻塞:将线程从调度器的调度队列里摘出,加入等待队列。
唤醒:将线程从等待队列里摘出,加入调度器的调度队列。
现在我们就知道是什么情况了。只要没有线程去唤醒这些被阻塞的线程,它们就永远不会被调度器选中。那么最开始的问题是解决了,只需要把put_str这样的函数再封装一次,在外部加上获取锁和释放锁的操作,就能保证不会有上面那样的错误出现了;而对于不需要打印字符串的线程也能够正常的进行操作。
PART 2 <键盘驱动>
虽然本章第二节开始都在讲这个,但概况起来看,内容不是很多,个人认为更多的是一些了解性的知识。
在中断那章有注意到,8259A芯片的IRQ1对应的就是键盘中断了。键盘每次击键时都会发生若干次中断,具体过程如下:
键盘内置的8048芯片在每次按下按键时,会根据不同的按键向8042芯片发送对应的扫描码,同时在松开按键的时候也会发送不同的扫描码。
8042芯片将该扫描码转换成兼容早期键盘的第一套扫描码后,将其送到固定的端口,同时触发8259A芯片的中断。
(注:扫描码多为单字节,但也存在多字节扫描码,每传输一个字节的扫描码就需要触发一次中断,因此一个按键就可能触发多次中断)
8259A芯片在接收中断以后做对应的处理。键盘驱动似乎就是对应的中断处理函数 **(笔者还不确定这么说是否合适)**。
扫描码如下:注释中标明了每列的意思。
1 |
|
但需要注意的是,只有处理器每次从0x60号端口取走一字节的扫描码以后,键盘才会触发下一次中断。所以对于一些组合键或是多字节扫描码的按键来说,需要多次中断才能判明用户的行为。所以中断处理函数似乎变得有些臃肿。
注:同一个按键按下时产生“通码”,松开时产生“断码”。通码和断码从开始就设计好了,它们只差了二进制数的第七位。对于第七位为0的数是通码,为1的则为断码。下述代码的break_code就是断码,make_code是通码。
1 |
|
然后将该函数注册到IDT中即可。但是我们只是让自己敲的键盘字符出现的屏幕上,并不是像shell那样能被读取。这些字符并不是出现在缓冲区里的,所以本书最后一节实现了一个简单的环形缓冲区,用以暂存从键盘上输入的字符,让其他程序能够从该缓冲区里读出用户键入的数据。
注:上面的intr_keyboard_handler来自本章最后一节,实际上它已经实现了缓冲区了。通过ioq_putchar函数将数据放入缓冲区中。
PART 3 <环形缓冲区>
最后一节也没有太多内容了,关于环形缓冲区的思路随便搜一下就能找到。还有关于生产者和消费者的问题,个人认为书上的表述并不太好,还是看代码字节理解来得更快。本节内容并不是什么复杂的东西,这里就不赘述了。
1 |
|
1 |
|
1 |
|
两个函数分别从缓冲区中放入和读取一个字节。此处的缓冲区也属于全局资源,所以也需要加锁,同时是用while进行判断的,因为可能唤醒该线程的时,线程还是不符合条件的情况出现。
然后现在回顾上一个PART的中intr_keyboard_handler函数。
1 |
|
kbd_buf是内核全局变量,也就是内核缓冲区。键盘的输入会存入内核缓冲区,然后由其他程序读出以实现交互(其实就是本书本章本节前面实现的控制台了)。
插画ID:92002347