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

Linux 6.18.37 · Locking

Robust futex ABI

Thread별 robust_list_head, lock word의 TID·WAITERS·OWNER_DIED bit와 exit 중 list 삽입·삭제 race를 처리하는 ABI를 설명합니다.

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

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

1. 요약·해설

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

Thread별 user-space list 등록

robust-futex-ABI.rst:1-38
long set_robust_list(struct robust_list_head *head, size_t len);
long get_robust_list(int pid,
                     struct robust_list_head **head_ptr,
                     size_t *len_ptr);

각 thread는 자신이 현재 보유한 robust futex lock entry의 singly linked list head를 kernel에 한 번 등록합니다. Kernel은 current task에 user pointer만 기억하고 normal lock/unlock에는 관여하지 않다가 task exit 때 list를 걷습니다.

robust_list_head와 lock word 배치

robust-futex-ABI.rst:39-87
Field역할
head.list현재 보유 lock entry의 circular singly linked list
head.futex_offsetentry 주소에서 32-bit futex word까지의 signed offset
head.list_op_pending삽입 또는 제거 중인 entry의 임시 주소
lock word bit 0..29owner thread TID
lock word bit 30FUTEX_OWNER_DIED
lock word bit 31FUTEX_WAITERS

Kernel은 user-level mutex 구조 전체를 알지 않습니다. 모든 entry에서 같은 futex_offset만 알면 각 lock word를 계산할 수 있습니다. Application은 수천 개 lock 중 현재 thread가 보유한 것만 list에 연결합니다.

삽입과 제거의 list_op_pending protocol

robust-futex-ABI.rst:88-137
삽입 순서제거 순서
1. list_op_pending = entry1. list_op_pending = entry
2. futex lock 획득2. entry를 held list에서 제거
3. entry를 list에 연결하고 word에 TID 기록3. futex lock 해제
4. list_op_pending = NULL4. list_op_pending = NULL

Task가 lock을 얻은 직후 list에 넣기 전에 죽거나 list에서 뺀 직후 unlock하기 전에 죽는 instruction window를 pending field가 덮습니다. Exit scanner는 list entry뿐 아니라 pending entry도 검사하여 중간 상태의 owner death를 놓치지 않습니다.

Task exit 시 kernel 동작

robust-futex-ABI.rst:138-172

계산한 lock word의 하위 30비트가 exiting thread TID와 같으면 kernel은 FUTEX_OWNER_DIED를 atomic하게 세웁니다. FUTEX_WAITERS도 켜져 있으면 해당 address에서 futex wake를 수행하여 다음 waiter 하나가 owner death를 처리하게 합니다.

User pointer가 잘못되었거나 계산한 word가 유효하지 않거나 list가 100만 entry 한도를 넘으면 scanner는 조용히 중단합니다. User space를 신뢰하지 않으며 exit path가 손상된 list 때문에 무한 loop하거나 fault하면 안 됩니다.

복구 책임의 경계

robust-futex-ABI.rst:173-185

Kernel이 하는 일은 owner가 죽었다는 bit를 남기고 waiter를 깨우는 데까지입니다. 다음 owner는 pthread robust mutex API의 EOWNERDEAD를 받아 보호 데이터가 복구 가능한지 판단하고, 일관성을 복구한 뒤 pthread_mutex_consistent()에 해당하는 처리를 해야 합니다.

2. 영어 원문 전체

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

