← Documents Documentation/arch/arm/vlocks.rst GitHub 원문 ↗

Linux 6.18.37 · Architecture

vlocks for Bare-Metal Mutual Exclusion

비일관 CPU가 단일 메모리 위치에 투표해 winner를 정하는 vlock 알고리즘, 공정성·확장성 한계와 ARM 최적화를 설명합니다.

Source pathDocumentation/arch/arm/vlocks.rst
Source versionLinux v6.18.37
TranslationDUJINLABS 전문 번역 + 해설

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

1. 요약·해설

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

요약과 해설

vlocks.rst:1-212

Vlock은 cache coherence와 spinlock을 사용할 수 없는 초기 bare-metal 단계에서 CPU 하나를 고르는 최소 상호 배제 방식입니다. 각 CPU가 참여 flag를 세우고 공통 `last_vote`에 ID를 기록한 뒤 모든 투표가 끝났을 때 남은 값으로 winner를 결정합니다.

마지막 참여자가 유리해 공정하지 않고 CPU 수가 많으면 계층화가 필요합니다. ARM 구현은 참여 flag를 word 단위로 묶어 읽고, cache가 꺼진 조건을 이용해 barrier를 줄이며, `.bss`의 0 초기화를 unlocked 상태로 사용합니다.

Vlock election
currently_voting[this_cpu] = 1last_vote 비어 있음 확인last_vote = this_cpu모든 voting flag가 0이 될 때까지 대기last_vote 비교

후보 등록, 투표 완료 대기, 최종 값 비교의 세 단계로 유일한 winner를 선택합니다.

2. 영어 원문 전체

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

