← Documents Documentation/locking/mutex-design.rst GitHub 원문 ↗

Linux 6.18.37 · Locking

Generic mutex subsystem 내부 구조

struct mutex의 owner encoding, fastpath·optimistic spinning·slowpath, 엄격한 owner 규칙과 object lifetime을 설명합니다.

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

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

1. 요약·해설

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

Kernel mutex의 의미

mutex-design.rst:9-22

커널에서 mutex는 상호 배제라는 일반 개념이 아니라 struct mutex로 구현된 특정 sleeping lock을 뜻합니다. 2006년에 binary semaphore를 대신할 목적으로 도입되었고, 더 단순한 interface와 명확한 owner semantics를 제공합니다.

경쟁이 생기면 task가 schedule되어 잠들 수 있으므로 mutex는 preemptible task context에서만 획득해야 합니다. hardirq, softirq, tasklet, timer callback처럼 sleep할 수 없는 context에서는 사용할 수 없습니다.

struct mutex와 owner field

mutex-design.rst:23-36

struct mutex는 include/linux/mutex.h에 선언되고 kernel/locking/mutex.c에서 구현됩니다. owner atomic field에는 현재 owner의 task_struct pointer가 들어가며 unlocked 상태에서는 NULL입니다. task_struct 정렬로 항상 0인 하위 3비트에는 waiter 존재 여부 같은 상태 bit를 함께 저장합니다.

  • owner: owner task pointer와 low-bit state
  • wait_lock: waiter list 조작과 slow path 직렬화
  • wait_list: sleep한 waiter 관리
  • osq: CONFIG_MUTEX_SPIN_ON_OWNER에서 optimistic spinner를 정렬하는 MCS 계열 queue

Fastpath, midpath, slowpath

mutex-design.rst:37-73
경로진입 조건동작
Fastpathowner가 0이고 state bit도 없음cmpxchg로 current를 owner에 원자적으로 기록
Midpathowner가 실행 중이고 need_resched가 없음owner가 곧 unlock할 가능성을 보고 optimistic spinning
Slowpath즉시 획득과 spinning이 모두 실패wait queue에 들어가 TASK_UNINTERRUPTIBLE로 sleep

Midpath의 spinner들은 MCS 계열 osq에 줄을 섭니다. 각 CPU가 자신의 local node를 보며 spin하므로 하나의 cache line을 계속 흔드는 test-and-set 방식보다 cacheline bouncing이 적습니다. Reschedule이 필요해진 spinner는 queue에서 빠져나와 즉시 slowpath로 갈 수 있습니다.

Mutex는 형식상 sleeping lock이지만 optimistic spinning 때문에 실제 성능 특성은 hybrid에 가깝습니다. Owner가 현재 CPU에서 실행 중이면 sleep과 wakeup 비용을 지불하는 것보다 짧게 기다리는 편이 유리한 workload가 많습니다.

Owner 규칙과 debug 검증

mutex-design.rst:75-103
  • 한 번에 하나의 task만 보유할 수 있다.
  • 오직 owner만 unlock할 수 있다.
  • 중복 unlock과 recursive lock은 허용되지 않는다.
  • 공식 API로 초기화하고 보유 중 재초기화하지 않는다.
  • Task는 mutex를 보유한 채 종료하면 안 된다.
  • 보유한 mutex storage를 해제하면 안 된다.
  • Hardware·software interrupt context에서 사용할 수 없다.

CONFIG_DEBUG_MUTEXES는 symbolic lock name, 획득 지점, owner, 시스템 전체 held-lock 목록을 기록하고 self recursion과 여러 task의 circular deadlock을 탐지합니다. Debug 구성을 통과하지 못하는 사용법은 release build에서 우연히 보이지 않을 뿐 올바른 코드가 아닙니다.

mutex_unlock() 이후의 object lifetime

mutex-design.rst:104-121

