🗒️Xv6-Lock

2023-5-16|2023-5-22
Anthony
Anthony
type
status
date
slug
summary
tags
category
icon
password
本Blog基于任职于美国波特兰洲立大学的教授@hhp3
在Youtube上对xv6内核的讲解视频
本期讲解xv6中的Spinlocks,视频链接在下面

Spinlock概述

在XV6中,spinlock仅仅由一个变量locked控制
当locked = 0 表示空闲/解锁/释放(free/unlocked/released)
当locked = 1 表示上锁/持有/获取(locked/held/acquire)
 
可以在kernel/spinlock.h中找到对spinlock结构的描述
 
其中有用的部分就是locked, 可以从注释中看到name和cpu都是给调试使用的
 

Spinlock相关函数

功能描述

与spinlock有关的函数分别是acquire, release, initlock, holding。每个函数都接收指向spinlock的指针,以此操作spinlock
 

Acquire

一个理所当然的acquire函数也许会长成这样
 
但是这不可避免会导致一些问题,比如有两个进程同时检查锁的状态,并且同时获取了锁(这显然是不合理的)那么就会造成错误。
 
因此XV6中引入了操作AMOSWAP(Atomic swap instruction)
该指令会同时地将一个值放入内存,并且从该内存地址取出先前的值。AMOSWAP操作是不允许被打断的,因此可以认为AMOSWAP保证了只有一个进程对locked进行访问和修改,不会被其他任何进程影响。
对于上述代码,我们将一个进程获取locked的行为可以描述为:对locked保存的地址,使用AMOSWAP操作向该地址写入“1”然后获取该地址先前的值,如果先前的值是0那么代表该进程成功获取到了锁,如果先前的进程是“1”,该进程就重新执行之前的操作。
 

Release

对于释放锁,也是使用AMOSWAP将locked变成0即可
 

代码理解

Init

 
要初始化一个spinlock,首先获取指向它的指针,以及它的名字,将spinlock结构体的对应字段赋值,然后locked初始化为0,表示未被使用。cpu也设置成0,表示未被进程获取。
 

Acquire

 
  • 其中的while(__sync_lock_test_and_set(&lk->locked, 1) != 0) 就是先前提到的AMOSWAP操作。该函数首先获取到spinlock的locked字段,然后尝试修改其为1,该函数会返回locked字段先前的值,如果先前的值是0,就代表成功获取了锁,如果是1,就要继续循环。
  • 其中的if(holding(lk)) 检查该进程是否已经持有了锁,如果是,就报错
  • 其中的lk->cpu = mycpu(); 将spinlock的cpu字段指向当先cpu,表示锁由当前cpu持有
  • 其中的 __sync_synchronize(); 注释的描述是“告诉 C 编译器和处理器在超过该点后不要移动加载或存储,以确保临界区的内存引用严格发生在获取锁之后。 在 RISC-V 上,这会发出一条围栏指令。” 其实就是为了防止编译器做优化,把一些应该在获取锁之后执行的代码优化到获取之前去了。
 

Release

 
  • 该函数首先检查进程是否拥有锁,如果不是则报错
  • lk->cpu = 0; 将当前锁指向的cpu重新设置成Null
  • __sync_lock_release(&lk->locked); 使用AMOSWAP操作将locked变成0。(虽然一条store指令又怎么可能不是原语指令呢)
  • __sync_synchronize(); 保证了释放锁之前的所有操作都已经完成

Holding

 
该函数检查locked位置是否为1,并且拥有该锁的cpu是当前cpu。如果都满足就返回true
 
 

特征及注意事项

  • spinlock不应该被长时间的持有。正如上文所讲的那样,acquire函数会一直执行循环等待锁的释放(非常耗资源)。因此,当进程获取了锁之后,它应该时刻想着要赶快释放掉。
  • 长时间的持有锁可以通过sleep()和 wakeup()来实现
  • 锁的主要作用是保护共享数据(critical section)
 
 
Xv6-VMXv6-Shell