MIT 6s081 lab3.xv6自旋锁和休眠锁

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); // 释放这把锁
}
  • 8
    点赞
  • 9
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值