Sleeping lock은 자신이 들어 있는 memory object에 암묵적인 reference를 제공하지 않습니다. 더 중요한 점은 mutex_unlock()이 내부적으로 owner를 놓은 뒤에도 mutex 구조체에 접근할 수 있다는 것입니다. 다른 task가 lock을 획득했다는 사실만으로 이전 unlock caller가 구조체 사용을 완전히 끝냈다고 볼 수 없습니다.

mutex를 포함한 object를 해제하려면 모든 획득자뿐 아니라 진행 중인 mutex_unlock() 호출도 반환했다는 수명 보장이 필요합니다. Lock 보호와 object lifetime reference는 별개의 문제입니다.

주요 interface와 선택 기준

mutex-design.rst:123-170
DEFINE_MUTEX(name);
mutex_init(&object->lock);

mutex_lock(&object->lock);
mutex_lock_interruptible(&object->lock);
mutex_trylock(&object->lock);
mutex_unlock(&object->lock);

if (mutex_is_locked(&object->lock))
    /* 상태 관찰용이며 동기화 자체를 대신하지 않음 */;

mutex_lock()은 획득할 때까지 uninterruptible하게 기다립니다. mutex_lock_interruptible()은 signal로 중단될 수 있고 반환값을 반드시 처리해야 합니다. trylock은 기다리지 않지만 성공·실패 직후 상태가 경쟁으로 바뀔 수 있으므로 설계된 fallback이 있어야 합니다.

x86-64 예에서 struct mutex는 32바이트로 작은 lock은 아닙니다. 그래도 엄격한 semantics가 맞고 critical section이 sleep 가능한 task context라면 일반적으로 다른 primitive보다 mutex를 우선합니다.

2. 영어 원문 전체

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

