#### 锁的作用

对同一个内存区域的并发操作可能会破坏这个内存区域的不变性。用锁把这样的操作**都**划定为临界区，这样并发操作就序列化了，保护了指定内存区域的不变性。数据的不变性，不是指它的内容，而是指它的结构。比如栈指针应指向栈顶，比如链表应指向它的第一个元素且各元素之间依次链接在一起。

#### 锁的实现

##### 自旋锁

锁本质上是个变量，当变量值为0时表示锁空闲，当变量值为1时表示锁被占有。xv6里的自旋锁`spinlock`真正有效的字段只有`locked`，`*name`和`*cpu`字段只是调试时才用的。

除了代表锁的变量，还需要两个操作锁的函数来划分临界区：`aquire`和`release`。它们最终都是使用了原子指令`amoswap`以实现其功能的。

如果线程和中断处理例程使用了同一个锁，则它们交互的时候有可能产生死锁。xv6的作法是当一个线程申请锁之前，它会在在`aquire`里执行`push_off`来记录临界区嵌套的层次，在`release`里执行`pop_off`来释放这个嵌套的层次。当嵌套层次为0时，`pop_off`会恢复临界区最外层的中断状态。

编译器或CPU有可能会改变指令的正常执行次序，这就有可能把临界区的代码放到临界区外执行。为了避免那样的问题，xv6在`acquire`和`release`里使用了`__sync_synchronize()`。`__sync_synchronize()`是一个内存屏障(memory barrier)：它告诉编译器不要跨屏障重新排序load和store。

##### 睡眠锁

睡眠锁`sleeplock`里有两个有用的字段：变量`locked`代表睡眠锁，变量`lk`是一个自旋锁。`*name`和`pid`字段只是调试时才用的。

除了睡眠锁它自己，还需要两个操作它的函数来划分临界区：`aquiresleep`和`releasesleep`。睡眠锁是用自旋锁来保护的。

在`aquiresleep`里，如果睡眠锁已经被占用，则当前进程不会死等，而是释放自旋锁并让出CPU，等待其它进程释放睡眠锁的时候唤醒自己。如果睡眠锁没有被占用，则占用睡眠锁并释放自旋锁。

在`releasesleep`里，先释放睡眠锁，并让睡在这个睡眠锁上的某个进程变为就绪态。当调度器运行的时候，那个进程就有机会继续执行了。

#### 锁的用法

##### 自旋锁

使用自旋锁的前提是有一个进程写变量，而其它进程有可能读或写同样的变量。如果不满足这个前提，就不应使用自旋锁。

在满足前提的条件下，如果并发性比较低，我感觉即可以使用自旋锁，也可以不用自旋锁而把这个变量放在一个线程里来处理(破坏使用自旋锁的前提条件)。

在满足前提的条件下，如果并发性比较高，但并发的效率和不并发的效率相差不大，或者低于不并发的效率，此时不应使用自旋锁。

在满足前提的条件下，如果并发的效率远大于不并发的效率，此时应使用自旋锁。可以修改变量让自旋锁的粒度更细，进而提升并发性。但这需要均衡性能和复杂性之间的矛盾。

当使用自旋锁的时候，一个不变性在内存里的所有位置都需要被同一个锁保护起来。

当一个代码路径上存在多个自旋锁，这些自旋锁之间就构成了一个锁序。其它代码路径也需要遵守同样的锁序，否则有可能造成死锁。

##### 睡眠锁

睡眠锁也是在保护不变性，所以它的使用前提和自旋锁是一样的。即：有一个进程写变量，而其它进程有可能读或写同样的变量。

在满足前提的条件下，如果需要持有锁的时间比较长，或在持有锁的时候允许让出CPU。此时可以使用睡眠锁。

在使用睡眠锁的时候允许中断发生，但在中断例程里不可以使用睡眠锁，因为中断例程让出CPU然后被调度器调度会产生严重的错误。

不可以在自旋锁的临界区内使用睡眠锁，但可以在睡眠锁的临界区内使用自旋锁。