원문 전체 펼치기
1 ======================================
2 vlocks for Bare-Metal Mutual Exclusion
3 ======================================
4
5 Voting Locks, or "vlocks" provide a simple low-level mutual exclusion
6 mechanism, with reasonable but minimal requirements on the memory
7 system.
8
9 These are intended to be used to coordinate critical activity among CPUs
10 which are otherwise non-coherent, in situations where the hardware
11 provides no other mechanism to support this and ordinary spinlocks
12 cannot be used.
13
14
15 vlocks make use of the atomicity provided by the memory system for
16 writes to a single memory location. To arbitrate, every CPU "votes for
17 itself", by storing a unique number to a common memory location. The
18 final value seen in that memory location when all the votes have been
19 cast identifies the winner.
20
21 In order to make sure that the election produces an unambiguous result
22 in finite time, a CPU will only enter the election in the first place if
23 no winner has been chosen and the election does not appear to have
24 started yet.
25
26
27 Algorithm
28 ---------
29
30 The easiest way to explain the vlocks algorithm is with some pseudo-code::
31
32
33 int currently_voting[NR_CPUS] = { 0, };
34 int last_vote = -1; /* no votes yet */
35
36 bool vlock_trylock(int this_cpu)
37 {
38 /* signal our desire to vote */
39 currently_voting[this_cpu] = 1;
40 if (last_vote != -1) {
41 /* someone already volunteered himself */
42 currently_voting[this_cpu] = 0;
43 return false; /* not ourself */
44 }
45
46 /* let's suggest ourself */
47 last_vote = this_cpu;
48 currently_voting[this_cpu] = 0;
49
50 /* then wait until everyone else is done voting */
51 for_each_cpu(i) {
52 while (currently_voting[i] != 0)
53 /* wait */;
54 }
55
56 /* result */
57 if (last_vote == this_cpu)
58 return true; /* we won */
59 return false;
60 }
61
62 bool vlock_unlock(void)
63 {
64 last_vote = -1;
65 }
66
67
68 The currently_voting[] array provides a way for the CPUs to determine
69 whether an election is in progress, and plays a role analogous to the
70 "entering" array in Lamport's bakery algorithm [1].
71
72 However, once the election has started, the underlying memory system
73 atomicity is used to pick the winner. This avoids the need for a static
74 priority rule to act as a tie-breaker, or any counters which could
75 overflow.
76
77 As long as the last_vote variable is globally visible to all CPUs, it
78 will contain only one value that won't change once every CPU has cleared
79 its currently_voting flag.
80
81
82 Features and limitations
83 ------------------------
84
85 * vlocks are not intended to be fair. In the contended case, it is the
86 _last_ CPU which attempts to get the lock which will be most likely
87 to win.
88
89 vlocks are therefore best suited to situations where it is necessary
90 to pick a unique winner, but it does not matter which CPU actually
91 wins.
92
93 * Like other similar mechanisms, vlocks will not scale well to a large
94 number of CPUs.
95
96 vlocks can be cascaded in a voting hierarchy to permit better scaling
97 if necessary, as in the following hypothetical example for 4096 CPUs::
98
99 /* first level: local election */
100 my_town = towns[(this_cpu >> 4) & 0xf];
101 I_won = vlock_trylock(my_town, this_cpu & 0xf);
102 if (I_won) {
103 /* we won the town election, let's go for the state */
104 my_state = states[(this_cpu >> 8) & 0xf];
105 I_won = vlock_lock(my_state, this_cpu & 0xf));
106 if (I_won) {
107 /* and so on */
108 I_won = vlock_lock(the_whole_country, this_cpu & 0xf];
109 if (I_won) {
110 /* ... */
111 }
112 vlock_unlock(the_whole_country);
113 }
114 vlock_unlock(my_state);
115 }
116 vlock_unlock(my_town);
117
118
119 ARM implementation
120 ------------------
121
122 The current ARM implementation [2] contains some optimisations beyond
123 the basic algorithm:
124
125 * By packing the members of the currently_voting array close together,
126 we can read the whole array in one transaction (providing the number
127 of CPUs potentially contending the lock is small enough). This
128 reduces the number of round-trips required to external memory.
129
130 In the ARM implementation, this means that we can use a single load
131 and comparison::
132
133 LDR Rt, [Rn]
134 CMP Rt, #0
135
136 ...in place of code equivalent to::
137
138 LDRB Rt, [Rn]
139 CMP Rt, #0
140 LDRBEQ Rt, [Rn, #1]
141 CMPEQ Rt, #0
142 LDRBEQ Rt, [Rn, #2]
143 CMPEQ Rt, #0
144 LDRBEQ Rt, [Rn, #3]
145 CMPEQ Rt, #0
146
147 This cuts down on the fast-path latency, as well as potentially
148 reducing bus contention in contended cases.
149
150 The optimisation relies on the fact that the ARM memory system
151 guarantees coherency between overlapping memory accesses of
152 different sizes, similarly to many other architectures. Note that
153 we do not care which element of currently_voting appears in which
154 bits of Rt, so there is no need to worry about endianness in this
155 optimisation.
156
157 If there are too many CPUs to read the currently_voting array in
158 one transaction then multiple transactions are still required. The
159 implementation uses a simple loop of word-sized loads for this
160 case. The number of transactions is still fewer than would be
161 required if bytes were loaded individually.
162
163
164 In principle, we could aggregate further by using LDRD or LDM, but
165 to keep the code simple this was not attempted in the initial
166 implementation.
167
168
169 * vlocks are currently only used to coordinate between CPUs which are
170 unable to enable their caches yet. This means that the
171 implementation removes many of the barriers which would be required
172 when executing the algorithm in cached memory.
173
174 packing of the currently_voting array does not work with cached
175 memory unless all CPUs contending the lock are cache-coherent, due
176 to cache writebacks from one CPU clobbering values written by other
177 CPUs. (Though if all the CPUs are cache-coherent, you should be
178 probably be using proper spinlocks instead anyway).
179
180
181 * The "no votes yet" value used for the last_vote variable is 0 (not
182 -1 as in the pseudocode). This allows statically-allocated vlocks
183 to be implicitly initialised to an unlocked state simply by putting
184 them in .bss.
185
186 An offset is added to each CPU's ID for the purpose of setting this
187 variable, so that no CPU uses the value 0 for its ID.
188
189
190 Colophon
191 --------
192
193 Originally created and documented by Dave Martin for Linaro Limited, for
194 use in ARM-based big.LITTLE platforms, with review and input gratefully
195 received from Nicolas Pitre and Achin Gupta. Thanks to Nicolas for
196 grabbing most of this text out of the relevant mail thread and writing
197 up the pseudocode.
198
199 Copyright (C) 2012-2013 Linaro Limited
200 Distributed under the terms of Version 2 of the GNU General Public
201 License, as defined in linux/COPYING.
202
203
204 References
205 ----------
206
207 [1] Lamport, L. "A New Solution of Dijkstra's Concurrent Programming
208 Problem", Communications of the ACM 17, 8 (August 1974), 453-455.
209
210 https://en.wikipedia.org/wiki/Lamport%27s_bakery_algorithm
211
212 [2] linux/arch/arm/common/vlock.S, www.kernel.org.
213

3. 한국어 전문 번역

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

Voting lock의 목적

1-26

Voting Locks, 즉 `vlocks`는 메모리 시스템에 합리적이지만 최소한의 요구만 두는 단순한 저수준 mutual exclusion mechanism입니다.

하드웨어가 다른 동기화 수단을 제공하지 않고 일반 spinlock을 사용할 수 없는 상황에서, 서로 cache-coherent하지 않은 CPU 사이의 critical activity를 조정하도록 설계했습니다.

Vlock은 한 메모리 위치에 대한 write atomicity를 사용합니다. 각 CPU가 공통 메모리 위치에 고유 번호를 기록해 자신에게 투표하며, 모든 vote가 끝난 뒤 그 위치에서 보이는 최종 값이 winner를 식별합니다.

Election이 유한 시간 안에 모호하지 않은 결과를 내도록 CPU는 아직 winner가 없고 election도 시작되지 않은 것으로 보일 때만 선거에 참여합니다.

기본 알고리즘

27-67

다음 pseudo-code는 `currently_voting[]`으로 각 CPU의 참여 상태를 알리고 `last_vote`에 최종 후보를 기록하는 `vlock_trylock()`과, 값을 초기 상태로 되돌리는 `vlock_unlock()`을 보여 줍니다.

int currently_voting[NR_CPUS] = { 0, };
int last_vote = -1; /* no votes yet */

bool vlock_trylock(int this_cpu)
{
        /* signal our desire to vote */
        currently_voting[this_cpu] = 1;
        if (last_vote != -1) {
                /* someone already volunteered himself */
                currently_voting[this_cpu] = 0;
                return false; /* not ourself */
        }

        /* let's suggest ourself */
        last_vote = this_cpu;
        currently_voting[this_cpu] = 0;

        /* then wait until everyone else is done voting */
        for_each_cpu(i) {
                while (currently_voting[i] != 0)
                        /* wait */;
        }

        /* result */
        if (last_vote == this_cpu)
                return true; /* we won */
        return false;
}

bool vlock_unlock(void)
{
        last_vote = -1;
}

선거가 수렴하는 이유

68-81

`currently_voting[]` 배열은 CPU가 election 진행 여부를 판단하게 하며 Lamport's bakery algorithm [1]의 `entering` 배열과 비슷한 역할을 합니다.

Election이 시작된 뒤에는 underlying memory system의 atomicity가 winner를 고릅니다. 따라서 tie-breaker용 static priority rule이나 overflow할 수 있는 counter가 필요하지 않습니다.

`last_vote`가 모든 CPU에 globally visible하면 모든 CPU가 `currently_voting` flag를 지운 시점부터 그 값은 더 바뀌지 않는 하나의 값만 갖습니다.

공정성·확장성 한계와 계층형 투표

82-118

Vlock은 fair하도록 설계하지 않았습니다. 경합 상황에서는 lock 획득을 마지막으로 시도한 CPU가 이길 가능성이 가장 높습니다. 따라서 어떤 CPU가 이기는지는 중요하지 않고 고유한 winner 하나를 뽑는 것이 중요한 상황에 적합합니다.

유사한 메커니즘처럼 CPU 수가 많아지면 확장성이 좋지 않습니다. 필요하면 `4096 CPUs`의 다음 가상 예처럼 town, state, country 단계의 voting hierarchy로 vlock을 cascade할 수 있습니다.

/* first level: local election */
my_town = towns[(this_cpu >> 4) & 0xf];
I_won = vlock_trylock(my_town, this_cpu & 0xf);
if (I_won) {
        /* we won the town election, let's go for the state */
        my_state = states[(this_cpu >> 8) & 0xf];
        I_won = vlock_lock(my_state, this_cpu & 0xf));
        if (I_won) {
                /* and so on */
                I_won = vlock_lock(the_whole_country, this_cpu & 0xf];
                if (I_won) {
                        /* ... */
                }
                vlock_unlock(the_whole_country);
        }
        vlock_unlock(my_state);
}
vlock_unlock(my_town);

ARM 구현의 배열 packing 최적화

119-168

현재 ARM 구현 [2]은 기본 알고리즘보다 몇 가지 최적화를 더합니다. `currently_voting` 배열 원소를 가깝게 packing하면 경합 가능한 CPU 수가 충분히 적을 때 배열 전체를 한 transaction으로 읽어 외부 메모리 왕복 횟수를 줄일 수 있습니다.

ARM 구현에서는 다음 단일 load와 comparison을 사용할 수 있습니다.

LDR        Rt, [Rn]
CMP        Rt, #0

이는 다음처럼 byte를 하나씩 확인하는 코드에 해당합니다.

LDRB        Rt, [Rn]
CMP        Rt, #0
LDRBEQ        Rt, [Rn, #1]
CMPEQ        Rt, #0
LDRBEQ        Rt, [Rn, #2]
CMPEQ        Rt, #0
LDRBEQ        Rt, [Rn, #3]
CMPEQ        Rt, #0

이 최적화는 fast-path latency를 줄이고 경합 시 bus contention도 줄일 수 있습니다.

ARM memory system이 크기가 다른 overlapping memory access 사이의 coherency를 보장한다는 사실에 의존합니다. `currently_voting`의 어느 원소가 `Rt`의 어느 bit에 들어가는지는 중요하지 않으므로 endianness도 문제가 되지 않습니다.

CPU가 너무 많아 한 transaction으로 배열을 읽을 수 없으면 word-sized load의 단순 loop를 사용합니다. 그래도 byte를 개별적으로 읽는 것보다 transaction 수가 적습니다. 원칙적으로 `LDRD`나 `LDM`으로 더 집계할 수 있지만 초기 구현에서는 코드 단순성을 위해 시도하지 않았습니다.

캐시 제약과 0 초기화

169-189

Vlock은 현재 cache를 아직 켤 수 없는 CPU 사이의 조정에만 사용합니다. 따라서 cached memory에서 알고리즘을 실행할 때 필요할 많은 barrier를 구현에서 제거합니다.

경합 CPU가 모두 cache-coherent하지 않으면 한 CPU의 cache writeback이 다른 CPU가 쓴 값을 덮을 수 있으므로 packed `currently_voting` 배열은 cached memory에서 동작하지 않습니다. 모든 CPU가 cache-coherent하다면 일반 spinlock을 사용하는 편이 낫습니다.

`last_vote`의 "no votes yet" 값은 pseudo-code의 -1이 아니라 0입니다. 그러면 정적으로 할당한 vlock을 `.bss`에 두는 것만으로 unlocked state로 암묵 초기화할 수 있습니다. 어떤 CPU도 ID 0을 쓰지 않도록 이 변수를 설정할 때 각 CPU ID에 offset을 더합니다.

작성 배경과 라이선스

190-203

Dave Martin이 Linaro Limited에서 ARM 기반 big.LITTLE 플랫폼에 사용하려고 처음 만들고 문서화했습니다. Nicolas Pitre와 Achin Gupta가 검토와 의견을 제공했으며, Nicolas Pitre는 관련 mail thread에서 본문의 대부분을 수집하고 pseudo-code를 작성했습니다.

Copyright (C) 2012-2013 Linaro Limited. `linux/COPYING`에 정의된 GNU General Public License Version 2 조건으로 배포합니다.

참고 문헌

204-212

[1] Lamport, L. "A New Solution of Dijkstra's Concurrent Programming Problem", Communications of the ACM 17, 8 (August 1974), 453-455.

[2] `linux/arch/arm/common/vlock.S`, www.kernel.org.