6.s081 xv6-lock实现

xv6-源码解析-LOCK

锁结构

xv6,中用结构体的一个int字段来判断是否上锁

//锁结构体
struct spinlock {
  // 锁是否已被持有的标志位
  uint locked;       // Is the lock held?

  // For debugging:
  // 译:用于debug的一些附加信息
  char *name;        // Name of lock.
  struct cpu *cpu;   // The cpu holding the lock.
};

初始化

void
initlock(struct spinlock *lk, char *name)
{
  lk->name = name;
  lk->locked = 0;
  lk->cpu = 0;
}

加锁动作

__sync_lock_test_and_set:原子性的设置1到指定内存地址,然后返回该内存地址被修改前的值;另外还有内存屏障的作用,防止指令的重排序


//获取锁的方法
void acquire(struct spinlock *lk){
   //如果locked已经被其他线程上锁了,那么一定返回1(1表示修改前的值,此时已经被上锁了,所以是1),while继续重试
    //如果locked没有被其他线程上锁,那么一定返回0,跳出while
    while(__sync_lock_test_and_set(&lk->locked, 1) != 0){

    } 
}

关中断

​ 如果先获取Lock1,然后触发中断,中断又获取Lock1,那么该程序会一直死锁,中断也永远不可能返回。因此再获取锁之前都要关闭中断。

​ 如果出现先获取lock1,在获取lock2…这样需要记录中断关闭多少次,所以需要用push_off(),封装一下开关中断的方法,而不是直接intr_off();

void
push_off(void)
{
  // 获取当前CPU的中断开关状态
  int old = intr_get();
  
  // 无论如何,都关闭中断
  // 事实上在初次获取到锁之后,中断就已经是关闭状态了
  intr_off();
  
  // 如果是刚刚进入锁链
  // 就将进入锁链之前的中断状态,也就是上面的old
  // 保存在intena中
  if(mycpu()->noff == 0)
    mycpu()->intena = old;
  
  // 嵌套深度+1
  mycpu()->noff += 1;
}

总结

结合上面所说,实现一个锁最终由以下代码,但是获取锁的时候会持续while空转,浪费性能。

// Acquire the lock.
// Loops (spins) until the lock is acquired.
// 译:获取锁
// 在原地循环直到锁被获取
void
acquire(struct spinlock *lk)
{
  // 调用push_off关闭中断,增加迭代深度
  push_off(); // disable interrupts to avoid deadlock.
  
  // 如果已经持有了要获取的锁,则内核陷入panic
  // Xv6不允许可重入锁(re-entrant lock)
  if(holding(lk))
    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)
  
  // amoswap.w.aq(原子交换字大小的变量值:atomic swap.word.acquire)
  // acquire标志保证在此操作之后的访存操作不可以被搬运到此操作之前
  // 相当于隐含了内存定序(memory ordering)的语义
  // 使用sync_lock_test_and_set 函数,可以原子式的完成test&set操作
  // __sync_lock_test_and_set函数的第一个参数是一个指针,指向一个内存位置
  // __sync_lock_test_and_set函数的第二个参数是要往上述内存区域写入的值
  // 返回值是第一个参数指向的内存位置被改写之前的值
  // 如果返回值是1,表示这把锁正在被其他进程持有,我们继续向其中写入1,不会影响到其他进程
  // 如果返回值是0,那么说明这把锁是空闲状态,当前进程成功获取了锁,于是可以退出循环
  // Test&Set指令必须是原子的,因为多个进程尝试获取锁的过程
  // 本质上也是对临界区(lcoked标志)的访问,如果不是原子化的也会出错
  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.
  // 译:告知C编译器和处理器,不要让load/store命令随意翻越此处
  // 这是为了保证对临界区内存的访问严格发生在锁被获取之后
  // 在RISC-V架构上,这会生成一个fence指令
  // fence指令将会保证位于这条指令前后的访存指令,都互相不越界
  __sync_synchronize();

  // Record info about lock acquisition for holding() and debugging.
  // 译:将当前获取锁的PCU信息记录在锁结构体中,方便调试
  lk->cpu = mycpu();
}