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

Linux 6.18.37 · Locking

Lightweight PI futex

User-space uncontended fastpath를 유지하면서 FUTEX_LOCK_PI slowpath에서 rt_mutex priority inheritance를 연결하는 원리를 설명합니다.

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

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

1. 요약·해설

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

왜 lightweight인가

pi-futex.rst:1-19
  • 경쟁이 없으면 user space atomic operation만으로 lock과 unlock이 끝난다.
  • Slowpath syscall과 scheduling pattern이 일반 futex와 비슷하다.
  • 한 owner, owner-only unlock, non-recursive라는 mutex 규칙으로 in-kernel PI 구현 범위를 제한한다.

PI futex는 미리 kernel에 등록하는 object가 아닙니다. Futex word의 uncontended state에서는 kernel queue와 pi_state가 존재하지 않으며 실제 경쟁이 생길 때만 kernel object를 구성합니다.

User-space lock에 PI가 필요한 이유

pi-futex.rst:20-74

High-priority audio thread가 low-priority UI thread와 짧은 lock을 공유한다고 가정합니다. Critical section 자체가 bounded여도 medium-priority decoder가 low owner를 계속 선점하면 high thread의 대기는 bounded하지 않습니다. User space는 critical section에서 interrupt나 preemption을 끌 수 없으므로 spinlock도 같은 inversion 문제를 가집니다.

PI는 모든 application latency를 자동으로 고정하지 않지만 lock dependency로 발생하는 unbounded 중간-priority 간섭을 줄입니다. Lockless algorithm이 실제로 가능한지와 review 가능한 복잡도도 함께 고려해야 합니다.

Futex word와 fastpath

pi-futex.rst:75-91
User futex word의미
0unlocked
TID해당 task가 owner, waiter 없음
FUTEX_WAITERS | TIDowner가 있고 kernel slowpath waiter 존재

Acquire는 0에서 own TID로 atomic transition을 시도하고, release는 own TID에서 0으로 바꿉니다. 두 연산이 성공하면 syscall이 없습니다. FUTEX_WAITERS bit가 있거나 owner 값이 다르면 slowpath로 갑니다.

FUTEX_LOCK_PI와 FUTEX_UNLOCK_PI

pi-futex.rst:92-123

FUTEX_LOCK_PI는 futex address의 queue를 찾거나 만들고 word에 기록된 TID로 owner task를 확인합니다. Queue의 pi_state에 rt_mutex를 연결하고 기존 task를 owner로 설정한 뒤 FUTEX_WAITERS bit를 세웁니다. Caller는 rt_mutex에서 block되고, 획득 후 futex word를 자신의 TID로 갱신해 반환합니다.

Unlock fastpath가 FUTEX_WAITERS 때문에 실패하면 FUTEX_UNLOCK_PI가 user space 대신 pi_state->rt_mutex를 해제하고 다음 waiter를 깨웁니다. Robustness와 PI는 서로 독립된 속성이므로 일반, robust, PI, robust+PI 네 조합이 모두 가능합니다.

2. 영어 원문 전체

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