원문 전체 펼치기
1 ====================
2 The robust futex ABI
3 ====================
4
5 :Author: Started by Paul Jackson <[email protected]>
6
7
8 Robust_futexes provide a mechanism that is used in addition to normal
9 futexes, for kernel assist of cleanup of held locks on task exit.
10
11 The interesting data as to what futexes a thread is holding is kept on a
12 linked list in user space, where it can be updated efficiently as locks
13 are taken and dropped, without kernel intervention. The only additional
14 kernel intervention required for robust_futexes above and beyond what is
15 required for futexes is:
16
17 1) a one time call, per thread, to tell the kernel where its list of
18 held robust_futexes begins, and
19 2) internal kernel code at exit, to handle any listed locks held
20 by the exiting thread.
21
22 The existing normal futexes already provide a "Fast Userspace Locking"
23 mechanism, which handles uncontested locking without needing a system
24 call, and handles contested locking by maintaining a list of waiting
25 threads in the kernel. Options on the sys_futex(2) system call support
26 waiting on a particular futex, and waking up the next waiter on a
27 particular futex.
28
29 For robust_futexes to work, the user code (typically in a library such
30 as glibc linked with the application) has to manage and place the
31 necessary list elements exactly as the kernel expects them. If it fails
32 to do so, then improperly listed locks will not be cleaned up on exit,
33 probably causing deadlock or other such failure of the other threads
34 waiting on the same locks.
35
36 A thread that anticipates possibly using robust_futexes should first
37 issue the system call::
38
39 asmlinkage long
40 sys_set_robust_list(struct robust_list_head __user *head, size_t len);
41
42 The pointer 'head' points to a structure in the threads address space
43 consisting of three words. Each word is 32 bits on 32 bit arch's, or 64
44 bits on 64 bit arch's, and local byte order. Each thread should have
45 its own thread private 'head'.
46
47 If a thread is running in 32 bit compatibility mode on a 64 native arch
48 kernel, then it can actually have two such structures - one using 32 bit
49 words for 32 bit compatibility mode, and one using 64 bit words for 64
50 bit native mode. The kernel, if it is a 64 bit kernel supporting 32 bit
51 compatibility mode, will attempt to process both lists on each task
52 exit, if the corresponding sys_set_robust_list() call has been made to
53 setup that list.
54
55 The first word in the memory structure at 'head' contains a
56 pointer to a single linked list of 'lock entries', one per lock,
57 as described below. If the list is empty, the pointer will point
58 to itself, 'head'. The last 'lock entry' points back to the 'head'.
59
60 The second word, called 'offset', specifies the offset from the
61 address of the associated 'lock entry', plus or minus, of what will
62 be called the 'lock word', from that 'lock entry'. The 'lock word'
63 is always a 32 bit word, unlike the other words above. The 'lock
64 word' holds 2 flag bits in the upper 2 bits, and the thread id (TID)
65 of the thread holding the lock in the bottom 30 bits. See further
66 below for a description of the flag bits.
67
68 The third word, called 'list_op_pending', contains transient copy of
69 the address of the 'lock entry', during list insertion and removal,
70 and is needed to correctly resolve races should a thread exit while
71 in the middle of a locking or unlocking operation.
72
73 Each 'lock entry' on the single linked list starting at 'head' consists
74 of just a single word, pointing to the next 'lock entry', or back to
75 'head' if there are no more entries. In addition, nearby to each 'lock
76 entry', at an offset from the 'lock entry' specified by the 'offset'
77 word, is one 'lock word'.
78
79 The 'lock word' is always 32 bits, and is intended to be the same 32 bit
80 lock variable used by the futex mechanism, in conjunction with
81 robust_futexes. The kernel will only be able to wakeup the next thread
82 waiting for a lock on a threads exit if that next thread used the futex
83 mechanism to register the address of that 'lock word' with the kernel.
84
85 For each futex lock currently held by a thread, if it wants this
86 robust_futex support for exit cleanup of that lock, it should have one
87 'lock entry' on this list, with its associated 'lock word' at the
88 specified 'offset'. Should a thread die while holding any such locks,
89 the kernel will walk this list, mark any such locks with a bit
90 indicating their holder died, and wakeup the next thread waiting for
91 that lock using the futex mechanism.
92
93 When a thread has invoked the above system call to indicate it
94 anticipates using robust_futexes, the kernel stores the passed in 'head'
95 pointer for that task. The task may retrieve that value later on by
96 using the system call::
97
98 asmlinkage long
99 sys_get_robust_list(int pid, struct robust_list_head __user **head_ptr,
100 size_t __user *len_ptr);
101
102 It is anticipated that threads will use robust_futexes embedded in
103 larger, user level locking structures, one per lock. The kernel
104 robust_futex mechanism doesn't care what else is in that structure, so
105 long as the 'offset' to the 'lock word' is the same for all
106 robust_futexes used by that thread. The thread should link those locks
107 it currently holds using the 'lock entry' pointers. It may also have
108 other links between the locks, such as the reverse side of a double
109 linked list, but that doesn't matter to the kernel.
110
111 By keeping its locks linked this way, on a list starting with a 'head'
112 pointer known to the kernel, the kernel can provide to a thread the
113 essential service available for robust_futexes, which is to help clean
114 up locks held at the time of (a perhaps unexpectedly) exit.
115
116 Actual locking and unlocking, during normal operations, is handled
117 entirely by user level code in the contending threads, and by the
118 existing futex mechanism to wait for, and wakeup, locks. The kernels
119 only essential involvement in robust_futexes is to remember where the
120 list 'head' is, and to walk the list on thread exit, handling locks
121 still held by the departing thread, as described below.
122
123 There may exist thousands of futex lock structures in a threads shared
124 memory, on various data structures, at a given point in time. Only those
125 lock structures for locks currently held by that thread should be on
126 that thread's robust_futex linked lock list a given time.
127
128 A given futex lock structure in a user shared memory region may be held
129 at different times by any of the threads with access to that region. The
130 thread currently holding such a lock, if any, is marked with the threads
131 TID in the lower 30 bits of the 'lock word'.
132
133 When adding or removing a lock from its list of held locks, in order for
134 the kernel to correctly handle lock cleanup regardless of when the task
135 exits (perhaps it gets an unexpected signal 9 in the middle of
136 manipulating this list), the user code must observe the following
137 protocol on 'lock entry' insertion and removal:
138
139 On insertion:
140
141 1) set the 'list_op_pending' word to the address of the 'lock entry'
142 to be inserted,
143 2) acquire the futex lock,
144 3) add the lock entry, with its thread id (TID) in the bottom 30 bits
145 of the 'lock word', to the linked list starting at 'head', and
146 4) clear the 'list_op_pending' word.
147
148 On removal:
149
150 1) set the 'list_op_pending' word to the address of the 'lock entry'
151 to be removed,
152 2) remove the lock entry for this lock from the 'head' list,
153 3) release the futex lock, and
154 4) clear the 'lock_op_pending' word.
155
156 On exit, the kernel will consider the address stored in
157 'list_op_pending' and the address of each 'lock word' found by walking
158 the list starting at 'head'. For each such address, if the bottom 30
159 bits of the 'lock word' at offset 'offset' from that address equals the
160 exiting threads TID, then the kernel will do two things:
161
162 1) if bit 31 (0x80000000) is set in that word, then attempt a futex
163 wakeup on that address, which will waken the next thread that has
164 used to the futex mechanism to wait on that address, and
165 2) atomically set bit 30 (0x40000000) in the 'lock word'.
166
167 In the above, bit 31 was set by futex waiters on that lock to indicate
168 they were waiting, and bit 30 is set by the kernel to indicate that the
169 lock owner died holding the lock.
170
171 The kernel exit code will silently stop scanning the list further if at
172 any point:
173
174 1) the 'head' pointer or an subsequent linked list pointer
175 is not a valid address of a user space word
176 2) the calculated location of the 'lock word' (address plus
177 'offset') is not the valid address of a 32 bit user space
178 word
179 3) if the list contains more than 1 million (subject to
180 future kernel configuration changes) elements.
181
182 When the kernel sees a list entry whose 'lock word' doesn't have the
183 current threads TID in the lower 30 bits, it does nothing with that
184 entry, and goes on to the next entry.
185

