← Documents Documentation/locking/futex-requeue-pi.rst GitHub 원문 ↗

Linux 6.18.37 · Locking

Futex requeue PI

Condition variable waiter를 non-PI futex에서 PI futex로 옮길 때 owner 없는 rt_mutex를 만들지 않는 proxy-lock 절차를 설명합니다.

Source pathDocumentation/locking/futex-requeue-pi.rst
Source versionLinux v6.18.37
TranslationDUJINLABS 전문 번역 + 해설

요약·해설과 원문, 전문 번역을 서로 분리했습니다. API 이름, symbol, source path는 원문 표기를 사용합니다.

1. 요약·해설

원문의 핵심 논리와 kernel programming 관점의 보충 설명입니다. 아래의 전문 번역과는 별도로 작성했습니다.

Condition variable broadcast의 문제

futex-requeue-pi.rst:1-46

일반 pthread_cond_broadcast()가 condition futex의 모든 waiter를 깨우면 모두가 연결된 mutex를 얻으려 경쟁하는 thundering herd가 생깁니다. 이상적인 동작은 최고 priority waiter만 mutex owner로 진행시키고, 나머지는 그 mutex의 unlock 순서에 따라 깨우는 것입니다.

단순 FUTEX_REQUEUE로 waiter를 PI mutex 쪽 futex queue에 옮기기만 하면 user space로 돌아간 waiter가 나중에 mutex를 획득합니다. 그 사이 underlying rt_mutex에는 waiter가 있지만 owner가 없는 상태가 생겨 priority inheritance가 누구에게 boost를 전달해야 할지 알 수 없게 됩니다.

WAIT_REQUEUE_PI의 반환 계약

futex-requeue-pi.rst:47-85
pthread_cond_wait_pi(cond, mutex)
{
    unlock(mutex);
    futex_wait_requeue_pi(cond_futex, mutex_futex);
    /* 성공 반환 시 kernel이 mutex를 이미 획득해 두었다. */
}

PI-aware wait에서는 syscall이 성공하여 user space로 돌아올 때 caller가 target PI futex를 이미 보유해야 합니다. 이 계약 덕분에 rt_mutex waiter가 존재하는 동안 owner가 비는 구간이 사라지고 glibc가 다시 lock(mutex)를 호출할 필요도 없습니다.

같은 원리는 pthread_cond_timedwait(), pthread_cond_signal(), pthread_cond_broadcast()에 적용됩니다. 실제 library는 별도 public API를 만들기보다 mutex attribute가 PI인지 보고 내부 futex operation을 선택합니다.

두 futex operation과 proxy lock

futex-requeue-pi.rst:86-121
Operation호출자역할
FUTEX_WAIT_REQUEUE_PIcond wait의 waiter초기 non-PI futex에서 잠들고 PI futex로 requeue될 준비
FUTEX_CMP_REQUEUE_PIsignal 또는 broadcast waker비교 후 top waiter를 대신해 target rt_mutex 획득을 시도하고 나머지를 이동
rt_mutex_start_proxy_lock()kernel requeue pathwaiter 대신 uncontended lock을 얻거나 contended rt_mutex waiter로 enqueue
rt_mutex_finish_proxy_lock()깨어난 waiterproxy acquire 결과를 마무리하고 반환 상태 결정

Waker는 top waiter를 대신해 target PI futex를 먼저 획득하려고 시도합니다. 성공하면 그 waiter를 깨우고, 나머지 task를 옮길 때마다 start_proxy_lock()으로 rt_mutex waiter 상태를 먼저 구성합니다. 단순 wake 후 경쟁시키는 방식과 달리 requeue syscall 반환과 waiter 실행 사이의 race window를 닫습니다.

nr_wake와 nr_requeue 규칙

futex-requeue-pi.rst:122-133

FUTEX_CMP_REQUEUE_PI는 nr_wake가 반드시 1이어야 합니다. 실제 처리 상한은 nr_wake + nr_requeue이고, kernel은 target lock을 대신 획득할 수 있는 task만 즉시 깨웁니다. Broadcast는 nr_requeue를 INT_MAX로, signal은 0으로 두는 방식이 사용됩니다.

