锁保证正确性
并发访问共享数据(至少一个写)必须通过锁保护,否则会出现竞态条件。
6.1810 / 6.1810 · 第 12 课 · nz
应用希望利用多核处理器获得并行加速,因此内核必须处理并发的系统调用,以及并发的内核数据访问(缓冲区缓存、进程表等)。锁帮助正确共享数据,但也可能限制并行加速。
如果删除 kfree() 中的 acquire/release 会发生什么?
// kfree() 简化
r->next = freelist; // ①
freelist = r; // ②
| Core 1 | Core 2 |
|---|---|
| r1->next = freelist | r2->next = freelist |
| freelist = r1 | freelist = r2 |
结果:r1 仍在 freelist 中(r2->next = r1),但 freelist 现在指向 r2。r1 之后会被分配出去——但某个进程可能仍在使用它!
我们需要锁来保证正确性,但锁会让 kfree() 串行化,损失性能。
lock l;
acquire(l);
x = x + 1; // 临界区
release(l);
acquire(l) 时,只有一个立即返回,其余等待(阻塞)如果两个线程可能使用同一内存位置,且至少一个是写操作,就需要锁。
printf() 并不总是简单的锁/数据对应x = x + 1 或链表插入rename(f1, f2) 必须持有两个目录的锁:
| Core A | Core B |
|---|---|
| rename(d1/x, d2/y) | rename(d2/a, d1/b) |
| lock d1 ✓ | lock d2 ✓ |
| lock d2 ...(等待) | lock d1 ...(等待) |
解决方案:程序员确定所有锁的全序,所有代码必须按该顺序获取锁。即预测锁、排序、获取——复杂!
锁使得难以在模块内部隐藏细节。为避免死锁,我需要知道被调用函数获取了哪些锁,甚至可能需要在调用前就获取它们。锁通常不是个体的私有事务。
锁阻止并行执行。为获得并行,通常需要拆分数据和锁,使每个核心通常使用不同数据和锁——"细粒度锁"。
| 并发源 | tx_lock 保护什么 |
|---|---|
| 进程/进程 | 不交错输出;原子 write(THR) + tx_busy=1 |
| 进程/中断 | 不变量 tx_busy=1 ⇒ 中断会来;原子 read(LSR) + tx_busy=0 |
struct lock { int locked; }
acquire(l) {
while (1) {
if (l->locked == 0) { // A
l->locked = 1; // B
return;
}
}
}
问题:两个 acquire() 都看到 locked==0,都设置 locked=1。一个释放时设置 locked=0;第二个释放时看到已释放,触发 panic。
// acquire() in kernel/kernel.asm
a5 = 1
s1 = &lk->locked
amoswap.w.aq a5, a5, (s1)
// 硬件完成:
// lock *s1 in memory system
// temp = *s1
// *s1 = a5
// a5 = temp
// unlock *s1
acquire(l) {
while (__sync_lock_test_and_set(&lk->locked, 1) != 0)
;
}
lk->locked 已是 1,sync_lock_test_and_set 再设为 1,返回 1,循环继续自旋为什么在 acquire 中禁用中断?如果设备中断,中断处理程序需要同一锁(如 UART 代码),会死锁。
// 这段代码可能被编译器或 CPU 重排!
locked = 1 (aquire)
x = x + 1
locked = 0 (release)
// 可能生成:
locked = 1
locked = 0 // 编译器把增量移出临界区!
x = x + 1
__sync_synchronize() 防止重排:编译器不会跨越它移动内存引用,并可能发出"内存屏障"指令告诉 CPU。使用锁时,你无需理解内存排序规则。
自旋锁在等待时浪费 CPU。为什么不放弃 CPU、切换到另一个进程?
自旋锁指南:
系统为较长临界区提供"阻塞锁"(等待线程放弃 CPU),但开销通常更高。
Q1:为什么非原子的 test-and-set 锁实现会失败?
Q2:死锁的解决方案是什么?
Q3:为什么自旋锁的 acquire 中要禁用中断?
Q4:__sync_synchronize() 的作用是什么?
Q5:锁粒度设计的最佳实践是什么?
并发访问共享数据(至少一个写)必须通过锁保护,否则会出现竞态条件。
RISC-V 的 amoswap 等硬件原子指令是实现锁的基石——把问题推给硬件。
锁粒度太粗限制并行,太细增加复杂度和死锁风险。从粗开始,按需细化。
编译器和 CPU 都可能重排内存引用,__sync_synchronize 和内存屏障确保正确顺序。