원문 전체 펼치기
1 =======================
2 Generic Mutex Subsystem
3 =======================
4
5 started by Ingo Molnar <[email protected]>
6
7 updated by Davidlohr Bueso <[email protected]>
8
9 What are mutexes?
10 -----------------
11
12 In the Linux kernel, mutexes refer to a particular locking primitive
13 that enforces serialization on shared memory systems, and not only to
14 the generic term referring to 'mutual exclusion' found in academia
15 or similar theoretical text books. Mutexes are sleeping locks which
16 behave similarly to binary semaphores, and were introduced in 2006[1]
17 as an alternative to these. This new data structure provided a number
18 of advantages, including simpler interfaces, and at that time smaller
19 code (see Disadvantages).
20
21 [1] https://lwn.net/Articles/164802/
22
23 Implementation
24 --------------
25
26 Mutexes are represented by 'struct mutex', defined in include/linux/mutex.h
27 and implemented in kernel/locking/mutex.c. These locks use an atomic variable
28 (->owner) to keep track of the lock state during its lifetime. Field owner
29 actually contains `struct task_struct *` to the current lock owner and it is
30 therefore NULL if not currently owned. Since task_struct pointers are aligned
31 to at least L1_CACHE_BYTES, low bits (3) are used to store extra state (e.g.,
32 if waiter list is non-empty). In its most basic form it also includes a
33 wait-queue and a spinlock that serializes access to it. Furthermore,
34 CONFIG_MUTEX_SPIN_ON_OWNER=y systems use a spinner MCS lock (->osq), described
35 below in (ii).
36
37 When acquiring a mutex, there are three possible paths that can be
38 taken, depending on the state of the lock:
39
40 (i) fastpath: tries to atomically acquire the lock by cmpxchg()ing the owner with
41 the current task. This only works in the uncontended case (cmpxchg() checks
42 against 0UL, so all 3 state bits above have to be 0). If the lock is
43 contended it goes to the next possible path.
44
45 (ii) midpath: aka optimistic spinning, tries to spin for acquisition
46 while the lock owner is running and there are no other tasks ready
47 to run that have higher priority (need_resched). The rationale is
48 that if the lock owner is running, it is likely to release the lock
49 soon. The mutex spinners are queued up using MCS lock so that only
50 one spinner can compete for the mutex.
51
52 The MCS lock (proposed by Mellor-Crummey and Scott) is a simple spinlock
53 with the desirable properties of being fair and with each cpu trying
54 to acquire the lock spinning on a local variable. It avoids expensive
55 cacheline bouncing that common test-and-set spinlock implementations
56 incur. An MCS-like lock is specially tailored for optimistic spinning
57 for sleeping lock implementation. An important feature of the customized
58 MCS lock is that it has the extra property that spinners are able to exit
59 the MCS spinlock queue when they need to reschedule. This further helps
60 avoid situations where MCS spinners that need to reschedule would continue
61 waiting to spin on mutex owner, only to go directly to slowpath upon
62 obtaining the MCS lock.
63
64
65 (iii) slowpath: last resort, if the lock is still unable to be acquired,
66 the task is added to the wait-queue and sleeps until woken up by the
67 unlock path. Under normal circumstances it blocks as TASK_UNINTERRUPTIBLE.
68
69 While formally kernel mutexes are sleepable locks, it is path (ii) that
70 makes them more practically a hybrid type. By simply not interrupting a
71 task and busy-waiting for a few cycles instead of immediately sleeping,
72 the performance of this lock has been seen to significantly improve a
73 number of workloads. Note that this technique is also used for rw-semaphores.
74
75 Semantics
76 ---------
77
78 The mutex subsystem checks and enforces the following rules:
79
80 - Only one task can hold the mutex at a time.
81 - Only the owner can unlock the mutex.
82 - Multiple unlocks are not permitted.
83 - Recursive locking/unlocking is not permitted.
84 - A mutex must only be initialized via the API (see below).
85 - A task may not exit with a mutex held.
86 - Memory areas where held locks reside must not be freed.
87 - Held mutexes must not be reinitialized.
88 - Mutexes may not be used in hardware or software interrupt
89 contexts such as tasklets and timers.
90
91 These semantics are fully enforced when CONFIG DEBUG_MUTEXES is enabled.
92 In addition, the mutex debugging code also implements a number of other
93 features that make lock debugging easier and faster:
94
95 - Uses symbolic names of mutexes, whenever they are printed
96 in debug output.
97 - Point-of-acquire tracking, symbolic lookup of function names,
98 list of all locks held in the system, printout of them.
99 - Owner tracking.
100 - Detects self-recursing locks and prints out all relevant info.
101 - Detects multi-task circular deadlocks and prints out all affected
102 locks and tasks (and only those tasks).
103
104 Mutexes - and most other sleeping locks like rwsems - do not provide an
105 implicit reference for the memory they occupy, which reference is released
106 with mutex_unlock().
107
108 [ This is in contrast with spin_unlock() [or completion_done()], which
109 APIs can be used to guarantee that the memory is not touched by the
110 lock implementation after spin_unlock()/completion_done() releases
111 the lock. ]
112
113 mutex_unlock() may access the mutex structure even after it has internally
114 released the lock already - so it's not safe for another context to
115 acquire the mutex and assume that the mutex_unlock() context is not using
116 the structure anymore.
117
118 The mutex user must ensure that the mutex is not destroyed while a
119 release operation is still in progress - in other words, callers of
120 mutex_unlock() must ensure that the mutex stays alive until mutex_unlock()
121 has returned.
122
123 Interfaces
124 ----------
125 Statically define the mutex::
126
127 DEFINE_MUTEX(name);
128
129 Dynamically initialize the mutex::
130
131 mutex_init(mutex);
132
133 Acquire the mutex, uninterruptible::
134
135 void mutex_lock(struct mutex *lock);
136 void mutex_lock_nested(struct mutex *lock, unsigned int subclass);
137 int mutex_trylock(struct mutex *lock);
138
139 Acquire the mutex, interruptible::
140
141 int mutex_lock_interruptible_nested(struct mutex *lock,
142 unsigned int subclass);
143 int mutex_lock_interruptible(struct mutex *lock);
144
145 Acquire the mutex, interruptible, if dec to 0::
146
147 int atomic_dec_and_mutex_lock(atomic_t *cnt, struct mutex *lock);
148
149 Unlock the mutex::
150
151 void mutex_unlock(struct mutex *lock);
152
153 Test if the mutex is taken::
154
155 int mutex_is_locked(struct mutex *lock);
156
157 Disadvantages
158 -------------
159
160 Unlike its original design and purpose, 'struct mutex' is among the largest
161 locks in the kernel. E.g: on x86-64 it is 32 bytes, where 'struct semaphore'
162 is 24 bytes and rw_semaphore is 40 bytes. Larger structure sizes mean more CPU
163 cache and memory footprint.
164
165 When to use mutexes
166 -------------------
167
168 Unless the strict semantics of mutexes are unsuitable and/or the critical
169 region prevents the lock from being shared, always prefer them to any other
170 locking primitive.
171

