如果刚开始系统调用,自旋锁被立即释放? 如果任何时候都可能发生中断?

  • 问题本质: 这是 Slow Path 中最棘手的竞态条件 (Race Condition) 问题,也称为“丢失唤醒 (Lost Wakeup)”问题。考虑以下场景:

    1. 线程 T1 尝试获取锁,发现锁被占用(原子操作失败)。
    2. T1 决定放弃自旋,准备调用 futex_wait 进入睡眠。
    3. 就在 T1 即将进入 futex_wait 系统调用或者刚刚进入但尚未完全在内核中进入等待状态时,持有锁的线程 T2 释放了锁。
    4. T2 检查等待队列(可能发现是空的,因为 T1 还没完全登记为等待者),或者根据锁状态判断不需要唤醒,于是没有调用 futex_wake
    5. T1 继续执行 futex_wait,成功进入睡眠状态。

    6. 结果: 锁现在是可用的,但 T1 却在睡眠,并且可能永远不会被唤醒(因为 T2 的唤醒信号“丢失”了)。

    7. 中断的影响: 任何时候都可能发生的中断会加剧这个问题,因为它可能恰好发生在上述竞态窗口期间,延迟 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 成功睡着的情况。