3. 한국어 전문 번역

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

Task exit에서 보유 lock을 정리하는 ABI

1-34

Paul Jackson이 시작한 이 문서는 robust futex ABI를 설명한다. Robust futex는 일반 futex에 더해 task가 exit할 때 보유 중인 lock을 kernel이 정리하도록 돕는다.

Thread가 보유한 futex 정보는 userspace linked list에 둔다. Lock을 획득하고 해제할 때 system call 없이 효율적으로 갱신할 수 있다. 일반 futex에 비해 필요한 추가 kernel 개입은 thread마다 한 번 robust futex list의 시작 주소를 알려 주는 일과 thread exit 때 list의 held lock을 처리하는 일뿐이다.

일반 futex도 경쟁 없는 lock은 system call 없이 처리하고 경쟁이 있으면 kernel에 waiter list를 두는 fast userspace locking을 제공한다. sys_futex(2) option은 특정 futex에서 기다리고 다음 waiter를 깨우는 동작을 제공한다.

Robust futex가 동작하려면 glibc 같은 userspace library가 kernel이 기대하는 정확한 layout과 순서로 list element를 관리해야 한다. 잘못 등록된 lock은 exit 때 정리되지 않아 같은 lock을 기다리는 thread의 deadlock이나 다른 failure를 일으킬 수 있다.

sys_set_robust_list()와 32/64-bit list

36-53
asmlinkage long
sys_set_robust_list(struct robust_list_head __user *head, size_t len);

Robust futex를 사용할 가능성이 있는 thread는 먼저 이 system call을 호출한다. head는 thread address space 안의 세 word 구조체를 가리킨다. Word는 32-bit architecture에서 32 bit, 64-bit architecture에서 64 bit이고 native byte order를 사용한다. Thread마다 private head가 있어야 한다.

64-bit native kernel에서 32-bit compatibility mode로 실행되는 thread는 32-bit compat list와 64-bit native list를 각각 가질 수 있다. 64-bit kernel이 compat mode를 지원하고 각 list에 대해 sys_set_robust_list()가 호출되었다면 task exit 때 두 list를 모두 처리한다.

robust_list_head의 세 word

55-71
Word역할
첫 번째 word: listLock마다 하나인 lock entry의 single linked list를 가리킨다. 빈 list는 head 자신을 가리키고 마지막 entry도 head로 돌아온다.
두 번째 word: offsetLock entry 주소에서 32-bit lock word까지의 signed offset이다. Lock word 상위 2 bit는 flag, 하위 30 bit는 owner TID다.
세 번째 word: list_op_pendingList insert/remove 중 작업 대상 lock entry 주소의 임시 사본이다. Lock/unlock 도중 thread가 exit하는 race를 올바르게 해결하는 데 필요하다.