원문 전체 펼치기
1 ======================
2 Lightweight PI-futexes
3 ======================
4
5 We are calling them lightweight for 3 reasons:
6
7 - in the user-space fastpath a PI-enabled futex involves no kernel work
8 (or any other PI complexity) at all. No registration, no extra kernel
9 calls - just pure fast atomic ops in userspace.
10
11 - even in the slowpath, the system call and scheduling pattern is very
12 similar to normal futexes.
13
14 - the in-kernel PI implementation is streamlined around the mutex
15 abstraction, with strict rules that keep the implementation
16 relatively simple: only a single owner may own a lock (i.e. no
17 read-write lock support), only the owner may unlock a lock, no
18 recursive locking, etc.
19
20 Priority Inheritance - why?
21 ---------------------------
22
23 The short reply: user-space PI helps achieving/improving determinism for
24 user-space applications. In the best-case, it can help achieve
25 determinism and well-bound latencies. Even in the worst-case, PI will
26 improve the statistical distribution of locking related application
27 delays.
28
29 The longer reply
30 ----------------
31
32 Firstly, sharing locks between multiple tasks is a common programming
33 technique that often cannot be replaced with lockless algorithms. As we
34 can see it in the kernel [which is a quite complex program in itself],
35 lockless structures are rather the exception than the norm - the current
36 ratio of lockless vs. locky code for shared data structures is somewhere
37 between 1:10 and 1:100. Lockless is hard, and the complexity of lockless
38 algorithms often endangers to ability to do robust reviews of said code.
39 I.e. critical RT apps often choose lock structures to protect critical
40 data structures, instead of lockless algorithms. Furthermore, there are
41 cases (like shared hardware, or other resource limits) where lockless
42 access is mathematically impossible.
43
44 Media players (such as Jack) are an example of reasonable application
45 design with multiple tasks (with multiple priority levels) sharing
46 short-held locks: for example, a highprio audio playback thread is
47 combined with medium-prio construct-audio-data threads and low-prio
48 display-colory-stuff threads. Add video and decoding to the mix and
49 we've got even more priority levels.
50
51 So once we accept that synchronization objects (locks) are an
52 unavoidable fact of life, and once we accept that multi-task userspace
53 apps have a very fair expectation of being able to use locks, we've got
54 to think about how to offer the option of a deterministic locking
55 implementation to user-space.
56
57 Most of the technical counter-arguments against doing priority
58 inheritance only apply to kernel-space locks. But user-space locks are
59 different, there we cannot disable interrupts or make the task
60 non-preemptible in a critical section, so the 'use spinlocks' argument
61 does not apply (user-space spinlocks have the same priority inversion
62 problems as other user-space locking constructs). Fact is, pretty much
63 the only technique that currently enables good determinism for userspace
64 locks (such as futex-based pthread mutexes) is priority inheritance:
65
66 Currently (without PI), if a high-prio and a low-prio task shares a lock
67 [this is a quite common scenario for most non-trivial RT applications],
68 even if all critical sections are coded carefully to be deterministic
69 (i.e. all critical sections are short in duration and only execute a
70 limited number of instructions), the kernel cannot guarantee any
71 deterministic execution of the high-prio task: any medium-priority task
72 could preempt the low-prio task while it holds the shared lock and
73 executes the critical section, and could delay it indefinitely.
74
75 Implementation
76 --------------
77
78 As mentioned before, the userspace fastpath of PI-enabled pthread
79 mutexes involves no kernel work at all - they behave quite similarly to
80 normal futex-based locks: a 0 value means unlocked, and a value==TID
81 means locked. (This is the same method as used by list-based robust
82 futexes.) Userspace uses atomic ops to lock/unlock these mutexes without
83 entering the kernel.
84
85 To handle the slowpath, we have added two new futex ops:
86
87 - FUTEX_LOCK_PI
88 - FUTEX_UNLOCK_PI
89
90 If the lock-acquire fastpath fails, [i.e. an atomic transition from 0 to
91 TID fails], then FUTEX_LOCK_PI is called. The kernel does all the
92 remaining work: if there is no futex-queue attached to the futex address
93 yet then the code looks up the task that owns the futex [it has put its
94 own TID into the futex value], and attaches a 'PI state' structure to
95 the futex-queue. The pi_state includes an rt-mutex, which is a PI-aware,
96 kernel-based synchronization object. The 'other' task is made the owner
97 of the rt-mutex, and the FUTEX_WAITERS bit is atomically set in the
98 futex value. Then this task tries to lock the rt-mutex, on which it
99 blocks. Once it returns, it has the mutex acquired, and it sets the
100 futex value to its own TID and returns. Userspace has no other work to
101 perform - it now owns the lock, and futex value contains
102 FUTEX_WAITERS|TID.
103
104 If the unlock side fastpath succeeds, [i.e. userspace manages to do a
105 TID -> 0 atomic transition of the futex value], then no kernel work is
106 triggered.
107
108 If the unlock fastpath fails (because the FUTEX_WAITERS bit is set),
109 then FUTEX_UNLOCK_PI is called, and the kernel unlocks the futex on the
110 behalf of userspace - and it also unlocks the attached
111 pi_state->rt_mutex and thus wakes up any potential waiters.
112
113 Note that under this approach, contrary to previous PI-futex approaches,
114 there is no prior 'registration' of a PI-futex. [which is not quite
115 possible anyway, due to existing ABI properties of pthread mutexes.]
116
117 Also, under this scheme, 'robustness' and 'PI' are two orthogonal
118 properties of futexes, and all four combinations are possible: futex,
119 robust-futex, PI-futex, robust+PI-futex.
120
121 More details about priority inheritance can be found in
122 Documentation/locking/rt-mutex.rst.
123

3. 한국어 전문 번역

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

PI-futex가 lightweight인 세 가지 이유

1-18
  • PI가 활성화된 futex의 userspace fast path에는 kernel 작업이나 PI 관련 복잡성이 전혀 없다. 사전 등록이나 추가 system call 없이 userspace의 빠른 atomic operation만 수행한다.
  • Slow path에서도 system call과 scheduling pattern은 일반 futex와 매우 비슷하다.
  • Kernel 내부 PI 구현은 mutex abstraction을 중심으로 단순화되어 있다. 한 lock에는 owner가 하나만 존재하고, owner만 unlock할 수 있으며, recursive locking과 read-write lock을 지원하지 않는 등의 엄격한 규칙으로 구현 복잡성을 제한한다.

Priority inheritance가 필요한 이유

20-27

짧게 답하면 userspace PI는 userspace application의 결정성을 달성하거나 개선하는 데 도움이 된다. 가장 좋은 경우에는 동작을 결정적으로 만들고 latency에 명확한 upper bound를 둘 수 있다. 최악의 경우에도 locking 때문에 발생하는 application 지연의 통계적 분포를 개선한다.

공유 lock이 피할 수 없는 이유

29-55