3. 한국어 전문 번역

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

Kernel mutex의 의미

1-21

Ingo Molnar가 시작하고 Davidlohr Bueso가 갱신한 문서다. Linux kernel에서 mutex는 학술 문헌의 일반적인 mutual exclusion 개념만을 뜻하는 것이 아니라 shared-memory system의 실행을 직렬화하는 특정 locking primitive를 뜻한다.

Mutex는 binary semaphore와 비슷하게 동작하는 sleeping lock이다. 2006년에 binary semaphore의 대안으로 도입되었으며 더 단순한 interface와 당시 기준 더 작은 code 같은 장점이 있었다. 배경은 https://lwn.net/Articles/164802/ 에서 확인할 수 있다.

struct mutex와 owner field

23-35

Mutex는 include/linux/mutex.h에 정의된 struct mutex로 표현되고 kernel/locking/mutex.c에서 구현된다. Lifetime 동안 lock state를 추적하기 위해 atomic variable인 ->owner를 사용한다.

owner에는 현재 lock owner의 struct task_struct *가 들어 있으며 owner가 없으면 NULL이다. task_struct pointer는 최소 L1_CACHE_BYTES에 맞춰 align되므로 아래쪽 3 bit는 waiter list가 비어 있지 않은지 같은 추가 state를 저장하는 데 사용한다.

기본 구조에는 wait queue와 그 queue 접근을 직렬화하는 spinlock도 들어 있다. CONFIG_MUTEX_SPIN_ON_OWNER=y system에는 optimistic spinner를 queueing하는 MCS lock인 ->osq가 추가된다.

Acquire path 1: fast path

37-43

Fast path는 cmpxchg()로 owner를 current task로 atomic하게 바꿔 lock 획득을 시도한다. 경쟁이 없는 경우에만 성공한다. cmpxchg()는 owner word를 0UL과 비교하므로 아래쪽 state bit 세 개도 모두 0이어야 한다. 경쟁 상태라면 다음 path로 넘어간다.

Acquire path 2: optimistic spinning

45-62

Mid path는 optimistic spinning이라고도 한다. Lock owner가 현재 실행 중이고 need_resched가 가리키는 더 높은 priority의 runnable task가 없다면 lock 획득을 기다리며 spin한다. Owner가 실행 중이라면 곧 lock을 해제할 가능성이 높다는 판단에 기반한다.

Mutex spinner는 MCS lock queue에 들어가 한 번에 하나의 spinner만 mutex를 두고 경쟁한다. Mellor-Crummey와 Scott이 제안한 MCS lock은 공정하고 각 CPU가 local variable에서 spin하므로 일반 test-and-set spinlock의 비싼 cacheline bouncing을 피한다.

Sleeping lock의 optimistic spinning에 맞춘 MCS-like lock은 reschedule이 필요해진 spinner가 MCS queue에서 빠져나갈 수 있다는 추가 성질을 갖는다. Reschedule이 필요한 spinner가 owner를 계속 기다린 뒤 MCS lock을 얻자마자 slow path로 가는 낭비를 줄인다.

Acquire path 3: sleep

65-73

