date: 2019-03-02
tags: OS 6.828
这里会记录阅读6.828课程lecture note的我的个人笔记。可能会中英混杂,不是很适合外人阅读,也请见谅。
lock的简单抽象:
lock l
acquire(l)
x = x + 1 -- "critical section"
release(l)
lock就是一个object,如果多个线程调用acquire
,只有一个thread能拿到,其他的就需要等待release
。
程序经常有很多数据,也就对应很多锁,不同的数据对应不同的锁。不过注意,锁并不是和数据绑定的,而是在和数据相关的critical region使用的。
那我们什么时候需要锁呢?
执行上述的规则时,太过保守或是太过自由都不好。有的时候故意有一些race没关系。
那我们能不能自动加锁呢?比如说每个数据都自动的和一个锁相连。
这样太死板,会出现问题,比如说rename("d1/x", "d2/y")
,实行的步骤就会是
这样会导致有一段时间文件消失了,那么信息也就没了...我们需要的是:
也就是说程序员需要能够控制中间过程。
我们可以把锁想想成如下几点:
对于上面的那个rename
,对于有2个锁的方案,如果同时运行rename(d1/x, d2/y)
和rename(d2/a, d1/b)
就会发生死锁。
解决方案就是让程序员给所有的锁制定一个顺序,并让代码遵循这个顺序。显然这是很复杂的。。。
同时这个解决方案会出现一个tradeoff,因为为了避免死锁,我们需要知道函数里面是怎么上锁的。或者说locks are often not the provate business of individual modules。所以一些时候,我们就 粗暴的在函数两端上锁来使其变为单线程运行的。
锁实际上是在避免并行操作。而符合分割数据和锁,或者说设计"fine grained locks"是很难的。所以一般从一个单独的大锁开始,如果有需要再分为多个锁。
比如ide.c
,里面只有idelock
这一个锁。
下面的这种实现方法为什么不对呢?
why not:
struct lock { int locked; }
acquire(l) {
while(1){
if(l->locked == 0){ // A
l->locked = 1; // B
return;
}
}
}
因为有两个程序先后运行到A,而从而都获取了锁。所以我们应该把A,B变得atomic
在x86中,有一个指令xchg
就是用来做这个的,从而使得锁的实现变为:
acquire(l){
while(1){
if(xchg(&l->locked, 1) == 0){
break
}
}
}
xv6的实际代码是(spinlock.c
):
void
acquire(struct spinlock *lk)
{
pushcli(); // disable interrupts to avoid deadlock.
if(holding(lk))
panic("acquire");
// The xchg is atomic.
while(xchg(&lk->locked, 1) != 0)
;
// Tell the C compiler and the processor to not move loads or stores
// past this point, to ensure that the critical section's memory
// references happen after the lock is acquired.
__sync_synchronize();
// Record info about lock acquisition for debugging.
lk->cpu = mycpu();
getcallerpcs(&lk, lk->pcs);
}
注意出了使用了xchg
,还使用了pushcli
,也就是关闭了中断机制,因为中断可能会导致死锁。
release
的代码就简单的多:
void
release(struct spinlock *lk)
{
if(!holding(lk))
panic("release");
lk->pcs[0] = 0;
lk->cpu = 0;
// Tell the C compiler and the processor to not move loads or stores
// past this point, to ensure that all the stores in the critical
// section are visible to other cores before the lock is released.
// Both the C compiler and the hardware may re-order loads and
// stores; __sync_synchronize() tells them both not to.
__sync_synchronize();
// Release the lock, equivalent to lk->locked = 0.
// This code can't use a C assignment, since it might
// not be atomic. A real OS would use C atomics here.
asm volatile("movl $0, %0" : "+m" (lk->locked) : );
popcli();
}
这里之所以要调用__sync_synchronize()
,就是因为编译器可能会在优化的时候更改指令顺序。如,虽然我们写的是:
Core A: Core B:
locked = 1
x = x + 1 while(locked == 1)
locked = 0 ...
locked = 1
x = x + 1
locked = 0
但是有可能会被优化为:
locked = 1
locked = 0
x = x + 1
这样就让锁失效了。__sync_synchronize()
就是有这样的作用,让编译器不会move a memory reference past it. 事实上xchg
也会有同样的效果(intel保证的)。
因为上述的存在,在用锁的情况下,不需要考虑memory ordering rules,只有在写exotic "lock-free"的代码的时候才需要。
我们会发现spinlock会阻塞cpu,那么其只适合短时间的锁。我们之后会讨论和spinlock不太一样的一种锁,在等待的时候会让出CPU。
关于用锁的几点建议: