我觉得锁是理解xv6内核代码的一个难点,锁实现本身更简单一些,但是锁的使用反而更难理解,这跟常规的思路可能不太一致。
本部分记录下xv6中的自旋锁,自旋锁顾名思义就是不停的旋转等待,直到获得锁。 它在等待CPU
过程中相当于一个while(1),会消耗CPU时间。直接看xv6中spinlock的结构体,注释说Mutual exclusion lock,其实这个自旋锁也是互斥锁,仅有一个线程可以获得锁。
// 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需要关注,简单来说 locked = 1
, 说明获得锁,否则未获得锁。
0x01. spinlock
相关函数
spinlock 相关函数相对来说还是比较容易理解,最主要就是 acquire 和 release 两个函数。
1. acquire 获得 spinlock
void
acquire(struct spinlock *lk)
{
push_off(); // 这个函数很重要
if(holding(lk)) panic("acquire");
// 使用编译器提供的函数来实现原子操作,等待,直到locked=0,然后给他设置成locked=1
while(__sync_lock_test_and_set(&lk->locked, 1) != 0);
// 内存屏障,不让编译器优化改变这里的代码执行顺序,以免出错
__sync_synchronize();
// 设置锁的CPU
lk->cpu = mycpu();
}
2. release 释放 spinlock
void
release(struct spinlock *lk)
{
if(!holding(lk)) panic("release");
lk->cpu = 0;
__sync_synchronize();
__sync_lock_release(&lk->locked); // 与__sync_lock_test_and_set 对应
pop_off(); // 与 push_off 对应
}
3. push_off 和 pop_off
- spinlock在加锁的时候,必须要关闭CPU的中断。 加锁和解锁中间,如果发生中断,就会在这中间增加很多操作,同时中断处理过程中再用到其他锁,很可能发生死锁或者内核panic。 所以 xv6 中的处理是,加锁的第一步就是关中断。
- 有一种情况,执行过程中加了好几把锁,那么如何来关闭和打开中断? 就是使用 push_off和pop_off 函数。为关闭中断的加一个关闭次数,或者说嵌套关闭次数的记录,并只在嵌套次数=0的时候,再打开中断。
- 有个疑问? 关闭中断的嵌套次数应该保存在哪里?? 为啥是 struct cpu里?
- 真正打开和关闭中断的操作,通过修改sstatus寄存器中调整SSTATUS_SIE位来实现。
4. __sync_lock_test_and_set 和 __sync_lock_release
在 riscv中__sync_lock_test_and_set 函数就是如下面的代码实现:
a5 = 1
s1 = &lk->locked
amoswap.w.aq a5, a5, (s1)
__sync_lock_test_and_set 是 GNU 提供的函数, 针对不同的架构提供原子 lock 原子操作,不需要自己写汇编代码。
__sync_lock_release 函数也是如此。
5. holding 函数
验证 lock 是否已经被获得,是返回1,否则返回0。
int
holding(struct spinlock *lk)
{
int r;
r = (lk->locked && lk->cpu == mycpu());
return r;
}
0x02. spinlock 的使用
被spinlock保护的代码区域叫做临界区,也就是说acquire和release之间的代码就是临界区代码。 同时有多个CPU运行到这段代码的时候,他们不会同时运行,而是变成顺序执行。可能如下图所示:
spinlock 保护的是什么? 最简单的就是保护一个全局变量。
1. 保护 ticks
时钟中断发生的时候,xv6需要维护一个时钟中断次数的全局变量ticks,每次需要将ticks++,为了避免 竞争,所以需要用spinlock保护。非常简单,如下操作即可:
// kernel/trap.c 省略了其他代码
// 定义
struct spinlock tickslock;
uint ticks;
// ...
// 初始化锁
initlock(&tickslock, "time");
// ...
// 使用锁
void
clockintr()
{
acquire(&tickslock); // 加锁
ticks++;
wakeup(&ticks); // 暂时忽略
release(&tickslock); // 解锁
}
当然,spinlock 保护的内容可以更多一些,比如 p->lock 就复杂一些。
2. 进程锁
// kernel/proc.h
// Per-process state
struct proc {
struct spinlock lock;
// p->lock must be held when using these:
enum procstate state; // Process state
void *chan; // If non-zero, sleeping on chan
int killed; // If non-zero, have been killed
int xstate; // Exit status to be returned to parent's wait
int pid; // Process ID
// wait_lock must be held when using this:
struct proc *parent; // Parent process
// ...
};
进程锁 p->lock 需要保护的内容较多:
- state: 进程的状态,每次修改状态的时候,会有其他相关的变量需要调整,因此必须要用进程锁保护起来。比如 running状态进入放弃CPU,state需要修改为RUNNABLE,那么同时要切换context,这个过程得用进程锁保护起来。
- chan: 进程进入sleep状态的标识,wakeup时通过chan来判断哪些进程需要被唤醒。
- killed:进程是否已经被杀死,因为进程的退出有一些内存需要回收等操作,需要进行保护;
- xstate: 给parent进程的退出状态;
- pid: 进程ID,不知道为啥? 暂记下来。