Lock entry와 lock word

73-91

Head에서 시작하는 single linked list의 각 lock entry는 다음 lock entry 또는 끝에서 head를 가리키는 한 word로 이루어진다. 각 entry의 offset 위치에는 하나의 lock word가 있다.

Lock word는 항상 32 bit이며 일반 futex mechanism에서 쓰는 lock variable과 같은 word다. Exit 때 kernel이 다음 waiter를 깨우려면 그 waiter가 futex mechanism을 사용해 lock word 주소를 kernel에 등록해 두었어야 한다.

Thread가 현재 보유한 futex lock 중 exit cleanup을 원하는 lock마다 list에 lock entry 하나를 둔다. Thread가 lock을 보유한 채 죽으면 kernel이 list를 순회해 owner가 죽었다는 bit를 lock word에 표시하고 futex mechanism으로 다음 waiter를 깨운다.

등록한 head 조회와 embedded lock

93-114
asmlinkage long
sys_get_robust_list(int pid, struct robust_list_head __user **head_ptr,
                    size_t __user *len_ptr);

Kernel은 sys_set_robust_list()로 받은 head pointer를 task에 저장하고 sys_get_robust_list()로 다시 조회할 수 있게 한다.

일반적으로 robust futex는 lock마다 하나인 더 큰 userspace locking structure 안에 embedded된다. 같은 thread가 사용하는 모든 robust futex에서 lock word의 offset만 같다면 kernel은 structure의 나머지 field에 관여하지 않는다. Kernel이 쓰는 single linked list 외에 userspace가 reverse link 같은 별도 link를 추가해도 무방하다.

Kernel이 아는 head에서 현재 held lock을 연결해 두면 예상하지 못한 exit가 일어날 때 kernel이 lock 정리를 도울 수 있다.

정상 동작 중 kernel의 최소 개입

116-131

정상적인 lock과 unlock은 경쟁 thread의 userspace code와 기존 futex wait/wakeup mechanism이 전부 처리한다. Robust futex에서 kernel의 필수 역할은 list head를 기억하고 thread exit 때 list를 순회해 아직 보유 중인 lock을 처리하는 것이다.

Thread가 공유 memory에 수천 개의 futex lock structure를 볼 수 있어도 특정 시점에 그 thread가 실제로 보유한 lock만 robust list에 있어야 한다. 공유 영역의 같은 lock은 시간에 따라 다른 thread가 보유할 수 있고 현재 owner TID는 lock word 하위 30 bit에 기록된다.

List insert와 remove protocol

133-155

Signal 9처럼 list 조작 중 예기치 않게 task가 exit하더라도 kernel이 정리할 수 있도록 userspace는 다음 순서를 지켜야 한다.

InsertRemove
1. list_op_pending에 삽입할 lock entry 주소를 기록한다.1. list_op_pending에 제거할 lock entry 주소를 기록한다.
2. futex lock을 획득한다.2. head list에서 lock entry를 제거한다.
3. lock word 하위 30 bit에 TID를 넣고 entry를 head list에 추가한다.3. futex lock을 해제한다.
4. list_op_pending을 clear한다.4. pending word를 clear한다. 원문은 이 줄에서 lock_op_pending이라고 표기하지만 앞에서 정의된 ABI field는 list_op_pending이다.

Exit scan과 lock word flag

156-169

Exit 때 kernel은 list_op_pending에 저장된 주소와 head list를 순회해 찾은 각 lock word 주소를 조사한다. 각 주소에서 offset만큼 떨어진 lock word의 하위 30 bit가 exiting thread의 TID와 같으면 두 작업을 수행한다.

  • Bit 31, 즉 0x80000000이 설정되어 있으면 해당 주소에 futex wakeup을 시도해 futex wait 중인 다음 thread를 깨운다.
  • Lock word의 bit 30, 즉 0x40000000을 atomic하게 설정한다.

Bit 31은 waiter가 자신이 기다리고 있음을 표시하는 bit이고, bit 30은 kernel이 lock owner가 lock을 보유한 채 죽었다고 표시하는 bit다.

Kernel이 list scan을 중단하는 조건

171-184
  • head pointer 또는 뒤따르는 linked-list pointer가 유효한 userspace word 주소가 아니다.
  • entry 주소에 offset을 적용해 계산한 lock word 위치가 유효한 32-bit userspace word 주소가 아니다.
  • List element가 100만 개를 넘는다. 이 제한은 향후 kernel configuration에 따라 바뀔 수 있다.

List entry의 lock word 하위 30 bit가 current exiting thread의 TID와 다르면 kernel은 그 entry를 변경하지 않고 다음 entry로 진행한다.