如果刚开始系统调用,自旋锁被立即释放? 如果任何时候都可能发生中断?
-
问题本质: 这是 Slow Path 中最棘手的竞态条件 (Race Condition) 问题,也称为“丢失唤醒 (Lost Wakeup)”问题。考虑以下场景:
- 线程 T1 尝试获取锁,发现锁被占用(原子操作失败)。
- T1 决定放弃自旋,准备调用
futex_wait
进入睡眠。 - 就在 T1 即将进入
futex_wait
系统调用或者刚刚进入但尚未完全在内核中进入等待状态时,持有锁的线程 T2 释放了锁。 - T2 检查等待队列(可能发现是空的,因为 T1 还没完全登记为等待者),或者根据锁状态判断不需要唤醒,于是没有调用
futex_wake
。 -
T1 继续执行
futex_wait
,成功进入睡眠状态。 -
结果: 锁现在是可用的,但 T1 却在睡眠,并且可能永远不会被唤醒(因为 T2 的唤醒信号“丢失”了)。
- 中断的影响: 任何时候都可能发生的中断会加剧这个问题,因为它可能恰好发生在上述竞态窗口期间,延迟 T1 进入睡眠或延迟 T2 释放锁和检查等待者的过程。
-
futex
(Fast Userspace Mutex) 机制的设计巧妙地解决了这个问题: -
原子检查与睡眠:
futex_wait(void *uaddr, int expected_val)
系统调用的关键在于,它在内核态执行一个原子操作:- 它首先检查用户态地址
uaddr
(通常是锁变量的地址)的值。 - 只有当该地址的值仍然等于调用者传入的
expected_val
(即线程 T1 在用户态看到的那个“被占用”的值)时,内核才会将该线程放入等待队列并使其睡眠。 - 如果内核检查时发现
uaddr
的值已经不等于expected_val
(意味着锁的状态在 T1 调用futex_wait
后、内核检查前发生了改变,很可能锁已经被释放了),那么futex_wait
不会让线程睡眠,而是立即返回一个错误码(通常是EWOULDBLOCK
或类似指示条件不满足的错误)。 -
避免丢失唤醒:
-
当
futex_wait
因为值不匹配而立即返回时,线程 T1 就知道锁的状态可能变了,它会回到用户态重新尝试获取锁(回到 Fast Path 或再次短暂自旋)。 - 当锁的持有者 T2 释放锁时,它会先修改锁变量的值(例如,从“锁定”改为“未锁定”),然后再调用
futex_wake(uaddr, num_to_wake)
。 - 由于
futex_wait
的原子检查机制,保证了如果一个线程 T1 确实因为看到了“锁定”状态而成功进入了内核等待队列,那么 T2 在将状态改为“未锁定”后调用的futex_wake
一定能找到并唤醒 T1(或者其他等待者)。不可能出现 T1 看到锁定状态 -> T2 改为未锁定且不唤醒 -> T1 成功睡着的情况。
- 它首先检查用户态地址