Condvar signal 또는 broadcast caller가 연결 mutex를 보유하는 정상 사용에서는 target PI mutex가 contended 상태이므로 requeue 순간 즉시 깨울 수 있는 waiter 수는 보통 0입니다.

2. 영어 원문 전체

번역 기준이 된 Linux v6.18.37 원문입니다. 줄 번호는 이 버전의 파일 좌표입니다.

원문 전체 펼치기
1 ================
2 Futex Requeue PI
3 ================
4
5 Requeueing of tasks from a non-PI futex to a PI futex requires
6 special handling in order to ensure the underlying rt_mutex is never
7 left without an owner if it has waiters; doing so would break the PI
8 boosting logic [see rt-mutex-design.rst] For the purposes of
9 brevity, this action will be referred to as "requeue_pi" throughout
10 this document. Priority inheritance is abbreviated throughout as
11 "PI".
12
13 Motivation
14 ----------
15
16 Without requeue_pi, the glibc implementation of
17 pthread_cond_broadcast() must resort to waking all the tasks waiting
18 on a pthread_condvar and letting them try to sort out which task
19 gets to run first in classic thundering-herd formation. An ideal
20 implementation would wake the highest-priority waiter, and leave the
21 rest to the natural wakeup inherent in unlocking the mutex
22 associated with the condvar.
23
24 Consider the simplified glibc calls::
25
26 /* caller must lock mutex */
27 pthread_cond_wait(cond, mutex)
28 {
29 lock(cond->__data.__lock);
30 unlock(mutex);
31 do {
32 unlock(cond->__data.__lock);
33 futex_wait(cond->__data.__futex);
34 lock(cond->__data.__lock);
35 } while(...)
36 unlock(cond->__data.__lock);
37 lock(mutex);
38 }
39
40 pthread_cond_broadcast(cond)
41 {
42 lock(cond->__data.__lock);
43 unlock(cond->__data.__lock);
44 futex_requeue(cond->data.__futex, cond->mutex);
45 }
46
47 Once pthread_cond_broadcast() requeues the tasks, the cond->mutex
48 has waiters. Note that pthread_cond_wait() attempts to lock the
49 mutex only after it has returned to user space. This will leave the
50 underlying rt_mutex with waiters, and no owner, breaking the
51 previously mentioned PI-boosting algorithms.
52
53 In order to support PI-aware pthread_condvar's, the kernel needs to
54 be able to requeue tasks to PI futexes. This support implies that
55 upon a successful futex_wait system call, the caller would return to
56 user space already holding the PI futex. The glibc implementation
57 would be modified as follows::
58
59
60 /* caller must lock mutex */
61 pthread_cond_wait_pi(cond, mutex)
62 {
63 lock(cond->__data.__lock);
64 unlock(mutex);
65 do {
66 unlock(cond->__data.__lock);
67 futex_wait_requeue_pi(cond->__data.__futex);
68 lock(cond->__data.__lock);
69 } while(...)
70 unlock(cond->__data.__lock);
71 /* the kernel acquired the mutex for us */
72 }
73
74 pthread_cond_broadcast_pi(cond)
75 {
76 lock(cond->__data.__lock);
77 unlock(cond->__data.__lock);
78 futex_requeue_pi(cond->data.__futex, cond->mutex);
79 }
80
81 The actual glibc implementation will likely test for PI and make the
82 necessary changes inside the existing calls rather than creating new
83 calls for the PI cases. Similar changes are needed for
84 pthread_cond_timedwait() and pthread_cond_signal().
85
86 Implementation
87 --------------
88
89 In order to ensure the rt_mutex has an owner if it has waiters, it
90 is necessary for both the requeue code, as well as the waiting code,
91 to be able to acquire the rt_mutex before returning to user space.
92 The requeue code cannot simply wake the waiter and leave it to
93 acquire the rt_mutex as it would open a race window between the
94 requeue call returning to user space and the waiter waking and
95 starting to run. This is especially true in the uncontended case.
96
97 The solution involves two new rt_mutex helper routines,
98 rt_mutex_start_proxy_lock() and rt_mutex_finish_proxy_lock(), which
99 allow the requeue code to acquire an uncontended rt_mutex on behalf
100 of the waiter and to enqueue the waiter on a contended rt_mutex.
101 Two new system calls provide the kernel<->user interface to
102 requeue_pi: FUTEX_WAIT_REQUEUE_PI and FUTEX_CMP_REQUEUE_PI.
103
104 FUTEX_WAIT_REQUEUE_PI is called by the waiter (pthread_cond_wait()
105 and pthread_cond_timedwait()) to block on the initial futex and wait
106 to be requeued to a PI-aware futex. The implementation is the
107 result of a high-speed collision between futex_wait() and
108 futex_lock_pi(), with some extra logic to check for the additional
109 wake-up scenarios.
110
111 FUTEX_CMP_REQUEUE_PI is called by the waker
112 (pthread_cond_broadcast() and pthread_cond_signal()) to requeue and
113 possibly wake the waiting tasks. Internally, this system call is
114 still handled by futex_requeue (by passing requeue_pi=1). Before
115 requeueing, futex_requeue() attempts to acquire the requeue target
116 PI futex on behalf of the top waiter. If it can, this waiter is
117 woken. futex_requeue() then proceeds to requeue the remaining
118 nr_wake+nr_requeue tasks to the PI futex, calling
119 rt_mutex_start_proxy_lock() prior to each requeue to prepare the
120 task as a waiter on the underlying rt_mutex. It is possible that
121 the lock can be acquired at this stage as well, if so, the next
122 waiter is woken to finish the acquisition of the lock.
123
124 FUTEX_CMP_REQUEUE_PI accepts nr_wake and nr_requeue as arguments, but
125 their sum is all that really matters. futex_requeue() will wake or
126 requeue up to nr_wake + nr_requeue tasks. It will wake only as many
127 tasks as it can acquire the lock for, which in the majority of cases
128 should be 0 as good programming practice dictates that the caller of
129 either pthread_cond_broadcast() or pthread_cond_signal() acquire the
130 mutex prior to making the call. FUTEX_CMP_REQUEUE_PI requires that
131 nr_wake=1. nr_requeue should be INT_MAX for broadcast and 0 for
132 signal.
133

