xv6中专门讲锁的篇幅并不多,锁的代码也在一两行左右,但是锁的使用却是无处不在的,而且要理解好也并不那么容易
对锁的需求来自于interleaving(交错执行),这可能是多处理器环境下,也可能是单处理器环境下在不同进程/线程间切换cpu
当他们需要访问共享的数据结构时,就出现了问题,如:
- 一个进程在读一个数据时,可能另一个进程正在修改它,那么这个进程读到的数据就可能是不正确的
- 多个进程同时更新一个数据,那么可能只有最后完成的更新保留了下来,之前的更新都丢失了
例如:
这样一个数据结构,list指向第一个节点(invariant),push新建一个节点,并将其插入到最前面,然后让list重新指向他
考虑下面情况:
此时c1这个节点就没有被插入到链表中去,发生这个错误的原因在于:同时执行15行违反了上述invariant,其他一个插入时,list没有指向第一个节点
当然上述只是一种可能的情况,也有可能先执行15行的,也先执行完了16行,之后另一个进程再执行15行,此时就没有问题,也就是说这个bug是否出现取决于两者执行的具体情况,这种一般称为race condition
A race condition is a situation in
which a memory location is accessed concurrently, and at least one access is a write. A race is often
a sign of a bug, either a lost update (if the accesses are writes) or a read of an incompletely-updated
data structure. The outcome of a race depends on the exact timing of the two CPUs involved and
how their memory operations are ordered by the memory system, which can make race-induced
errors difficult to reproduce and debug. For example, adding print statements while debugging
push might change the timing of the execution enough to make the race disappear.---xv6 book
常用的解决办法是使用锁:
此时,链表插入这个动作不能被并发/并行执行 ,一次只能一个进程执行这两行代码,invariant也就不会被破坏
acquire和release之间的被称为关键区域,如果一个进程在执行关键区域时,另一个进程只能等待,等待该进程释放锁之后,它再去获取锁
这种情况被称为contention
上面的关键区域当然可以更大,但是这样会降低并行性,从而降低性能
锁的实现
一个简单的想法是使用一个变量locked来表示是否持有该锁:
但是race仍然是存在的,例如两个cpu上的进程同时执行到25行,都发现现在锁未被持有,然后都获取了锁,这明显是不对的
要避免这种情况,我们应该让25,26行成为一个原子操作,这种指令被CAS(compare and swap)或者test and set,在RISC v上是amoswap,这里使用的是__sync_lock_test_and_set(gcc扩展),将其放在循环中,每次都将locked设置为1,并返回locked原值,如果为0,说明此时获取锁成功,如果为1,说明锁仍在使用中,获取锁成功之后,还有设置锁的cpuid字段为当前cpu
实现release时,则是首先检查是否持有锁,如果是,那么清除锁的cpu字段,并且设置locked为0
死锁和获取锁的顺序
如果要获取多个锁,那么任何时候要获取这些锁,都应该遵守相同的顺序
例如要获取两个锁a,b
一个路径是先获取a,然后b,另一个则相反,那么当一个获取a,一个获取b之后,前者想再获取b,而后者想获取a,就会死锁
锁和中断
xv6中主要有两种锁:spinlock和sleeplock
spinlock不仅会用在普通代码里,还会用在中断处理例程里,如处理定时中断时,就会获取tickslock,而sysproc中的sys_sleep也会,假如在执行sys_sleep时,在获取tickslock之后,发生定时中断,此时再去获取tickslock,因为sys_sleep不能释放它,中断处理程序也得不到他,就会死锁。为了处理这种情况,在xv6中,只要获取spinlock,就会禁用该cpu上的中断
为了处理多层的spinlock,struct cpu使用了一个noff字段来表示嵌套层数,pop_off减少它,push_off增加它
关于锁这里还涉及到指令重排的问题,可能会将关键区域内的操作移到关键区域外,这会破坏锁的效果,所以这里使用__sync_synchronize()来指示编译器和cpu不要进行指令重排
睡眠锁 sleep lock
上面介绍的主要是基于自旋锁,即要获取锁而锁被占用时,就不停的test and set
而一些情况下,我们希望此时能够让出cpu,让其他进程执行