XV6

XV6 的进程间同步只有一个简单机制,那就是用于互斥的自旋锁。内核同步中不允许睡眠,这是内核代码同步和用户代码同步的一个重要区别。从名字也可以看出,自旋锁在没有获得锁的情况下将会 “自旋” 反复申请,而用户进程的信号量则是在无法获得的情况下进入睡眠。

XV6 自旋锁使用 spinlock 结构体来表示,其核心成员是一个标志 locked,该标志位置 1 表示已经被加锁,为 0 表示仍未加锁。XV6 代码对该成员使用 xchg 指令来实现原子性的交换。

1. spinlock 结构体

自旋锁 spinlock 结构体中,locked 是用作是否上锁的标志,其余的三个成员是用于调试目的辅助目的,name 用于记录所得名字以便互相区分,cpu 用于记录持有该锁的处理器,pcs[] 用于记录对它执行加锁的函数调用栈对应的 PC 值数组。

2. spinlock 实现

作为自旋锁的实现,initlock() 提供锁的初始化函数、acquire() 为加锁函数(并不睡眠, 反复尝试加锁直至成功)、release() 为解锁函数。

初始化函数非常简单,它根据传入的字符串设置锁的名称,然后将加锁状态置 0,持有该锁的 cpu 为 0。

加锁操作由 acquire() 完成,先是通过 pushcli() 确保关闭中断,然后用 hoding 判断是否已经 加锁过,最后才是用 while 循环不断尝试加锁直至成功。在 while 中执行的是汇编语句 xchg(&lk->locked, 1),就是用内存中的值 1lk->locked 进行原子性的交换。如果当时 lk->locked 为 0,则 1 与之交换后变为 0、lk->locked 变为 1,则退出 while 循环表示成功加锁; 反之,如果 lk->locked 为 1,则交换后双方仍时 1,while 循环继续 “自旋”。加锁成功后,需要用 __sync_synchronize() 通知编译器和处理器保证该点前后的访存顺序不要发生倒置。最后还要根据查找 ebp 栈帧指针逐级记录调用栈。

解锁操作则由 realease() 完成,最关键的是将 lk->locked 置零,并用 popcli() 解除原来所作的关闭中断。

XV6 在 acquire() 中只是简单地关闭所有的中断,之所以需要关闭中断是为了防止当在抢占式内核中,当前过程占用锁然后进入中断处理再次想要获得锁时出现的死锁现象,使用自旋锁非常容易出现死锁现象,尤其是在抢占式内核中,所以 XV6 在中断处理和自旋锁的关系上做的非常决绝:占用锁时禁止所有中断。

pushcli() 主要关闭外部中断并递增调用 pushcli() 关闭中断的次数,这样做的原因是如果代码中获得了两个锁,那么只有当两个锁都被释放后中断才会被允许。同时 acquire() 一定要在可能获得锁的 xchg() 之前调用 pushcli()。如果两者颠倒了,就可能在这短暂时间里里出现问题:中断仍被允许,而锁也被获得了,如果此时不幸地发生了中断,则系统就会死锁。类似的,release() 也一定要在释放锁的 xchg() 之后调用 popcli()

pushcli()popcli() 则是作为辅助函数,前者用于关闭中断并记录嵌套关闭中断的次数 cpu->ncli,如果是首次调用 pushcli() 则记录调用之前的中断使能状态在 cpu->intena(可能是开中断也可能是关闭中断状态)。后者用于解除 pushcli() 的影响,如果在嵌套关闭中断前 CPU 原 来的中断为使能状态,则用 sti() 重新使能中断。