一个操作系统的设计与实现——第25章 多处理器(上):多处理器同步原语

25.1 多处理器同步原语的实现原理

当计算机中存在不止一个CPU时,基于关中断的同步原语就失效了。这是因为每个CPU的中断是独立的,关闭一个CPU的中断并不会影响其他CPU。从本质上说,中断由rflags控制,但rflags在每个CPU中都有一个,因此,只有找到一个共享区域,才能实现多CPU间的同步原语。内存正是这样的共享区域,其可用于实现锁。

如果使用内存实现锁,那就需要一块内存标记锁的状态,并需要判断并尝试修改锁的状态。如果使用常规方法实现这套功能,需要的指令一定不止一条,这就陷入了循环依赖:这段指令本身也需要锁,但这段指令本身就是用于实现锁的。

想要解决这个问题,就需要使用一条指令同时判断并尝试修改锁的状态。xchg是实现这个功能的最简单的选择,具体来说:

  • 将锁初始化为0
  • 加锁时,固定使用1与锁进行xchg。如果交换来的是0,就说明锁曾经是0,现在是1,加锁成功;如果交换来的是1,就说明锁在交换前已经是1了,加锁失败,此时,任务应通过某种方式等待锁
  • 解锁时,mov [锁], 0即可

加锁是一个对效率要求很高的操作,因此,CPU提供了cmpxchg的二合一增强版:cmpxchg指令。顾名思义,cmpxchg能同时进行比较与交换操作。具体来说,cmpxchg lhs, rhs的效果是:比较lhsal/ax/eax/rax,如果相等,则mov lhs, rhs,否则,mov rax, lhs,此外,比较操作会影响rflags的ZF位,即je/jne考察的位。

cmpxchg的效果看上去比较绕,如果将其用在锁上,就可以得到一个比较具体的描述:将rax设为0,rdx设为1,然后执行cmpxchg [锁], rdxcmpxchg首先比较[锁]rax,如果相等,就说明锁是0,是可用的,于是执行mov [锁], rdx,将锁置1,同时修改rflags的ZF位,使je通过,且rax仍为0,表示加锁成功;否则,如果不等,就说明锁是1,是不可用的,于是执行mov rax, [锁],将rax置1,表示加锁失败,同时修改rflags的ZF位,使jne通过。

25.2 总线锁定

当内存被多个CPU同时访问时,其也需要锁。因此,CPU提供了总线锁定前缀lock,当使用lock前缀时,内存会被当前指令锁定,其他CPU不能使用内存,直至当前指令结束。

总线锁定对效率有影响,因此是不能滥用的。此外,仅有非常少的指令支持lock前缀,它们是:add, adc, and, btc, btr, bts, cmpxchg, cmpxch8b, cmpxchg16b, dec, inc, neg, not, or, sbb, sub, xor, xadd, xchg,其他指令不能使用lock前缀。

xchg被强制视为具有lock前缀。

25.3 自旋锁的实现

在我们的操作系统中,使用自旋锁进行多处理器同步。

请看本章代码25/Lock.h

第5行,声明了Lock类型。当锁不可用时,自旋锁将进行忙等待,因此其不需要等待队列,只需要一个整数即可。

第7~9行,声明了锁的接口函数,这些函数是用汇编语言实现的。

接下来,请看本章代码25/Lock.s

lockInit函数用于初始化锁,简单的将锁置0即可。

lockAcquire函数用于加锁并返回rflags的值。

第18行,将rdx置1,准备进入自旋状态。

第20~24行,不断尝试加锁。注意:xor rax, rax不能放在循环外面,因为cmpxchg会在加锁失败时将rax从0改成1。

第26~28行,返回rflags的值,然后关中断。

lockRelease函数用于解锁并恢复rflags的值。

第36行,将锁重新置0。

第38~39行,恢复rflags的值。

25.4 自旋锁的使用

在我们的操作系统中,任务队列和显卡驱动会被每个CPU使用,因此是需要加锁的。

请看本章代码25/Queue.h

第16行,在Queue中加入自旋锁。

接下来,请看本章代码25/Queue.hpp

第11行,初始化锁。

第17、21、29、37、43、50行,将关中断升级为自旋锁。

接下来,请看本章代码25/Print.hpp

第8行,定义显卡驱动锁。

第98、105行,在printStr函数中加入锁。

第214行,初始化显卡驱动锁。

25.5 编译与测试

本章代码25/Makefile增加了Lock.s的编译与链接命令。

本章代码25/Kernel.c测试了加锁以后的任务切换和printStr函数。

posted @ 2024-09-01 10:29  樱雨楼  阅读(3)  评论(0编辑  收藏  举报