3. 한국어 전문 번역

영어 원문의 문단 순서와 의미를 유지한 전체 번역입니다. 코드, 함수명, symbol과 URL은 원문 표기를 유지합니다.

Requeue PI가 필요한 불변 조건

1-11

Task를 non-PI futex에서 PI futex로 requeue할 때는 특별한 처리가 필요하다. 기반 rt_mutex에 waiter가 있는데 owner가 없는 상태를 절대로 만들면 안 되기 때문이다. 그런 상태는 rt-mutex-design.rst에서 설명하는 PI priority boosting logic을 깨뜨린다.

이 문서에서는 이 동작을 줄여서 requeue_pi라고 부르고 priority inheritance는 PI로 표기한다.

pthread_cond_broadcast()와 thundering herd

13-22

requeue_pi가 없으면 glibc의 pthread_cond_broadcast() 구현은 pthread condition variable에서 기다리는 모든 task를 깨워야 한다. 깨어난 task들은 누가 먼저 실행할지를 두고 고전적인 thundering herd 형태로 경쟁한다.

이상적인 구현은 가장 높은 priority의 waiter 하나만 깨우고, 나머지는 condition variable과 연결된 mutex가 unlock될 때 자연스럽게 이어지는 wakeup에 맡기는 방식이다.

일반 condvar requeue의 문제

