并发控制:互斥 (自旋锁、互斥锁和 futex) 在多处理器系统上实现互斥
RISC-V: 另一种原子操作的设计
考虑常见的原子操作:
- atomic test-and-set
reg = load(x); if (reg == XX) { store(x, YY); }
- lock xchg
reg = load(x); store(x, XX);
- lock add
t = load(x); t++; store(x, t);
它们的本质都是:
- load
- exec (处理器本地寄存器的运算)
- store
Load-Reserved/Store-Conditional (LR/SC)
LR: 在内存上标记 reserved (盯上你了),中断、其他处理器写入都会导致标记消除
lr.w rd, (rs1)
rd = M[rs1]
reserve M[rs1]
SC: 如果 “盯上” 未被解除,则写入
sc.w rd, rs2, (rs1)
if still reserved:
M[rs1] = rs2
rd = 0
else:
rd = nonzero
Compare-and-Swap 的 LR/SC 实现
int cas(int *addr, int cmp_val, int new_val) {
int old_val = *addr;
if (old_val == cmp_val) {
*addr = new_val; return 0;
} else { return 1; }
}
cas:
lr.w t0, (a0) # Load original value.
bne t0, a1, fail # Doesn’t match, so fail.
sc.w t0, a2, (a0) # Try to update.
bnez t0, cas # Retry if store-conditional failed.
li a0, 0 # Set return to success.
jr ra # Return.
fail:
li a0, 1 # Set return to failure.
jr ra # Return
LR/SC 的硬件实现
https://jyywiki.cn/OS/2022/slides/5.slides#/2/11
BOOM (Berkeley Out-of-Order Processor)
- riscv-boom
lsu/dcache.scala
- 留意
s2_sc_fail
的条件- s2 是流水线 Stage 2
- (yzh 扒出的代码)
本次课回答的问题
- Q: 如何在多处理器系统上实现互斥?
Take-away message
- 软件不够,硬件来凑 (自旋锁)
- 用户不够,内核来凑 (互斥锁)
- 找到你依赖的假设,并大胆地打破它
- Fast/slow paths: 性能优化的重要途径
Futex: Fast Userspace muTexes (cont'd)
先在用户空间自旋
- 如果获得锁,直接进入
- 未能获得锁,系统调用
- 解锁以后也需要系统调用
- futex.py
- 更好的设计可以在 fast-path 不进行系统调用
RTFM (劝退)
- futex (7), futex (2)
- A futex overview and update (LWN)
- Futexes are tricky (论 model checker 的重要性)
- (我们不讲并发算法)
Futex: Fast Userspace muTexes
小孩子才做选择。我当然是全都要啦!
- Fast path: 一条原子指令,上锁成功立即返回
- Slow path: 上锁失败,执行系统调用睡眠
- 性能优化的最常见技巧
- 看 average (frequent) case 而不是 worst case
- 性能优化的最常见技巧
POSIX 线程库中的互斥锁 (pthread_mutex
)
- sum-scalability.c,换成 mutex
- 观察系统调用 (strace)
- gdb 调试
set scheduler-locking on
,info threads
,thread X
关于互斥的一些分析
自旋锁 (线程直接共享 locked)
- 更快的 fast path
- xchg 成功 → 立即进入临界区,开销很小
- 更慢的 slow path
- xchg 失败 → 浪费 CPU 自旋等待
睡眠锁 (通过系统调用访问 locked)
- 更快的 slow path
- 上锁失败线程不再占用 CPU
- 更慢的 fast path
- 即便上锁成功也需要进出内核 (syscall)
Futex = Spin + Mutex
实现线程 + 长临界区的互斥 (cont'd)
操作系统 = 更衣室管理员
- 先到的人 (线程)
- 成功获得手环,进入游泳馆
*lk = 🔒
,系统调用直接返回
- 后到的人 (线程)
- 不能进入游泳馆,排队等待
- 线程放入等待队列,执行线程切换 (yield)
- 洗完澡出来的人 (线程)
- 交还手环给管理员;管理员把手环再交给排队的人
- 如果等待队列不空,从等待队列中取出一个线程允许执行
- 如果等待队列为空,
*lk = ✅
- 管理员 (OS) 使用自旋锁确保自己处理手环的过程是原子的
实现线程 + 长临界区的互斥
作业那么多,与其干等 Online Judge 发布,不如把自己 (CPU) 让给其他作业 (线程) 执行?
“让” 不是 C 语言代码可以做到的 (C 代码只能计算)
- 把锁的实现放到操作系统里就好啦!
syscall(SYSCALL_lock, &lk);
- 试图获得
lk
,但如果失败,就切换到其他线程
- 试图获得
syscall(SYSCALL_unlock, &lk);
- 释放
lk
,如果有等待锁的线程就唤醒
- 释放
https://jyywiki.cn/OS/2022/slides/5.slides#/3/1
https://jyywiki.cn/OS/2022/slides/5.slides#/2/2
x86 原子操作:LOCK
指令前缀
例子:sum-atomic.c
sum = 200000000
Atomic exchange (load + store)
int xchg(volatile int *addr, int newval) {
int result;
asm volatile ("lock xchg %0, %1"
: "+m"(*addr), "=a"(result) : "1"(newval));
return result;
}
更多的原子指令:stdatomic.h (C11)
原子指令
内联汇编 asm volatile("lock addq $1, %0": "+m"(sum));
#include "thread.h" #define N 100000000 long sum = 0; void Tsum() { for (int i = 0; i < N; i++) { asm volatile("lock addq $1, %0": "+m"(sum)); } } int main() { create(Tsum); create(Tsum); join(); printf("sum = %ld\n", sum); }
https://jyywiki.cn/OS/2022/slides/5.slides#/1/3
实现互斥的根本困难:不能同时读/写共享内存
- load (环顾四周) 的时候不能写,只能 “看一眼就把眼睛闭上”
- 看到的东西马上就过时了
- store (改变物理世界状态) 的时候不能读,只能 “闭着眼睛动手”
- 也不知道把什么改成了什么
【并发控制:互斥 (自旋锁、互斥锁和 futex) [南京大学2022操作系统-P5]】https://www.bilibili.com/video/BV1ja411h7jt