Futex Requeue PI¶
将任务从非 PI futex 重新排队到 PI futex 需要特殊处理,以确保如果底层 rt_mutex 有等待者,它永远不会没有所有者;这样做会破坏 PI 提升逻辑 [参见 RT 互斥实现设计] 。为了简洁起见,本文档中将此操作称为“requeue_pi”。优先级继承在全文中缩写为“PI”。
动机¶
如果没有 requeue_pi,pthread_cond_broadcast() 的 glibc 实现必须诉诸于唤醒所有等待 pthread_condvar 的任务,并让它们尝试以经典的雷霆群模式来确定哪个任务先运行。理想的实现是唤醒最高优先级的等待者,其余的留给与条件变量关联的互斥锁解锁时固有的自然唤醒。
考虑简化的 glibc 调用
/* caller must lock mutex */
pthread_cond_wait(cond, mutex)
{
lock(cond->__data.__lock);
unlock(mutex);
do {
unlock(cond->__data.__lock);
futex_wait(cond->__data.__futex);
lock(cond->__data.__lock);
} while(...)
unlock(cond->__data.__lock);
lock(mutex);
}
pthread_cond_broadcast(cond)
{
lock(cond->__data.__lock);
unlock(cond->__data.__lock);
futex_requeue(cond->data.__futex, cond->mutex);
}
一旦 pthread_cond_broadcast() 将任务重新排队,cond->mutex 就会有等待者。请注意,pthread_cond_wait() 仅在返回到用户空间后才尝试锁定互斥锁。这将使底层 rt_mutex 具有等待者,而没有所有者,从而破坏了之前提到的 PI 提升算法。
为了支持 PI 感知的 pthread_condvar,内核需要能够将任务重新排队到 PI futex。此支持意味着在成功调用 futex_wait 系统调用后,调用者将已经持有 PI futex 返回到用户空间。glibc 实现将修改如下
/* caller must lock mutex */
pthread_cond_wait_pi(cond, mutex)
{
lock(cond->__data.__lock);
unlock(mutex);
do {
unlock(cond->__data.__lock);
futex_wait_requeue_pi(cond->__data.__futex);
lock(cond->__data.__lock);
} while(...)
unlock(cond->__data.__lock);
/* the kernel acquired the mutex for us */
}
pthread_cond_broadcast_pi(cond)
{
lock(cond->__data.__lock);
unlock(cond->__data.__lock);
futex_requeue_pi(cond->data.__futex, cond->mutex);
}
实际的 glibc 实现可能会测试 PI,并在现有调用中进行必要的更改,而不是为 PI 情况创建新的调用。pthread_cond_timedwait() 和 pthread_cond_signal() 也需要类似的更改。
实现¶
为了确保 rt_mutex 在有等待者的情况下有所有者,重新排队代码和等待代码都需要能够在返回到用户空间之前获取 rt_mutex。重新排队代码不能简单地唤醒等待者并让它去获取 rt_mutex,因为它会在重新排队调用返回到用户空间和等待者唤醒并开始运行之间打开一个竞争窗口。在没有竞争的情况下尤其如此。
该解决方案涉及两个新的 rt_mutex 辅助例程,rt_mutex_start_proxy_lock() 和 rt_mutex_finish_proxy_lock(),它们允许重新排队代码代表等待者获取未竞争的 rt_mutex,并将等待者排队到竞争的 rt_mutex。两个新的系统调用提供了内核 <-> 用户接口来 requeue_pi:FUTEX_WAIT_REQUEUE_PI 和 FUTEX_CMP_REQUEUE_PI。
FUTEX_WAIT_REQUEUE_PI 由等待者(pthread_cond_wait() 和 pthread_cond_timedwait())调用,以阻止初始 futex 并等待重新排队到 PI 感知的 futex。该实现是 futex_wait() 和 futex_lock_pi() 之间高速冲突的结果,并带有额外的逻辑来检查额外的唤醒场景。
FUTEX_CMP_REQUEUE_PI 由唤醒者(pthread_cond_broadcast() 和 pthread_cond_signal())调用,以重新排队并可能唤醒等待的任务。在内部,此系统调用仍然由 futex_requeue 处理(通过传递 requeue_pi=1)。在重新排队之前,futex_requeue()
尝试代表顶级等待者获取重新排队的目标 PI futex。如果可以,则唤醒此等待者。futex_requeue()
然后继续将剩余的 nr_wake+nr_requeue 任务重新排队到 PI futex,并在每次重新排队之前调用 rt_mutex_start_proxy_lock(),以准备该任务作为底层 rt_mutex 上的等待者。如果也可以在此阶段获取锁,则唤醒下一个等待者以完成锁的获取。
FUTEX_CMP_REQUEUE_PI 接受 nr_wake 和 nr_requeue 作为参数,但它们的总和才是真正重要的。futex_requeue()
将唤醒或重新排队最多 nr_wake + nr_requeue 个任务。它只会唤醒尽可能多可以获取锁的任务,在大多数情况下应为 0,因为良好的编程实践表明 pthread_cond_broadcast() 或 pthread_cond_signal() 的调用者在进行调用之前获取互斥锁。FUTEX_CMP_REQUEUE_PI 要求 nr_wake=1。对于广播,nr_requeue 应为 INT_MAX,对于信号,nr_requeue 应为 0。