Zircon のスピンロック実装(aarch64編)
Zircon のスピンロック実装(x86-64 編) の続編です。
- 対象のソースコード
zircon/kernel/arch/arm64/spinlock.cc
事前知識
関連する aarch64 命令
SEVL
Send Event Local は、マルチプロセッサシステムの他の PE にイベントを通知することなく、ローカルにイベントを通知するためのヒント命令です。WFE 命令で始まる待機ループを起動することができます。
— C6.2.238 SEVL, Arm® Architecture Reference Manual Armv8, for Armv8-A architecture profile
SEVL
命令は、WFE
命令の実行前に必要です。
詳細は、こちらを参照。
WFE
Wait For Event は、PE が低電力状態になり、ウェイクアップイベントが発生するまでその状態を維持できることを示すヒント命令です。ウェイクアップイベントには、マルチプロセッサシステム内の任意の PE で SEV 命令を実行した結果として通知されるイベントが含まれます。
— C6.2.344 WFE, Arm® Architecture Reference Manual Armv8, for Armv8-A architecture profile
ロック取得を待つ間、単純にループを回し続けると消費電力が高くなります。
WFE
命令は、ある条件が満たされるまで低電力状態で待つことにより、消費電力を抑えることができます。
スピンロックでの使用例では、すでに他 CPU がロックを取得しているときは低電力状態で待ち、ロックの共有変数の値が書き換わる(ロックが解放される)と起床します。
詳細は、こちらを参照。
LDR
Load Register(register)は、ベースレジスタ値とオフセットレジスタ値からアドレスを計算し、メモリからワードをロードし、レジスタに書き込みます。オフセットレジスタの値は、オプションでシフトや拡張が可能です。
— C6.2.133 LDR (register), Arm® Architecture Reference Manual Armv8, for Armv8-A architecture profile
LDR
命令は、メモリから値を読み込み、レジスタに格納します。
LDAXR
Load-Acquire Exclusive Register は、ベース・レジスタ値からアドレスを導き出し、32 ビット・ワードまたは 64 ビット・ダブルワードをメモリからロードし、レジスタに書き込みます。メモリアクセスはアトミックに行われます。PE は、アクセスされる物理アドレスを排他的アクセスとしてマークします。この排他的アクセスマークは Store Exclusive 命令でチェックされます。B2-166 ページの「同期とセマフォ」を参照してください。また、この命令は「B2-139 ページの Load-Acquire、Load-AcquirePC、および Store-Release」で説明されているように、メモリ順序のセマンティクス を持っています。メモリアクセスについては、「C1-187 ページの Load/Store アドレッシングモード」を参照してください。
— C6.2.114 LDAXR, Arm® Architecture Reference Manual Armv8, for Armv8-A architecture profile
LDR
に、Acquire
、Exclusive
が追加された命令です。
-
Acquire
LDAXR
命令に続く命令は、必ずこの命令が終了した後に実行されることを保証する。
詳細は、こちら -
Exclusive
STXR
命令と組み合わせて、排他的なメモリアクセスを実装できる。
LDXR
命令で当該メモリアドレスを(global exclusives monitor に)アイコンする。
他 CPU により当該メモリアドレスの値が変更されるとアイコンがクリアされる
詳細は、こちらを参照。
STR
Store Register(register)は、ベースレジスタの値とオフセットレジスタの値からアドレスを計算し、計算したアドレスに 32 ビットのワードまたは 64 ビットのダブルワードをレジスタから格納します。
— C6.2.275 STR (register), Arm® Architecture Reference Manual Armv8, for Armv8-A architecture profile
STR
命令は、レジスタの値をメモリに書き込みます。
他 CPU がWFE
命令で当該メモリアドレスの値変更を待っているとき、STR
命令によりその CPU は起床されます。
STXR
Store Exclusive Register は、PE がメモリアドレスへの排他的アクセス権を持っている場合、レジスタから 32 ビットワードまたは 64 ビットダブルワードをメモリにストアし、ストアが成功した場合は 0、ストアが実行されなかった場合は 1 のステータス値を返します。B2-166 ページの「同期とセマフォ」を参照してください。メモリアクセスについては、「ロード/ストアのアドレッシングモード(C1-187 ページ)」を参照してください。
— C6.2.302 STXR, Arm® Architecture Reference Manual Armv8, for Armv8-A architecture profile
STR
に、Exclusive
が追加された命令です。
-
Exclusive
LDAXR
命令(LDXR
命令)と組み合わせて、排他的なメモリアクセスを実装できる。-
LDAXR
命令による当該メモリアドレスへのアイコンが残っていた場合、LDAXR - STXR
命令間で他 CPU が当該メモリアドレスの値を書き換えなかったことが保証される。
このとき、自 CPU により当該メモリアドレスの値を書き込み、STXR
命令は成功する(ステータス値 0 を返す)。 - アイコンがクリアされていた場合、他 CPU が当該メモリアドレスに書き込みを行ったので、自 CPU では書き込みを行わず
STXR
命令は失敗する(ステータス値 1 を返す)。 -
STXR
命令が失敗したとき、再度LDAXR
命令からやり直し、STXR
命令が成功するまで繰り返すことで、排他的なメモリアクセス(読み込みから書き込みの間に自 CPU のみがメモリを変更できる)を実現できる。
-
詳細は、こちらを参照。
STLR
Store-Release Register は、レジスタから 32 ビットワードまたは 64 ビットダブルワードをメモリ位置に格納します。この命令は、「B2-139 ページの Load-Acquire、Load-AcquirePC、Store-Release」で説明したメモリ順序セマンティクスも持っています。メモリアクセスについては、「C1-187 ページの Load/Store アドレッシングモード」を参照してください。
— C6.2.262 STLR, Arm® Architecture Reference Manual Armv8, for Armv8-A architecture profile
STR
に、reLease
が追加された命令です。
-
reLease
STLR
命令より以前の命令は、この命令の実行前に必ずすべて実行されることを保証する
詳細は、こちら
Zircon のスピンロック関数
-
arch_spin_lock()
ロックを取得するまでループする関数 -
arch_spin_trylock()
一度だけロック取得を試みる関数 -
arch_spin_unlock()
取得したロックを解放する関数
仕様:
- 共有変数は
lock->value
- 共有変数が 0 の時、誰もロックしていない
- 共有変数が 0 以外の時、誰かがロックしている
- 共有変数にはロックを取得した CPU の
CPU番号+1
がセットされる。これで、誰がロックを取得しているかがわかる
(CPU 番号は 0 から始まるため、誰もロックしていない状態と区別するために、1 を足す)
- 共有変数にはロックを取得した CPU の
arch_spin_lock()
1: void arch_spin_lock(arch_spin_lock_t* lock) TA_NO_THREAD_SAFETY_ANALYSIS {
2: unsigned long val = arch_curr_cpu_num() + 1;
3: uint64_t temp;
4:
5: __asm__ volatile(
6: "sevl;"
7: "1: wfe;"
8: "2: ldaxr %[temp], [%[lock]];"
9: "cbnz %[temp], 1b;"
10: "stxr %w[temp], %[val], [%[lock]];"
11: "cbnz %w[temp], 2b;"
12: : [temp] "=&r"(temp)
13: : [lock] "r"(&lock->value), [val] "r"(val)
14: : "cc", "memory");
15: WRITE_PERCPU_FIELD32(num_spinlocks, READ_PERCPU_FIELD32(num_spinlocks) + 1);
16: }
ロックを取得するまでループする関数です。
-
引数
arch_spin_lock_t *lock
zircon/kernel/arch/arm64/include/arch/spinlock.htypedef struct TA_CAP("mutex") arch_spin_lock { unsigned long value; } arch_spin_lock_t;
-
lock->value
が共有変数 -
TA_CAP("mutex")
の説明はこちら
-
1: void arch_spin_lock(arch_spin_lock_t *lock) TA_NO_THREAD_SAFETY_ANALYSIS {
-
TA_NO_THREAD_SAFETY_ANALYSIS
の説明はこちら
2: unsigned long val = arch_curr_cpu_num() + 1;
-
val
はCPU 番号(0..)+ 1
。
ロック取得成功時に、この値を共有変数lock->value
にセットする -
arch_curr_cpu_num()
は、実行中 CPU の固有情報struct arm64_percpu
のcpu_num
メンバー(CPU 番号を表す)を返すzircon/kernel/arch/arm64/include/arch/arm64/mp.hstatic inline cpu_num_t arch_curr_cpu_num() { return arm64_read_percpu_u32(offsetof(struct arm64_percpu, cpu_num)); }
-
offsetof(struct arm64_percpu, cpu_num) は、構造体
arm64_percpu
のメンバーcpu_num
のオフセットを返す。cpu_num
は構造体の先頭のメンバーなので、offsetof(struct arm64_percpu, cpu_num)
は 0 を返す -
cpu_num_t
はuint32_t
zircon/kernel/arch/arm64/include/arch/arm64/mp.hstatic inline uint32_t arm64_read_percpu_u32(size_t offset) { uint32_t val; // mark as volatile to force a read of the field to make sure // the compiler always emits a read when asked and does not cache // a copy between __asm__ volatile("ldr %w[val], [x20, %[offset]]" : [val] "=r"(val) : [offset] "Ir"(offset)); return val; }
マークを volatile にして、フィールドの読み取りを強制することで、コンパイラが要求されたときに常に読み取りを行い、キャッシュを間に挟まないようにしています。
実行中 CPU 固有の
arm64_percpu
構造体のオフセットoffset
バイトから 4 バイト読み込む。
__asm__
文についてはこちら。-
"ldr %w[val], [x20, %[offset]]"
x20
レジスタには、CPU ごとに確保されたarm64_percpu
構造体へのアドレスが格納されている。詳細はこちら。
%w[val]
レジスタ(変数val
に割り当てた 32 ビットレジスタw0..w30
)に、x20 レジスタ値 + %[offset] 即値
のアドレス(arm64_percpu
構造体のオフセットoffset
バイト)から読み込んだ値を格納する -
: [val] "=r"(val)
C 言語部分の変数
val
を、アセンブラ部分で[val]
として参照し、値を変更する。
変数val
はレジスタ(32 ビットレジスタw0..w30
)上に確保する -
: [offset] "Ir"(offset)
C 言語部分の変数
offset
を、アセンブラ部分で[offset]
として参照し、値を取得する
-
3: uint64_t temp;
一時レジスタとして使用する変数です。
5: __asm__ volatile(
6: "sevl;"
7: "1: wfe;"
8: "2: ldaxr %[temp], [%[lock]];"
9: "cbnz %[temp], 1b;"
10: "stxr %w[temp], %[val], [%[lock]];"
11: "cbnz %w[temp], 2b;"
12: : [temp] "=&r"(temp)
13: : [lock] "r"(&lock->value), [val] "r"(val)
14: : "cc", "memory");
動作の詳細は、後述のアセンブラパートで紹介します。
__asm__
文についてはこちら。
-
最適化抑制
マルチスレッドのコードでは、一見(当該コードだけを見ると)無駄に見えるが実は必要な処理が存在する。
最適化によりこれらが削除されたり順序を入れ替えられたりしないように、抑制する必要がある。-
volatile
でコンパイラによる最適化を抑制 -
ldaxr
のAcquire
により、CPU によるアウトオブオーダーを抑制
-
-
1:
、2:
はローカルラベル。
1b
、2b
で、当該命令より前に定義した1:
、2:
を参照する -
%[temp]
は 64 ビットレジスタ -
[%[lock]]
は 64 ビットレジスタに格納されているアドレスの参照先 -
%w[temp]
は 32 ビットレジスタ -
%[val]
は 64 ビットレジスタ -
: [temp] "=&r"(temp)
は出力オペランドC 言語部分の変数
temp
を、アセンブラ内で%[temp]
、%w[temp]
シンボルとして扱い値を変更する。
変数temp
はレジスタ上に確保される -
: [lock] "r"(&lock->value), [val] "r"(val)
は入力オペランド- C 言語部分の構造体メンバー
lock->value
のアドレスを、アセンブラ内で%[lock]
として扱う。アドレスはレジスタ上に格納される - C 言語部分の変数
val
の値を、アセンブラ内で%[val]
として扱う。値はレジスタ上に格納される
- C 言語部分の構造体メンバー
-
: "cc", "memory"
。cc
とmemory
が変更されることをコンパイラに伝える。
cc
はフラグレジスタが変更されることを表す。しかし、フラグレジスタを変更する命令はアセンブラ中に含まれていない(はず)。
memory
は&lock->value
アドレスの参照先へアクセスすることを表す
15: WRITE_PERCPU_FIELD32(num_spinlocks, READ_PERCPU_FIELD32(num_spinlocks) + 1);
実行中 CPU 固有の arm64_percpu
構造体の num_spinlocks
メンバーをインクリメントします。
-
WRITE_PERCPU_FIELD32()
zircon/kernel/arch/arm64/include/arch/arm64/mp.h#define WRITE_PERCPU_FIELD32(field, value) \ arm64_write_percpu_u32(offsetof(struct arm64_percpu, field), (value))
-
arm64_percpu
構造体のfield
メンバーに(uint32_t)value
を書き込む -
offsetof(struct arm64_percpu, field)
はarm64_percpu
構造体のfield
メンバーのオフセットを返す
zircon/kernel/arch/arm64/include/arch/arm64/mp.hstatic inline void arm64_write_percpu_u32(size_t offset, uint32_t val) { __asm__("str %w[val], [x20, %[offset]]" ::[val] "r"(val), [offset] "Ir"(offset) : "memory"); }
__asm__
文についてはこちら。-
str %w[val], [x20, %[offset]]
C 言語で無理やり書くと
*(uint32_t*)((char*)arm64_percpu + offset) = val
。
arm64_percpu
は CPU ごとの構造体のアドレスとする -
:
で、出力オペランドは空 -
:[val] "r"(val), [offset] "Ir"(offset)
は入力オペランド-
: [val] "r"(val)
C 言語部分の変数
val
を、アセンブラ部分で[val]
として参照し、値を取得する。
変数val
はレジスタ(32 ビットレジスタw0..w30
)上に確保する -
: [offset] "Ir"(offset)
C 言語部分の変数
offset
を、アセンブラ部分で[offset]
として参照し、値を取得する。
値はアセンブラ内で即値として扱う
-
-
: "memory"
メモリ(アドレス
x20 + offset
)を書き換えることをコンパイラに指示
-
-
READ_PERCPU_FIELD32()
zircon/kernel/arch/arm64/include/arch/arm64/mp.h#define READ_PERCPU_FIELD32(field) arm64_read_percpu_u32(offsetof(struct arm64_percpu, field))
-
arm64_percpu
構造体の(uint32_t)field
メンバーを読み込む -
arm64_read_percpu_u32()
は前述の通り
-
アセンブラ
<arch_spin_lock(arch_spin_lock*)>:
1: ldr w8, [x20]
2: add w8, w8, #1 // =1
3: sevl
4: wfe // ffffffff0017d524
5: ldaxr x9, [x0] // ffffffff0017d528
6: cbnz x9, 0xffffffff0017d524 <arch_spin_lock(arch_spin_lock*)+0xc>
7: stxr w9, x8, [x0]
8: cbnz w9, 0xffffffff0017d528 <arch_spin_lock(arch_spin_lock*)+0x10>
9: ldr w8, [x20, #8]
10: add w8, w8, #1 // =1
11: str w8, [x20, #8]
12: ret
図はこちら。
1: ldr w8, [x20]
arch_curr_cpu_num()
に相当します。
x20
には arm64_percpu
構造体のアドレスが格納されています。
また、arm64_percpu
構造体の cpu_num
メンバーは構造体の先頭メンバーなので、arm64_percpu
構造体のアドレス = cpu_num
メンバーのアドレス、となります。
よって、この命令は cpu_num
の値を w8
レジスタにロードすることを表します。
2: add w8, w8, #1 // =1
cpu_num
をインクリメントし、w8
レジスタに格納します。
3: sevl
ローカル CPU のみ、イベントレジスタを 1 にセットします。
1 回目の wfe
を無条件で継続させるために必要です。
4: wfe // ffffffff0017d524
- イベントレジスタが 1 の場合
イベントレジスタを 0 にクリアし、処理を継続する - イベントレジスタが 0 の場合
省電力状態で待機する。別の CPU(ARM 用語では PE/Processing Element)がstxr
でlock->value
の書き換えに成功すると、起床する
省電力対応という意味で、x86-64
の pause
に対応。
5: ldaxr x9, [x0] // ffffffff0017d528
x0
レジスタは arch_spin_lock()
の第一引数が格納されます。
アドレス &lock->value
から値をロードし x9
レジスタに格納します。
これ以降のメモリアクセス命令は、アウトオブオーダー実行でこの命令より前に移動しません(Load-Acquire
)。
lock->value
の物理アドレスを global exclusives monitor
で排他的アクセスとアイコンします。
6: cbnz x9, 0xffffffff0017d524 <arch_spin_lock(arch_spin_lock*)+0xc>
x9
レジスタ(lock->value
)が 0 以外の時(別の CPU がロックを取得しているとき)、4 行目に戻ります。
7: stxr w9, x8, [x0]
共有変数 lock->value
が 0 のとき、x8
レジスタ(変数 val
、cpu_num+1
)を lock->value
に書き込みます。
ldaxr
から stxr
の間に、他 CPU が lock->value
に書き込んでいない場合、書き込みに成功し、w9
レジスタを 0 にします。
他 CPU が書き込んでいた場合、書き込みに失敗し、w9
レジスタを 1 にします。
書き込み(store)に成功
したとき、wfe
で待っている他の CPU コアを起床させます。
8: cbnz w9, 0xffffffff0017d528 <arch_spin_lock(arch_spin_lock*)+0x10>
w9
レジスタが 0 以外の場合(stxr
に失敗した場合)、5 行目に戻ります。
9: ldr w8, [x20, #8]
10: add w8, w8, #1 // =1
11: str w8, [x20, #8]
arm64_percpu
構造体のオフセット 8 バイトにあるメンバー num_spinlocks
を w8
レジスタに読み込みます。
w8
レジスタの値をインクリメント。
num_spinlocks
へ w8
レジスタの値を書き込みます。
percpu
構造体のデータは CPU ごとに存在するので、(共有変数 lock->value
とは異なり)アクセスに排他制御は必要ありません。
12: ret
リンクレジスタ(x30
)が指すアドレスに無条件に分岐します。
つまり、呼び出し元の関数に戻ります。
arch_spin_trylock()
1: bool arch_spin_trylock(arch_spin_lock_t* lock) TA_NO_THREAD_SAFETY_ANALYSIS {
2: unsigned long val = arch_curr_cpu_num() + 1;
3: uint64_t out;
4:
5: __asm__ volatile(
6: "ldaxr %[out], [%[lock]];"
7: "cbnz %[out], 1f;"
8: "stxr %w[out], %[val], [%[lock]];"
9: "1:"
10: : [out] "=&r"(out)
11: : [lock] "r"(&lock->value), [val] "r"(val)
12: : "cc", "memory");
13:
14: if (out == 0) {
15: WRITE_PERCPU_FIELD32(num_spinlocks, READ_PERCPU_FIELD32(num_spinlocks) + 1);
16: }
17: return out;
18: }
一度だけロック取得を試みる関数です。
-
引数
arch_spin_lock_t *lock
こちらを参照 -
戻り値
-
false
ロック取得に成功。
一般的に、ロック取得時はtrue
を返すのが自然な気がしますが、なぜfalse
を返すのか
-
true
ロック取得に失敗
-
1: bool arch_spin_trylock(arch_spin_lock_t *lock) TA_NO_THREAD_SAFETY_ANALYSIS {
-
TA_NO_THREAD_SAFETY_ANALYSIS
の説明はこちら
2: unsigned long val = arch_curr_cpu_num() + 1;
-
val
はCPU 番号(0..)+ 1
。
ロック取得成功時に、この値を共有変数lock->value
にセットする
3: uint64_t out;
5: __asm__ volatile(
6: "ldaxr %[out], [%[lock]];"
7: "cbnz %[out], 1f;"
8: "stxr %w[out], %[val], [%[lock]];"
9: "1:"
10: : [out] "=&r"(out)
11: : [lock] "r"(&lock->value), [val] "r"(val)
12: : "cc", "memory");
動作の詳細は、後述のアセンブラパートで紹介します。
__asm__
文についてはこちら。
-
1:
はローカルラベル。
1f
で、当該命令より後に定義した1:
を参照します -
%[out]
は 64 ビットレジスタ -
[%[lock]]
は 64 ビットレジスタに格納されているアドレスの参照先 -
%w[out]
は 32 ビットレジスタ -
%[val]
は 64 ビットレジスタ -
: [out] "=&r"(out)
は出力オペランドC 言語部分の変数
out
を、アセンブラ内で%[out]
、%w[out]
シンボルとして扱い値を変更する。
変数out
はレジスタ上に確保される -
: [lock] "r"(&lock->value), [val] "r"(val)
-
: "cc", "memory"
。cc
とmemory
が変更されることをコンパイラに伝える。
cc
はフラグレジスタが変更されることを表す。しかし、フラグレジスタを変更する命令はアセンブラ中に含まれていない(はず)。
memory
は&lock->value
アドレスの参照先へアクセスすることを表す
14: if (out == 0) {
15: WRITE_PERCPU_FIELD32(num_spinlocks, READ_PERCPU_FIELD32(num_spinlocks) + 1);
16: }
out
は stxr
の結果が入っています。
stxr
成功時(ロック取得成功時)、out
は 0 となります。
WRITE_PERCPU_FIELD32()
については、arch_spin_lock()
を参照。
17: return out;
out
、すなわち stxr
の結果が入っています。
- ロック成功時、0(
false
にキャスト)を返す - ロック失敗時、1(
true
にキャスト)を返す
アセンブラ
<arch_spin_trylock(arch_spin_lock*)>:
1: ldr w8, [x20]
2: add w9, w8, #1 // =1
3: ldaxr x8, [x0]
4: cbnz x8, 0xffffffff0017d55c <arch_spin_trylock(arch_spin_lock*)+0x14>
5: stxr w8, x9, [x0]
6: cbnz x8, 0xffffffff0017d56c <arch_spin_trylock(arch_spin_lock*)+0x24> // ffffffff0017d55c
7: ldr w9, [x20, #8]
8: add w9, w9, #1 // =1
9: str w9, [x20, #8]
10: cmp x8, #0 // =0 // ffffffff0017d56c
12: cset w0, ne
13: ret
1: ldr w8, [x20]
2: add w9, w8, #1 // =1
unsigned long val = arch_curr_cpu_num() + 1;
に相当します。
詳細は、arch_spin_lock()
のアセンブラの項を参照。
3: ldaxr x8, [x0]
共有変数 lock->value
を x8
レジスタに読み込みます。
詳細は、arch_spin_lock()
のアセンブラの項を参照。
4: cbnz x8, 0xffffffff0017d55c <arch_spin_trylock(arch_spin_lock*)+0x14>
共有変数 lock->value
が 0 でないとき、6 行目に分岐します。
5: stxr w8, x9, [x0]
共有変数 lock->value
が 0 のとき、x9
レジスタ(変数 val
)を lock->value
に書き込みます。
ldaxr
から stxr
の間に、他 CPU が lock->value
に書き込んでいない場合、書き込みに成功し、w8
レジスタを 0 にします。
他 CPU が書き込んでいた場合、書き込みに失敗し、w8
レジスタを 1 にします。
6: cbnz x8, 0xffffffff0017d56c <arch_spin_trylock(arch_spin_lock*)+0x24> // ffffffff0017d55c
stxr
に失敗した場合(x8
レジスタが 1)、10 行目に分岐します。
7: ldr w9, [x20, #8]
8: add w9, w9, #1 // =1
9: str w9, [x20, #8]
stxr
に成功した場合、arm64_percpu
構造体のnum_spinlocks
メンバーをインクリメントします。
詳細は、arch_spin_lock()
のアセンブラの項を参照。
10: cmp x8, #0 // =0 // ffffffff0017d56c
12: cset w0, ne
-
cmp x8, #0
(x8 と 0 を比較)-
x8
レジスタが 0 のとき、NZCV レジスタの Z フラグが立つ -
x8
レジスタが 1 のとき、NZCV レジスタの Z フラグが立たない
-
-
cset w0, ne
(ne
はZフラグ == 0
を確認)- Z フラグが立っているとき
w0
レジスタに 0 が入る - Z フラグが立っていないとき
w0
レジスタに 1 が入る
- Z フラグが立っているとき
以上より、
-
stxr
に成功
した場合(x8
レジスタが 0)、w0
レジスタに 0 が入る -
stxr
に失敗
した場合(x8
レジスタが 1)、w0
レジスタに 1 が入る
13: ret
w0
レジスタを戻り値とし、呼び出し元の関数に戻ります。
arch_spin_unlock()
1: void arch_spin_unlock(arch_spin_lock_t* lock) TA_NO_THREAD_SAFETY_ANALYSIS {
2: WRITE_PERCPU_FIELD32(num_spinlocks, READ_PERCPU_FIELD32(num_spinlocks) - 1);
3: __atomic_store_n(&lock->value, 0UL, __ATOMIC_RELEASE);
}
取得したロックを解放する関数です。
- 引数
arch_spin_lock_t *lock
こちらを参照
1: void arch_spin_unlock(arch_spin_lock_t *lock) TA_NO_THREAD_SAFETY_ANALYSIS {
-
TA_NO_THREAD_SAFETY_ANALYSIS
の説明はこちら
2: WRITE_PERCPU_FIELD32(num_spinlocks, READ_PERCPU_FIELD32(num_spinlocks) - 1);
実行中 CPU 固有の arm64_percpu
構造体の num_spinlocks
メンバーをデクリメントします。
詳細は、arch_spin_lock()
を参照。
3: __atomic_store_n(&lock->value, 0UL, __ATOMIC_RELEASE);
共有変数 lock->value
を 0 にして、ロックを開放します。
__atmoic_store_n()
はコンパイラビルドイン関数。
こちらを参照。
アセンブラ
<arch_spin_unlock(arch_spin_lock*)>:
1: ldr w8, [x20, #8]
2: sub w8, w8, #1 // =1
3: str w8, [x20, #8]
4: stlr xzr, [x0]
5: ret
1: ldr w8, [x20, #8]
2: sub w8, w8, #1 // =1
3: str w8, [x20, #8]
WRITE_PERCPU_FIELD32(num_spinlocks, READ_PERCPU_FIELD32(num_spinlocks) - 1)
に相当します。
x20
レジスタには percpu
構造体のアドレスが格納されています。
1 行目で、percpu
構造体の先頭から 8 バイト目にあるメンバー num_spinlocks
を w8
レジスタに読み込みます。
2 行目で w8
レジスタの値をデクリメントします。
3 行目で、メモリに書き込みます。
percpu
構造体のデータは CPU ごとに存在するので、(共有変数 lock->value
とは異なり)アクセスに排他制御は必要ありません。
4: stlr xzr, [x0]
__atomic_store_n(&lock->value, 0UL, __ATOMIC_RELEASE)
に相当します。
x0
には、arch_spin_unlock()
の第一引数である arch_spin_lock *lock
が格納されています。
arch_spin_lock
構造体の先頭メンバーが value
であるので、lock
と &lock->value
は同じアドレスを指します。
xzr
レジスタは常に 0 を返すゼロレジスタなので、stlr
命令は lock->value
に 0 を書き込みます。
このとき、(&lock->value
アドレスをアイコンしている)WFE
命令による待機状態な他 CPU を起床させます。
str
命令との違いは Store-Release
で、stlr
命令より前のメモリアクセスする命令は、アウトオブオーダーによって stlr
命令より後に移動されないことを保証します。
5: ret
呼び出し元関数に戻ります。
付録
インラインアセンブラ
asm asm-qualifiers ( AssemblerTemplate
: OutputOperands
[ : InputOperands
[ : Clobbers ] ])
)
ansi や様々な-std オプションでコンパイルできるコードを書くときは、asm の代わりに __asm__ を使ってください(Alternate Keywords 参照)。
— 6.47.2 Extended Asm - Assembler Instructions with C Expression Operands
-
本記事での
asm-qualifiers
はvolatile
のみ拡張 asm 文の典型的な使い方は、入力値を操作して出力値を生成することです。しかし、asm 文が副作用を生むこともあります。その場合、volatile 修飾子を使って、ある種の最適化を無効にする必要があるかもしれません。Volatile 参照。
-
AssemblerTemplate
アセンブラコードの雛形となるリテラル文字列です。固定テキストと、入力、出力、および goto パラメータを参照するトークンを組み合わせたものです。AssemblerTemplate 参照。
-
OutputOperands
AssemblerTemplate の命令によって変更される C 変数のコンマ区切りのリストです。空のリストでも構いません。OutputOperands 参照。
-
InputOperands
AssemblerTemplate の命令で読み込まれる C 言語の式をカンマで区切ったリストです。空のリストでも構いません。InputOperands 参照。
-
Clobbers
AssemblerTemplate によって変更されるレジスタやその他の値のコンマ区切りのリストです。空のリストでも構いません。Clobbers and Scratch Registers 参照。
実例
__asm__ volatile(
"sevl;"
"1: wfe;"
"2: ldaxr %[temp], [%[lock]];"
"cbnz %[temp], 1b;"
"stxr %w[temp], %[val], [%[lock]];"
"cbnz %w[temp], 2b;"
: [temp] "=&r"(temp)
: [lock] "r"(&lock->value), [val] "r"(val)
: "cc", "memory");
-
1:
、2:
はローカルラベル。
1b
、2b
で、当該命令より前に定義した1:
、2:
を参照します。ローカルラベルを定義するには、"N:"(N は非負の整数)という形式のラベルを記述します。そのラベルの最新の前の定義を参照するには、ラベルを定義したときと同じ番号を使用して "Nb" と書きます。ローカルラベルの 次の定義を参照するには、"Nf" と書きます。"b" は "backwards" を、"f" は "forward" を表します。
-
: [temp] "=&r"(temp)
はOutputOperands
(アセンブラ内で変更される変数)C 言語部分の変数
temp
を、アセンブラ内で%[temp]
シンボルとして扱い、値を変更します。
temp
はレジスタ上に確保されます。-
[temp]
は[asmSymbolicName]
オペランドのシンボリック名を指定します。アセンブラのテンプレートでは、この名前を角括弧で囲んで参照します(例:'%[Value]')。名前のスコープは、定義を含む asm 文です。C の変数名は、周囲のコードですでに定義されているものも含めて、すべて有効です。同じ asm 文内の 2 つのオペランドに同じシンボリック名を使用することはできません。
asmSymbolicName を使用しない場合は、アセンブラテンプレートのオペランドリスト内のオペランドの位置(ゼロベース)を使用します。例えば、3 つの出力オペランドがある場合、1 つ目は '%0'、2 つ目は '%1'、3 つ目は '%2' をテンプレート内で使用します。
-
"=&r"
はconstraint
-
constraint
オペランドの配置に関する制約を指定する文字列定数です。詳細は Constraints を参照してください。
出力制約は、'=' (既存の値を上書きする変数)または '+' (読み書き時)のいずれかで始めなければなりません。オペランドが入力に関連付けられている場合を除き、'='を使用する場合、asm への入力時にその場所に 既存の値が含まれていると仮定してはいけません(Input Operandsを参照)。
前置詞の後には、値が存在する場所を示す 1 つ以上の追加制約(Constraints を参照)が必要です。一般的な制約としては、レジスタの「r」、メモリの「m」などがあります。複数の場所を指定した場合(例:「=rm」)、コンパイラは現在のコンテキストに基づいてもっとも効率的な場所を選択します。asm ステートメントで許可されている数だけ候補を挙げれば、オプティマイザーが最適なコードを生成することができます。特定のレジスタを使用しなければならないが、マシン制約では特定のレジスタを選択するための十分な制御ができない場合、ローカルレジスタ変数が解決策となる場合があります(Local Register Variablesを参照)。
-
=
この命令によって、このオペランドが書き込まれることを意味します:前の値は捨てられ、新しいデータで置き換えられます。
-
&
このオペランドは、命令が入力オペランドの使用を終了する前に書き込まれるアーリークローバーオペランドであることを意味します。そのため、このオペランドは、命令によって読み込まれるレジスタや、メモリアドレスの一部としては存在しない可能性があります。
— 6.47.3.3 Constraint Modifier Characters
入力と重なってはいけないすべての出力オペランドには、
&
制約修飾子(修飾子を参照)を使用してください。そうしないと、GCC はアセンブラコードが入力を消費してから出力を生成すると仮定して、出力オペランドを無関係な入力オペランドと同じレジスタに割り当てることがあります。アセンブラコードが実際に複数の命令で構成されている場合、この仮定は間違っている可能性があります。すべての入力オペランドを使う前に出力オペランドを使う場合、その出力オペランドには
&
をつける必要があります。参考:
-
r
レジスタオペランドは、一般的なレジスタであれば許されます。
-
-
(temp)
は(cvariablename)
出力を保持する C 言語の lvalue 式を指定します(通常は変数名)。括弧はこの構文の必須部分です。
-
-
: [lock] "r"(&lock->value), [val] "r"(val)
はInputOperands
(アセンブラ内で読み込む変数)-
[lock] "r"(&lock->value)
C 言語部分の変数
lock->value
のアドレスをレジスタに格納し、アセンブラ内で%[lock]
シンボルとして扱う-
[lock]
はasmSymbolicName
オペランドのシンボリック名を指定します。アセンブラのテンプレートでは、この名前を角括弧で囲んで参照します(例:'%[Value]')。名前のスコープは、定義を含む asm 文です。C の変数名は、周囲のコードですでに定義されているものも含めて、すべて有効です。同じ asm 文内の 2 つのオペランドに同じシンボリック名を使用することはできません。
asmSymbolicName を使用しない場合は、アセンブラテンプレートのオペランドリスト内のオペランドの位置(ゼロベース)を使用します。例えば、出力オペランドが 2 つ、入力が 3 つある場合、テンプレートでは、1 番目の入力オペランドには '%2'、2 番目の入力オペランドには '%3'、3 番目の入力オペランドには '%4' を使用します。
-
"r"
はconstraint
オペランドの配置に関する制約を指定する文字列定数です。
入力制約の文字列は、'=' または '+' で始まってはいけません。複数の場所を指定した場合 (例: '"irm"')、コンパイラは現在のコンテキストに基づいてもっとも効率的な場所を選択します。特定のレジスタを使用しなければならないが、マシン制約では希望する特定のレジスタを選択するための十分な制御ができない場合、ローカルレジスタ変数が解決策となる場合があります (Local Register Variables を参照)。
入力制約には数字も使用できます(例:「0」)。これは、指定された入力が、出力制約リストの(ゼロベースの)インデックスの出力制約と同じ場所になければならないことを示します。出力オペランドに asmSymbolicName 構文を使用する場合は、数字の代わりにこれらの名前(括弧「[]」で囲まれている)を使用することができます。
-
(&lock->value)
はcexpression
asm ステートメントに入力として渡される C 言語の変数または式です。括弧はこの構文の必須部分です。
-
-
[val] "r"(val)
C 言語部分の変数
val
をレジスタに格納し、アセンブラ内で%[val]
シンボルとして扱う
-
-
: "cc", "memory"
はClobbers
(アセンブラ内で変更されるリスト)-
Clobbers
コンパイラは出力オペランドに記載されているエントリの変更を認識していますが、インライン asm コードは出力以外にも変更を加える可能性があります。たとえば、計算のために追加のレジスタが必要になったり、特定のアセンブラ命令の副作用でプロセッサがレジスタを上書きしたりすることがあります。このような変更をコンパイラに知らせるために、クローバーリストにリストアップします。クロバーリストの項目は、レジスタ名または特別なクロバー(以下に記載)のいずれかです。クロバーリストの各項目は、二重引用符で囲んでカンマで区切った文字列定数です。
-
"cc"
フラグレジスタが変更されることを表す。
しかし、sevl
、wfe
、ldaxr
、cbnz
、stxr
のいずれもフラグレジスタを変更しないと思われる。
念の為つけているのか?
実際、アセンブラでもフラグレジスタ(NZCV
)の退避は行われていない。"cc" のクローバーは、アセンブラコードがフラグレジスタを変更することを示します。一部のマシンでは、GCC はコンディションコードを特定のハードウェアレジスタとして表現し、"cc" はこのレジスタに名前を付ける役割を果たします。他のマシンでは、コンディションコードの扱いが異なるため、"cc" を指定しても効果はありません。しかし、ターゲットが何であれ、これは有効です。
-
"memory"
&lock->value
が指すアドレスにアクセスする"memory" クローバーは、アセンブリコードが入出力オペランドに記載されている以外の項目に対してメモリの読み取りまたは書き込みを行うことをコンパイラに伝えます(例えば、入力パラメータの 1 つが指すメモリにアクセスするなど)。メモリに正しい値が含まれていることを確認するために、GCC は asm を実行する前に特定のレジスタ値をメモリにフラッシュする必要があるかもしれません。さらに、コンパイラは asm の前にメモリから読み込んだ値が asm の後も変わらないとは考えず、必要に応じて再読み込みを行います。"memory" クローバーを使用すると、コンパイラーに読み書き可能なメモリバリアが効果的に形成されます。
ただし、このクローバーを使っても、プロセッサが asm 文以降に投機的な読み込みを行うのを防ぐことはできません。これを防ぐには、プロセッサ固有のフェンス命令が必要です。
-
__asm__ volatile("ldr %w[val], [x20, %[offset]]" : [val] "=r"(val) : [offset] "Ir"(offset));
-
: [offset] "Ir"(offset)
-
"I"
ADD 命令の即値オペランドとして有効な整数定数。
C 言語部分の変数
offset
を、アセンブラ部分で[offset]
として参照し、値を取得する。
値はアセンブラ内で即値として扱う。よって、おそらくコンパイル時に値が決定していないとエラーとなると思われる
-
percpu 構造体と x20 レジスタ
x20 レジスタを使用して、常にローカル cpu 構造体を指すようにして、高速アクセスを実現します。
x20 は、clang が(-ffixed-x20 コマンドラインで)固定としてマークすることを許可する、最初に利用可能な callee-saved レジスタです。
PSCI(Power State Coordination Interface) や SMCC(Secure Monitor Call Calling) へのファームウェアコールを行う際に callee で保存されているので、レジスタは自然に保存・復元されます。
// Per cpu structure, pointed to by a fixed register while in kernel mode.
// Aligned on the maximum architectural cache line to avoid cache
// line sharing between cpus.
struct arm64_percpu {
CPU ごとの構造で、カーネルモードでは固定のレジスタで指定されます。
CPU 間でのキャッシュラインの共有を避けるため、アーキテクチャ上の最大キャッシュラインにアラインされます。
percpu 構造体の実体
// per cpu structures, each cpu will point to theirs using the fixed register
arm64_percpu arm64_percpu_array[SMP_MAX_CPUS];
arm64_percpu
構造体は、配列で確保されます。
-
SMP_MAX_CPUS
は 16zircon/kernel/BUILD.gnkernel_defines = [ # TODO: should not be needed in C, but is in one place now. "KERNEL_BASE=$kernel_base", "SMP_MAX_CPUS=$smp_max_cpus", ]
zircon/kernel/params.gniif (current_cpu == "arm64") { smp_max_cpus = 16 }
gn
のパラメータ設定ファイルで指定しています。
x20
レジスタ割当
Primary CPU (CPU 0) での // The kernel is compiled using -ffixed-x20 so the compiler will never use
// this register.
percpu_ptr .req x20
percpu_ptr
は x20
レジスタのエイリアスです。
name .req register name
This creates an alias for register name called name. For example:
foo .req r0
...
// set the per cpu pointer for cpu 0
adr_global percpu_ptr, arm64_percpu_array
...
CPU 0
の x20
レジスタに arm64_percpu_array
配列の先頭アドレスをセットします。
x20
レジスタ割当
Secondary CPUs (CPU 1 以降) での void arm64_init_percpu_early(void) {
// slow lookup the current cpu id and setup the percpu structure
cpu_num_t cpu = arch_curr_cpu_num_slow();
arm64_write_percpu_ptr(&arm64_percpu_array[cpu]);
...
arch_curr_cpu_num_slow()
では、MPIDR_EL1
(Multiprocessor Affinity Register)レジスタの下位 8 ビットを CPU 番号として取得します。
関数 arm64_init_percpu_early()
は、各 CPU ごとに実行されます。
static inline void arm64_write_percpu_ptr(struct arm64_percpu* percpu) {
__asm__ volatile("mov x20, %0" :: "r"(percpu));
}
各 CPU の x20
レジスタに &arm64_percpu_array[cpu]
(cpu
は CPU の番号)をセットします。
スピンロックと SEVL、WFE 命令
SEVL
命令は、初回 WFE
命令を無条件で、待機させないために必要。
Global Exclusives Monitor
は 2 つの役割を行います。
-
LDAXR
命令とSTXR
命令の間にlock->value
へ書き込みがあったかどうかの判断。-
LDAXR
命令で自 CPU のGlobal Exclusives Monitor
をセットする(lock->value
の物理アドレスを対応つける) -
STXR
命令-
Global Exclusives Monitor
がセットされている場合、lock->value
へ書き込み、書き込みに成功する。
このとき、他 CPU のlock->value
に対応したGlobal Exclusives Monitor
をクリアする。
(他 CPU に、自 CPU が書き込みを成功したことを知らしめる)
(自 CPU のGlobal Exclusives Monitor
をクリアするかどうかはIMPLEMENTATION DEFINED
) -
Global Exclusives Monitor
がクリアされている場合(他 CPU が書き込みを行った場合)、lock->value
への書き込みを行わず、書き込みに失敗する
-
-
-
WFE
命令で低消費電力状態になった CPU を起床させる Wake-up イベントの送信。
Global Exclusives Monitor
がセット(排他アクセス状態)からクリア(オープアクセス状態)になると、Wake-up イベントが送信される
ちなみに、通常の STR
命令も、Global Exclusives Monitor
をクリアします。
(Figure B2-5 Global monitor state machine diagram for PE(n) in a multiprocessor system 参照)。
arch_spin_unlock()
では、STLR
命令で他 CPU の Global Exclusives Monitor
をクリアします。
(自 CPU の Global Exclusives Monitor
をクリアするかどうかは IMPLEMENTATION DEFINED
)。
これにより、WFE
で待っている他 CPU に Wake-up イベントが送信され、待機状態から復帰します。
STXR
との違いは、Global Exclusives Monitor
の状態に関わらず書き込みを行う点です。
通常の LDR
命令は Global Exclusives Monitor
をセットしません。
D1.16.1 Wait for Event mechanism and Send event
PE(Processing Element)
PE は、イベントレジスタの値に応じて、WFE(Wait For Event)メカニズムを使用して低電力状態に入ることができます。低電力状態に入るために、PE はイベント待ち命令である WFE を実行し、イベントレジスタがクリアであれば、PE は低電力状態に入ることができます。
PE が低電力状態になった場合、WFE ウェイクアップイベントを受信するまで低電力状態のままです。
アーキテクチャでは、WFE 命令の実行によってメモリコヒーレンシーが失われてはならないことを除いて、低消費電力状態の正確な性質は定義されていません。
WFE メカニズムの動作は、以下のサブセクションで説明されているすべての相互作用に依存します。
Wait For Event と Send Event のメカニズムを使用することで、スピンロックのエネルギー効率を向上させることができます。
- ロックの取得に失敗した PE は、ロックを保持している場所のアドレスを保持する Exclusives モニターが設定された時点で、低消費電力状態への移行を要求する WFE 命令を実行します。
- PE がロックを解放すると、ロック位置への書き込みにより、ロック位置を監視している PE の排他的モニターがクリアされます。この排他的モニターのクリアにより、それらの PE のそれぞれに WFE ウェイクアップイベントが生成されます。その後、これらの PE は再びロックの取得を試みることができます。
— D1.16.1 Wait for Event mechanism and Send event, Arm® Architecture Reference Manual Armv8, for Armv8-A architecture profile
- ロックを保持している場所のアドレスを保持する Exclusives モニター
- ロック位置への書き込みにより、ロック位置を監視している PE の排他的モニターがクリアされます
D9.7.5 Effect on the exclusive monitors
The Event Register
PE のイベントレジスタは、以下のいずれかによって設定されます:
- PE によって実行される、SEVL 命令(Send Event Local)
(中略)The Wait For Event instruction
WFE 命令(Wait For Event)の動作は、イベントレジスタの状態によって異なります:
- イベントレジスタが設定されている場合、この命令はレジスタをクリアし、直ちに完了します。
- イベントレジスタがクリアの場合、PE は実行を中断して低電力状態になります。この状態は、PE が WFE ウェイクアップイベントを検出するまで、または実装が選択した場合はそれ以前に検出するまで、あるいはリセットされるまで続きます。PE が WFE ウェイクアップイベントを検出すると、またはそれ以前に選択した場合は、WFE 命令が完了します。ウェイクアップイベントでイベントレジスタがセットされた場合、実行再開時にイベントレジスタがクリアされるかどうかは、IMPLEMENTATION DEFINED で決定される。
WFE wake-up events in AArch64 state
- PE のグローバルモニターがクリアされたことによるイベント
— D9.7.5 Effect on the exclusive monitors, Arm® Architecture Reference Manual Armv8, for Armv8-A architecture profile
E2.10.2 Exclusive access instructions and shareable memory locations
E2-4067, Operation of the global Exclusives monitor
シェア可能なメモリからの Load-Exclusive 命令は、メモリからのロードを実行し、アクセスの PA を要求元の PE のための排他的アクセスとしてマークさせます。
また、このアクセスにより、要求元の PE がマークした他の PA から排他的アクセスのマークが削除されることもあります。
Note グローバルモニターは、各 PE の共有メモリへの単一の卓越した排他的アクセスのみをサポートします。ある PE が Load-Exclusive 命令を実行しても、他の PE のグローバルモニター状態には影響しません。
Store-Exclusive 命令は、メモリへの条件付きストアを実行します:
ストアは、アクセスされた PA がリクエストした PE の排他的アクセスとしてマークされており、リクエストした PE のローカルモニターとグローバルモニターのステートマシンの両方が排他的アクセスの状態にある場合にのみ、成功が保証されます。この場合には:
- ストアが成功したことを示すために、ステータス値 0 がレジスタに返されます
- リクエストした PE のグローバルモニターのステートマシンの最終状態は IMPLEMENTATION DEFINED です
- アクセスされたアドレスが、他の PE のグローバルモニターステートマシンで排他的アクセスとマークされている場合、そのステートマシンはオープンアクセスの状態に遷移します
要求している PE のために排他的アクセスとしてマークされているアドレスがない場合、ストアは成功しません
- ストアが失敗したことを示すために、ステータス値として 1 がレジスタに返されます
- グローバルモニターは影響を受けず、リクエストした PE に対してオープンアクセスの状態を維持します
別の PA が要求する PE の排他的アクセスとしてマークされている場合、ストアが成功するかどうかは IMPLEMENTATION DEFINED である。
- ストアが成功した場合はステータス値 0 がレジスタに返され、そうでない場合は値 1 が返されます
- PE のグローバル・モニター・ステート・マシンが Store-Exclusive 命令の前に Exclusive Access 状態であった場合、そのステート・マシンが Open Access 状態に移行するかどうかは、IMPLEMENTATION DEFINED である
Store-Exclusive 命令は、ステータス値を返すレジスタを定義します。
共有メモリシステムでは、グローバルモニターは、システム内の各 PE に対して個別のステートマシンを実装します。
PE(n)による共有メモリへのアクセスのためのステートマシンは、PE(n)に見えるすべての共有メモリへのアクセスに応答できる。つまり、それは次に応答します:
- PE(n)が生成したアクセス数
- メモリロケーションの Shareability ドメイン内の他のオブザーバーによって生成されたアクセス。これらのアクセスは、(!n)として識別されます
共有メモリシステムでは、グローバルモニターは、システム内で Load-Exclusive または Store-Exclusive 命令を生成できる各オブザーバーに対して、個別のステートマシンを実装します。
グローバルモニター:
- Exclusive Access の状態では「セット」となっています
- オープンアクセスの状態では「クリア」です
Clear global monitor event
PE のグローバルモニタの状態が、排他的アクセスからオープンアクセスに変化すると、イベントが生成され、その PE のイベントレジスタに保持されます。
このレジスタは,「イベント待ち」メカニズムで使用されます(D1-2391 ページの「低電力状態に移行するためのメカニズム」を参照)。— E2.10.2 Exclusive access instructions and shareable memory locations, Arm® Architecture Reference Manual Armv8, for Armv8-A architecture profile
G1.18.1 Wait For Event and Send Event
WFE(Wait For Event)機構は、PE が低電力状態への移行を要求し、その要求が成功した場合、SEV 操作によってイベントが生成されるか、別の WFE ウェイクアップイベントが発生するまで、その状態を維持することを可能にします。
Example G1-2 では、スピンロックの実装で、このメカニズムを使ってエネルギーを節約する方法を説明しています。Example G1-2 Spinlock as an example of using Wait For Event and Send Event
マルチプロセッサ OS では、複数の PE から同時にアクセスされるデータ構造を保護するためのロック機構が必要です。
これらの機構は、異なる PE が矛盾した変更を行おうとすると、データ構造が矛盾したり破損したりするのを防ぎます。
ある PE がデータ構造を使用しているためにロックがビジー状態になると、他の PE にとってはロックが解除されるのを待つ以外に何もできない場合があります。
例えば、ある PE がデバイスからの割り込みを処理している場合、デバイスから受け取ったデータをキューに追加する必要があるかもしれません。
他の PE がキューからデータを削除している場合、その PE はキューを保持するメモリ領域をロックしていることになります。
最初の PE は、キューの状態が一貫しており、ロックが解除されるまで、新しいデータを追加することができません。
最初の PE は、データがキューに追加されるまで割り込みハンドラから戻ることができないので、待たなければなりません。一般的には、このような場合にはスピンロック機構を使用します:
- 保護されたデータへのアクセスを必要とする PE は、シングルコピーのアトミックな同期とセマフォ (E2-4063 ページ) で説明した Load-Exclusive および Store-Exclusive 操作などのシングルコピー・アトミック同期プリミティブを使用してロックを取得しようとします。
- PE がロックを取得した場合、PE はメモリ操作を行い、ロックを解除します。
- PE は、ロックを取得できない場合、ロックが使用可能になるまで、タイトループでロック値を繰り返し読み取ります。この時点で、PE は再びロックの取得を試みます。
スピンロック機構はすべての状況に適しているわけではありません:
— G1.18.1 Wait For Event and Send Event, Arm® Architecture Reference Manual Armv8, for Armv8-A architecture profile
Load-Acquire and Store-Release
Load-Acquire, Load-AcquirePC, and Store-Release
(中略)
Load-Acquire と Load-AcquirePC 命令の基本原理は、以下の間に順序を導入することです:
- Load-Acquire または Load-AcquirePC 命令によって生成されたメモリアクセス
- Load-Acquire または Load-AcquirePC 命令の後に、プログラム順で現れるメモリアクセス(中略)
(中略)
Store-Release 命令の基本原則は、以下の間に順序を導入することです:
- Store-Release 命令を実行する PE によって生成され、Store-Release 命令の前にプログラム順に表示される一連のメモリアクセス、RWx(中略)
- Store-Release 命令によって生成されたメモリアクセス(中略)
— B2.3.10 Memory barriers, Arm® Architecture Reference Manual Armv8, for Armv8-A architecture profile
ARM によるスピンロックのリファレンス実装
CAS (Compare and Swap)
を使ったスピンロックの実装は、別記事で紹介しています。
TA_* マクロ
void arch_spin_lock(arch_spin_lock_t* lock) TA_ACQ(lock);
bool arch_spin_trylock(arch_spin_lock_t* lock) TA_TRY_ACQ(false, lock);
void arch_spin_unlock(arch_spin_lock_t* lock) TA_REL(lock);
こちらを参照。
Discussion