vsnprintf函数中有锁吗_10.7 自旋锁(Spin lock)的实现(一)

接下来我们看一下如何实现自旋锁。锁的特性就是只有一个进程可以获取锁,在任何时间点都不能有超过一个锁的持有者。我们接下来看一下锁是如何确保这里的特性。
我们先来看一个有问题的锁的实现,这样我们才能更好的理解这里的挑战是什么。实现锁的主要难点在于锁的acquire接口,在acquire里面有一个死循环,循环中判断锁对象的locked字段是否为0,如果为0那表明当前锁没有持有者,当前对于acquire的调用可以获取锁。之后我们通过设置锁对象的locked字段为1来获取锁。最后返回。

c40af63833d9e1eab6029792753a055a.png


如果锁的locked字段不为0,那么当前对于acquire的调用就不能获取锁,程序会一直spin。也就是说,程序在循环中不停的重复执行,直到锁的持有者调用了release并将锁对象的locked设置为0。
在这个实现里面会有什么样的问题?
学生回答:两个进程可能同时读到锁的locked字段为0。
是的,所以这里会有race condition,在下面的位置会有race。

72b27b8b692bb3dacb997ed9ce83ea17.png


如果CPU0和CPU1同时到达A语句,它们会同时看到锁的locked字段为0,之后它们会同时走到B语句,这样它们都acquire了锁。这样我们就违背了锁的特性。
为了解决这里的问题并得到一个正确的锁的实现方式,其实有多种方法,但是最常见的方法是依赖于一个特殊的硬件指令。这个特殊的硬件指令会保证一次test-and-set操作的原子性。在RISC-V上,这个特殊的指令就是amoswap(atomic memory swap)。这个指令接收3个参数,分别是address,寄存器r1,寄存器r2。这条指令会先锁定住address,将address中的数据保存在一个临时变量中(tmp),之后将r1中的数据写入到地址中,之后再将保存在临时变量中的数据写入到r2中,最后再对于地址解锁。

46906591065b8c3d38c5cf9d6aed9923.png


通过这里的加锁,可以确保address中的数据存放于r2,而r1中的数据存放于address中,并且这一系列的指令打包具备原子性。大多数的处理器都有这样的硬件指令,因为这是一个实现锁的方便的方式。这里我们通过将一个软件锁转变为硬件锁最终实现了原子性。不同处理器的具体实现可能会非常不一样,处理器的指令集通常像是一个说明文档,它不会有具体实现的细节,具体的实现依赖于内存系统是如何工作的,比如说:

  • 多个处理器共用一个内存控制器,内存控制器可以支持这里的操作,比如给一个特定的地址加锁,然后让一个处理器执行2-3个指令,然后再解锁。因为所有的处理器都需要通过这里的内存控制器完成读写,所以内存控制器可以对操作进行排序和加锁。
  • 如果内存位于一个共享的总线上,那么需要总线控制器(bus arbiter)来支持。总线控制器需要以原子的方式执行多个内存操作。
  • 如果处理器有缓存,那么缓存一致性协议会确保对于持有了我们想要更新的数据的cache line只有一个写入者,相应的处理器会对cache line加锁,完成两个操作。

硬件原子操作的实现可以有很多种方法。但是基本上都是对于地址加锁,读出数据,写入新数据,然后再返回旧数据(注,也就是实现了atomic swap)。
接下来我们看一下如何使用这条指令来实现自旋锁。让我们来看一下XV6中的acquire和release的实现。首先我们看一下spinlock.h

f26482a550111662860ef57e50ec7384.png


如你所见,里面有spinlock结构体的定义。内容也比较简单,包含了locked字段表明当前是否上锁,其他两个字段主要是用来输出调试信息,一个是锁的名字,另一个是持有锁的CPU。
接下来我们看一下spinlock.c文件,先来看一下acquire函数,

c5ed17fd02abd7c9abd05e7018a287e8.png


在函数中有一个while循环,这就是我刚刚提到的test-and-set循环。实际上C的标准库已经定义了这些原子操作,所以C标准库中已经有一个函数__sync_lock_test_and_set,它里面的具体行为与我刚刚描述的是一样的。因为大部分处理器都有的test-and-set硬件指令,所以这个函数的实现比较直观。我们可以通过查看kernel.asm来了解RISC-V具体是如何实现的。下图就是atomic swap操作。

e44eb954b757c365b03b4289b230597c.png


这里比较复杂,总的来说,一种情况下我们跳出循环,另一种情况我们继续执行循环。C代码就要简单的多。如果锁没有被持有,那么锁对象的locked字段会是0,如果locked字段等于0,我们调用test-and-set将1写入locked字段,并且返回locked字段之前的数值0。如果返回0,那么意味着没有人持有锁,循环结束。如果locked字段之前是1,那么这里的流程是,先将之前的1读出,然后写入一个新的1,但是这不会改变任何数据,因为locked之前已经是1了。之后__sync_lock_test_and_set会返回1,表明锁之前已经被人持有了,这样的话,判断语句不成立,程序会持续循环(spin),直到锁的locked字段被设置回0。
接下来我们看一下release的实现,首先看一下kernel.asm中的指令

550090db5a74b9839a41de6ed3a801e2.png


可以看出release也使用了atomic swap操作,将0写入到了s1。下面是对应的C代码,它基本确保了将lk->locked中写入0是一个原子操作。

c7af6310c30827d8e7b3dc197783162a.png
  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值