24-51
/* 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()가 task를 requeue하면 cond->mutex에 waiter가 생긴다. 하지만 pthread_cond_wait()는 userspace로 돌아온 뒤에야 mutex lock을 시도한다. 이 사이에는 기반 rt_mutex에 waiter는 있지만 owner는 없는 상태가 되어 PI boosting algorithm의 전제를 깨뜨린다.

PI-aware condvar의 호출 형태

53-84

PI를 인식하는 pthread condition variable을 지원하려면 kernel이 task를 PI futex로 requeue할 수 있어야 한다. 이 방식에서는 futex_wait system call이 성공하면 caller가 PI futex를 이미 보유한 채 userspace로 돌아온다.

/* 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 전용 API를 새로 만들기보다 기존 호출 안에서 PI 여부를 검사하고 필요한 동작을 선택할 가능성이 높다. pthread_cond_timedwait()와 pthread_cond_signal()에도 같은 변경이 필요하다.

Owner 없는 rt_mutex를 막는 proxy lock

86-102

rt_mutex에 waiter가 있다면 반드시 owner도 있도록 보장하려면 requeue code와 wait code 모두 userspace로 돌아가기 전에 rt_mutex를 획득할 수 있어야 한다. Requeue code가 waiter를 깨운 뒤 waiter 스스로 rt_mutex를 획득하게 두면 requeue system call이 userspace로 돌아간 시점과 waiter가 실제로 깨어 실행되는 시점 사이에 race window가 생긴다. 경쟁이 없는 경우에도 이 문제는 중요하다.

해결책은 rt_mutex_start_proxy_lock()과 rt_mutex_finish_proxy_lock()이라는 두 helper다. Requeue code는 waiter를 대신해 경쟁 없는 rt_mutex를 획득하거나, 경쟁 중인 rt_mutex의 wait queue에 waiter를 넣을 수 있다.

Kernel과 userspace 사이의 requeue_pi interface는 FUTEX_WAIT_REQUEUE_PI와 FUTEX_CMP_REQUEUE_PI라는 두 futex operation이 제공한다.

FUTEX_WAIT_REQUEUE_PI

104-109

Waiter 측의 pthread_cond_wait()와 pthread_cond_timedwait()는 FUTEX_WAIT_REQUEUE_PI를 호출해 최초 futex에서 block되고 PI-aware futex로 requeue되기를 기다린다. 구현은 futex_wait()와 futex_lock_pi()를 결합한 형태이며 추가 wakeup 상황을 검사하는 logic이 더해져 있다.

FUTEX_CMP_REQUEUE_PI

111-132

Waker 측의 pthread_cond_broadcast()와 pthread_cond_signal()은 FUTEX_CMP_REQUEUE_PI를 호출해 기다리는 task를 requeue하고 가능하면 깨운다. Kernel 내부에서는 requeue_pi=1을 넘긴 futex_requeue()가 이 operation을 처리한다.

Requeue 전에 futex_requeue()는 가장 높은 priority의 waiter를 대신해 target PI futex 획득을 시도한다. 획득할 수 있으면 해당 waiter를 깨운다. 이어서 남은 nr_wake+nr_requeue개 task를 PI futex로 옮기며, 각 requeue 전에 rt_mutex_start_proxy_lock()을 호출해 task를 기반 rt_mutex의 waiter로 준비한다. 이 단계에서 lock을 획득할 수 있다면 다음 waiter를 깨워 lock 획득을 마무리하게 한다.

FUTEX_CMP_REQUEUE_PI는 nr_wake와 nr_requeue를 받지만 실질적으로 중요한 값은 둘의 합이다. futex_requeue()는 최대 nr_wake+nr_requeue개 task를 깨우거나 requeue한다. 실제로 깨우는 수는 lock을 대신 획득할 수 있는 task 수뿐이다.

올바른 사용에서는 pthread_cond_broadcast()나 pthread_cond_signal() caller가 먼저 mutex를 보유하므로 대개의 경우 즉시 깨울 수 있는 task 수는 0이다. FUTEX_CMP_REQUEUE_PI는 nr_wake=1을 요구한다. Broadcast에서는 nr_requeue=INT_MAX, signal에서는 nr_requeue=0을 사용한다.