Lec 10: Multiprocessors and locking
- Ref: https://github.com/huihongxiao/MIT6.S081/tree/master/lec10-multiprocessors-and-locking
- Preparation: xv6 book Chapter 6
为什么需要锁
- 多核运行提升性能
- 共享数据同时读写会产生竞态条件(race condition)
锁对象
- API:
acquire(&lcok)
: 任何时间只有一个进程能获取锁release(&lock)
: 释放锁, 其他进程需要等待至锁被释放才能获取.
- acquire 与 release 之间的代码片段称之为临界区, 代码片段整体执行具有原子性
- 通常操作系统会有多把锁, 使得不同系统调用使用不同的锁, 提高并行性.
使用锁的时机
如果两个进程访问了一个共享的数据结构, 并且其中一个进程会更新共享的数据结构, 那么就需要对这个共享数据结构加锁.
这一规则有时是严格的: 某些场景不加锁也可正常工作, 不加锁的程序称之为无锁编程(lock-free program).
有时又是宽松的, 对于一些非共享数据仍需要原子的输出, 如 printf()
的输出.
锁的作用
- 避免丢失更新
- 使得多步操作具有原子性
- 维护共享数据结构的不变性
死锁
- 重入引发的死锁: 一个进程获得锁进入临界区后又尝试获取同一个锁, 锁在非重入的情况下会引发死锁.
- deadly embrace: 多个进程持有多个锁, 彼此需要获取对方的锁
一种解决方案: 对(共同使用的)锁排序, 按序获取锁
锁实现
一般借助硬件指令 test-and-set, 如 XV6 中的原子交换操作(Atomic Memory Swap) amoswap addr, r1, r2
, 可以原子的将内存地址 addr 的值写入寄存器 r2, 将寄存器 r1 的值写入 addr.
注: store
指令不一定是原子操作.
// On RISC-V, sync_lock_test_and_set turns into an atomic swap:
// a5 = 1
// s1 = &lk->locked
// amoswap.w.aq a5, a5, (s1)
while(__sync_lock_test_and_set(&lk->locked, 1) != 0)
;
如上是 XV6 自旋锁中的 test-and-set 操作, 若 lk->locked==0
, 那么就写入 1 并返回 0, 循环退出, 表示获得到了锁. 若 lk->locked==1
, 则同样是写入 1 但返回的也是 1, 则会继续循环, 直到 lk->locked
字段为 0 后能获取锁.
__sync_synchronize()
函数会生成 fence
指令进行内存屏障, 防止指令重排.
在 acquire 和 release 操作时都需要关闭中断, 防止原子操作被打断或出现锁重入情况.