여러 task가 lock을 공유하는 것은 흔한 programming 방식이며 lockless algorithm으로 대체할 수 없는 경우가 많다. Kernel처럼 복잡한 프로그램에서도 lockless 자료 구조는 일반적인 방식이 아니라 예외에 가깝다. 공유 자료 구조에서 lockless code와 lock을 사용하는 code의 비율은 대략 1:10에서 1:100 사이로 추정된다.

Lockless algorithm은 작성하기 어렵고 복잡성이 커서 견고한 code review를 방해할 수 있다. 따라서 중요한 real-time application도 핵심 자료 구조를 보호할 때 lockless algorithm보다 lock을 선택하는 경우가 많다. 공유 hardware나 자원 제한처럼 lockless access가 수학적으로 불가능한 사례도 있다.

Jack 같은 media player는 서로 다른 priority의 여러 task가 짧게 보유하는 lock을 공유하는 합리적인 설계 사례다. 높은 priority의 audio playback thread, 중간 priority의 audio data 생성 thread, 낮은 priority의 display 처리 thread가 함께 동작하고 video와 decoding까지 더하면 priority 단계가 더 많아진다.

Synchronization object가 피할 수 없고 multi-task userspace application이 lock 사용을 기대하는 것이 타당하다면, userspace에도 결정적인 locking 구현을 선택할 방법을 제공해야 한다.

Userspace lock의 priority inversion

57-73

Priority inheritance에 반대하는 기술적 논거 대부분은 kernel-space lock에만 적용된다. Userspace critical section에서는 interrupt를 disable하거나 task를 non-preemptible 상태로 만들 수 없으므로 spinlock을 사용하라는 해법은 적용되지 않는다. Userspace spinlock도 다른 userspace lock과 똑같은 priority inversion 문제를 갖는다.

현재 futex 기반 pthread mutex 같은 userspace lock에 좋은 결정성을 제공하는 사실상 유일한 기법은 priority inheritance다.

PI가 없을 때 높은 priority task와 낮은 priority task가 lock을 공유하면, 모든 critical section을 짧고 제한된 명령만 실행하도록 작성해도 높은 priority task의 실행 시간을 보장할 수 없다. 낮은 priority task가 lock을 보유한 채 critical section을 실행하는 동안 중간 priority task가 이를 preempt하면, lock owner의 실행이 기한 없이 밀리고 그 lock을 기다리는 높은 priority task도 함께 지연된다.

Userspace fast path의 futex word

75-83

PI가 활성화된 pthread mutex의 userspace fast path는 kernel에 들어가지 않는다. 일반 futex lock과 마찬가지로 futex word가 0이면 unlocked 상태이고 값이 TID이면 해당 thread가 lock을 보유한 상태다. List 기반 robust futex도 같은 표현을 사용한다.

경쟁이 없을 때 userspace는 atomic operation으로 0에서 자신의 TID로 바꾸어 lock하고, 자신의 TID에서 0으로 바꾸어 unlock한다.

FUTEX_LOCK_PI slow path

85-102
FUTEX_LOCK_PI
FUTEX_UNLOCK_PI

Lock acquire fast path, 즉 futex word를 0에서 자신의 TID로 바꾸는 atomic transition이 실패하면 FUTEX_LOCK_PI를 호출한다. 그 뒤의 작업은 kernel이 수행한다.

해당 futex address에 아직 futex queue가 연결되어 있지 않다면 kernel은 futex word에 자신의 TID를 기록해 둔 현재 owner task를 찾고 futex queue에 PI state 구조체를 연결한다. pi_state에는 PI를 인식하는 kernel synchronization object인 rt_mutex가 들어 있다.

Kernel은 기존 futex owner를 rt_mutex의 owner로 설정하고 futex word의 FUTEX_WAITERS bit를 atomic하게 설정한다. 요청 task는 이 rt_mutex를 lock하려다 block된다. rt_mutex lock에서 돌아왔을 때 요청 task는 mutex를 획득한 상태이며, futex word를 FUTEX_WAITERS|TID로 바꾸고 userspace로 돌아간다. Userspace가 추가로 처리할 작업은 없다.

Unlock fast path와 FUTEX_UNLOCK_PI

104-111

Unlock fast path에서 userspace가 futex word를 TID에서 0으로 atomic하게 바꾸는 데 성공하면 kernel 작업은 발생하지 않는다.

FUTEX_WAITERS bit가 설정되어 있어 unlock fast path가 실패하면 FUTEX_UNLOCK_PI를 호출한다. Kernel은 userspace를 대신해 futex를 unlock하고 연결된 pi_state->rt_mutex도 unlock하여 대기 중인 task를 깨운다.

사전 등록 없이 조합 가능한 속성

113-122

이 설계는 이전 PI-futex 방식과 달리 PI-futex를 미리 등록하지 않는다. 기존 pthread mutex ABI의 성질 때문에 사전 등록 자체가 현실적으로 어렵기도 하다.

Robustness와 PI는 서로 독립적인 futex 속성이다. 따라서 일반 futex, robust futex, PI-futex, robust+PI-futex라는 네 조합을 모두 만들 수 있다.

Priority inheritance의 더 자세한 동작은 Documentation/locking/rt-mutex.rst에서 설명한다.