0%

xv6-2020-锁机制

书中的第六章讲的是锁,但是这部分配套的实验其实是:

image-20210422105216277

当时是先做的实验,然后才看的Locking这一章。现在知道了,因为lab中的物理内存的引用计数数组是共享的,所以访问它修改它需要获取锁。

锁实现

Xv6,包括现在的很多操作系统,实现锁的最基本都是依靠底层的CPU指令,比如compare and swap,原子交换;亦或者是test and set这类。这个原子性是由指令来进行保证的。但是并不是一定要原子指令的支持,因为没有原子指令也是可以实现锁的。

那么首先第一个问题,CPU又是怎么实现原子性指令的呢?如果具体要追溯下去,这是Intel的商业机密,更加具体可以参考这个回答。这里只能简单分析一下:

On most instructions a lock prefix must be explicitly used except for the xchg instruction where the lock prefix is implied if the instruction involves a memory address.

几乎所有的指令都需要用lock来修饰才能保证原子性,但是xchg这条指令如果访问的是内存地址,那么会自动加上lock,所以本质上来说,一条指令如果希望它是原子性的,那么就加上Lock。

在java中,CAS本质上用的LOCK CMPXCHG,虽然和上面的xchg不一样,但是在多CPU环境下,虚拟机会自动帮你加上lock前缀,让它成为原子性的。

这里需要了解,CAS虽然底层用了lock cmpxchg这条指令,但是它确实是无锁编程,因为指令上的lock仅仅保证了这条指令的原子性,代价远远比操作系统实现的lock要小的多。而操作系统实现的lock,会让线程等待很久很久(相对指令来说)。无锁编程在于把这些会引发冲突的代码整合成原子性的代码。

其实lock前缀只有在多核的CPU上才需要,在单核的CPU中是不需要的。lock的实现方式是这样的:

  • 当访问的数据在内存时,通过在总线锁(也看到有人说其实是更加细粒度的,不过总线锁在理解层面更加简单)实现原子性;
  • 当访问的数据在处理器的缓存时,通过缓存一致性协议(如MESI协议)实现原子性;

所以了解了指令的原子性,就可以理解锁的基本理念了。

锁与中断处理

想象这么一个场景,如果在用户态有一个进程获取了锁,然后此时发生了中断,那么操作系统就进到中断处理程序中处理,如果这个中断处理程序也需要获取这把锁怎么办?中断处理程序永远也回不去,同时用户态的代码也永远执行不了,这样就造成了死锁。xv6的解决办法很简单,在获取锁的代码中,直接把中断给关了,然后只有当锁被释放了,才打开中断。

锁的种类

在xv6中实现了两种锁,第一种就是熟悉的自旋锁,如果没获得锁就一直死循环。还有一种是sleep锁,也就是没有获取到锁的话,就会使用sleep,不过这是之后的内容。