마지막 수단인 slow path에서도 lock을 얻지 못하면 task를 wait queue에 넣고 unlock path가 깨울 때까지 sleep한다. 보통 TASK_UNINTERRUPTIBLE 상태로 block된다.

형식상 kernel mutex는 sleepable lock이지만 optimistic spinning 때문에 실제로는 hybrid lock에 가깝다. 즉시 sleep해 task를 전환하는 대신 짧은 시간 busy-wait하면 여러 workload에서 성능이 크게 좋아진다. rw-semaphore도 같은 기법을 사용한다.

Mutex subsystem이 강제하는 규칙

75-103
  • 한 번에 하나의 task만 mutex를 보유할 수 있다.
  • Owner만 mutex를 unlock할 수 있다.
  • 여러 번 unlock할 수 없다.
  • Recursive locking과 recursive unlocking은 허용하지 않는다.
  • Mutex는 API를 통해서만 초기화한다.
  • Task는 mutex를 보유한 채 exit해서는 안 된다.
  • 보유 중인 lock이 놓인 memory 영역을 free해서는 안 된다.
  • 보유 중인 mutex를 다시 초기화해서는 안 된다.
  • Tasklet, timer 같은 hardware 또는 software interrupt context에서 mutex를 사용할 수 없다.

CONFIG_DEBUG_MUTEXES를 enable하면 이 semantic을 완전히 검사하고 강제한다. Debug code는 출력에 mutex symbol name을 사용하고, acquire 지점과 function symbol을 추적하며, system의 모든 held lock을 나열하고 owner를 추적한다. Self-recursion과 여러 task가 만드는 circular deadlock도 찾아 관련 lock과 task만 출력한다.

mutex_unlock()과 object lifetime

104-121

Mutex와 rwsem 같은 대부분의 sleeping lock은 자신이 차지하는 memory에 대한 암묵적 reference를 제공하지 않는다. 즉 mutex_unlock()과 함께 해제되는 object reference가 자동으로 생기지 않는다.

이는 spin_unlock()이나 completion_done()이 돌아간 뒤 lock 구현이 해당 memory를 더 이상 만지지 않는다고 보장할 수 있는 것과 다르다.

mutex_unlock()은 내부적으로 lock을 release한 뒤에도 mutex structure에 접근할 수 있다. 따라서 다른 context가 mutex를 획득했다는 사실만으로 이전 mutex_unlock() context가 structure 사용을 끝냈다고 가정해서는 안 된다.

Mutex 사용자는 release operation이 진행 중인 동안 mutex가 파괴되지 않도록 보장해야 한다. 구체적으로 mutex_unlock() caller는 function이 반환할 때까지 mutex object가 살아 있도록 해야 한다.

Mutex API

123-155
/* static definition */
DEFINE_MUTEX(name);

/* dynamic initialization */
mutex_init(mutex);

/* uninterruptible acquisition */
void mutex_lock(struct mutex *lock);
void mutex_lock_nested(struct mutex *lock, unsigned int subclass);
int  mutex_trylock(struct mutex *lock);

/* interruptible acquisition */
int mutex_lock_interruptible_nested(struct mutex *lock,
                                    unsigned int subclass);
int mutex_lock_interruptible(struct mutex *lock);

/* acquire when decrement reaches zero */
int atomic_dec_and_mutex_lock(atomic_t *cnt, struct mutex *lock);

/* release and state query */
void mutex_unlock(struct mutex *lock);
int mutex_is_locked(struct mutex *lock);

크기와 사용 기준

157-170

초기 설계 목적과 달리 struct mutex는 kernel에서 큰 lock 구조체에 속한다. 예를 들어 x86-64에서 struct mutex는 32 byte, struct semaphore는 24 byte, rw_semaphore는 40 byte다. 구조체가 클수록 CPU cache와 memory footprint를 더 많이 사용한다.

엄격한 mutex semantic이 사용 목적에 맞지 않거나 critical region의 성질 때문에 lock을 공유할 수 없는 경우가 아니라면 다른 locking primitive보다 mutex를 우선해서 사용한다.