xv6锁
xv6实现了两种锁:自旋锁和休眠锁,来保证临界资源的互斥访问。
锁的特性就是只有一个进程可以获取锁,在任何时间点都不能有超过一个锁的持有者。
在xv6中,锁的使用有以下几点需要注意:
- 获取锁的顺序相同:当一个代码在同一时间持有多个锁时,所有代码路径应该按照相同的次序去获取这些锁,否则可能会出现环路等待导致死锁。
- 锁与中断:有时候在中断处理程序中获取了锁,但有可能在进程进入中断处理函数之前已经持有了这把锁,这就导致了死锁,在xv6中,申请锁之前必须关闭中断。
- 指令定序:编译器可能会对指令的执行顺序重新排序来提升指令执行顺序,但对于并发场景可能会导致错误,此时必须加入内存屏障,避免指令重排队并发的影响。
自旋锁
struct spinlock定义于kernel/spinlock.h中
// Mutual exclusion lock.
struct spinlock {
uint locked; // Is the lock held?
// For debugging:
char *name; // Name of lock.
struct cpu *cpu; // The cpu holding the lock.
};
结构体的内容比较简单,包含了locked字段表明是否上锁,还有锁的名字和持有锁的cpu
打开中断or关闭中断
由于xv6可能在同一时间持有多把锁,按照获取锁的先后顺序,形成了一条链,因此不能简单的使用intr_off和intr_on函数进行中断的开关,而是要记录嵌套深度。
// push_off/pop_off are like intr_off()/intr_on() except that they are matched:
// it takes two pop_off()s to undo two push_off()s. Also, if interrupts
// are initially off, then push_off, pop_off leaves them off.
void push_off(void)
{
int old = intr_get(); //查询之前的中断状态
intr_off(); // 不管当前中断是否已经关闭,直接关闭中断
if(mycpu()->noff == 0) //如果是第一次获取锁
mycpu()->intena = old; //保存第一次获取锁之前的中断状态
mycpu()->noff += 1; // 增加嵌套层数,也就是链的长度
}
void pop_off(void)
{
struct cpu *c = mycpu();
if(intr_get()) // 此时中断是打开的,为错误状态
panic("pop_off - interruptible");
if(c->noff < 1) // 嵌套层数小于1,不应该再调用pop_off,为错误状态
panic("pop_off");
c->noff -= 1; // 减少链的长度
if(c->noff == 0 && c->intena) // 此时已经没有持有锁了,恢复cpu最初的中断状态
intr_on();
}
acquire获取锁
获取锁的函数位于kernel/spinlock.c中
// Acquire the lock. 获取锁
// Loops (spins) until the lock is acquired. 当锁被持有,就在这里面自旋
void acquire(struct spinlock *lk)
{
push_off(); // disable interrupts to avoid deadlock. //进入嵌套层
if(holding(lk)) // 如果当前cpu已经持有这把锁,就panic
panic("acquire");
// On RISC-V, sync_lock_test_and_set turns into an atomic swap:
// a5 = 1
// s1 = &lk->locked
// amoswap.w.aq a5, a5, (s1)
while(__sync_lock_test_and_set(&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 strictly after the lock is acquired.
// On RISC-V, this emits a fence instruction.
__sync_synchronize(); //防止编译器修改指令的执行顺序
// Record info about lock acquisition for holding() and debugging.
lk->cpu = mycpu();
}
__sync_lock_test_and_set函数(一个特殊的硬件指令,保证test-and-set操作的原子性):
- 如果锁没有被持有,那么锁对象的locked字段会是0,如果locked字段等于0,我们调用test-and-set将1写入locked字段,并且返回locked字段之前的数值0。这意味着没有cpu持有这个锁,循环结束。
- 如果locked字段之前是1,先将之前的1读出,然后写入一个新的1,但是这不会改变任何数据,因为locked之前已经是1了。之后__sync_lock_test_and_set会返回1,表明锁之前已经被别的cpu持有了,这样的话,判断语句不成立,程序会持续循环(spin),直到锁的locked字段被设置回0。
__sync_synchronize:
用于确定指令的移动范围(防止编译器改变指令的执行顺序),任何在它之前的load/store指令,都不能移动到它之后。
通过使用__sync_lock_test_and_set
和__sync_synchronize
,共同实现了内存屏障,保证了位于本条指令之前的访存指令(loads or stores)移到后面,也不允许位于本条指令之后的访存指令移到前面。
release释放锁
// Release the lock.
void
release(struct spinlock *lk)
{
if(!holding(lk)) // 如果当前进程并没有持有这个锁,panic
panic("release");
lk->cpu = 0;
// Tell the C compiler and the CPU to not move loads or stores
// past this point, to ensure that all the stores in the critical
// section are visible to other CPUs before the lock is released,
// and that loads in the critical section occur strictly before
// the lock is released.
// On RISC-V, this emits a fence instruction.
__sync_synchronize();
// Release the lock, equivalent to lk->locked = 0.
// This code doesn't use a C assignment, since the C standard
// implies that an assignment might be implemented with
// multiple store instructions.
// On RISC-V, sync_lock_release turns into an atomic swap:
// s1 = &lk->locked
// amoswap.w zero, zero, (s1)
__sync_lock_release(&lk->locked);
pop_off(); //回到嵌套的上一层
}
睡眠锁
睡眠锁定义于kernel/sleeplock.h
// Long-term locks for processes
struct sleeplock {
uint locked; // Is the lock held?
struct spinlock lk; // spinlock protecting this sleep lock
// For debugging:
char *name; // Name of lock.
int pid; // Process holding lock
};
包含的字段有锁状态,一把自旋锁(用于保护睡眠锁的其他字段),锁的名字和持有锁的进程Pid
sleep休眠
// Atomically release lock and sleep on chan.
// Reacquires lock when awakened.
void
sleep(void *chan, struct spinlock *lk)
{
struct proc *p = myproc();
// Must acquire p->lock in order to
// change p->state and then call sched.
// Once we hold p->lock, we can be
// guaranteed that we won't miss any wakeup
// (wakeup locks p->lock),
// so it's okay to release lk.
if(lk != &p->lock){ //DOC: sleeplock0
acquire(&p->lock); //DOC: sleeplock1
release(lk); // 注意这里把自旋锁释放了
}
// Go to sleep.
p->chan = chan;
p->state = SLEEPING;
sched(); // 发生调度,进程上下文切换
// 当被唤醒,并且被调度,此时从这里开始运行
// Tidy up.
p->chan = 0;
// Reacquire original lock.
if(lk != &p->lock){
release(&p->lock); //释放进程的锁
acquire(lk); //重新获取自旋锁。
}
// 返回到调用sleep的位置
}
sleep函数的作用主要就是修改进程的状态,将进程记录在sleeplock的地址处用于后续唤醒,然后发生调度,当唤醒和调度回来之后继续运行。
wakeup唤醒
// Wake up all processes sleeping on chan.
// Must be called without any p->lock.
void
wakeup(void *chan)
{
struct proc *p;
for(p = proc; p < &proc[NPROC]; p++) {
acquire(&p->lock);
if(p->state == SLEEPING && p->chan == chan) {
p->state = RUNNABLE;
}
release(&p->lock);
}
}
遍历全局的进程数组,判断进程p是否处于睡眠锁对应的chan上,并且是睡眠态SLEEPING,若是,则修改进程的状态为RUNNABLE。等待调度。
acquiresleep获取睡眠锁
void
acquiresleep(struct sleeplock *lk)
{
acquire(&lk->lk); // 获取睡眠锁关联的自旋锁,来处理查询lk->locked时的竞争问题
while (lk->locked) { // 睡眠锁已经被其他进程获取
sleep(lk, &lk->lk); // 当前进程休眠
}
// 程序运行到这里,则说明此时睡眠锁没有被获取:可能的情况是被其他进程唤醒,或是直接运行到这里
lk->locked = 1;
lk->pid = myproc()->pid;
release(&lk->lk);
}
之所以要用while去判断lk->locked而不是直接调用sleep,是因为多CPU下,有可能在当前进程被唤醒之后,在获取睡眠锁之前,有其他进程又拿走了睡眠锁,此时当前进程要重新进入睡眠。
releasesleep释放睡眠锁
void
releasesleep(struct sleeplock *lk)
{
acquire(&lk->lk); // 获取睡眠锁关联的自旋锁
lk->locked = 0; // 修改locked字段
lk->pid = 0;
wakeup(lk); // 唤醒其他正在等待这个睡眠锁的进程
release(&lk->lk); // 释放这把锁
}