💮

Zircon のスピンロック実装(aarch64編)

47 min read

Zircon のスピンロック実装(x86-64 編) の続編です。

事前知識

関連する 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 に、AcquireExclusive が追加された命令です。

  • 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 のスピンロック関数

仕様:

  • 共有変数は lock->value
  • 共有変数が 0 の時、誰もロックしていない
  • 共有変数が 0 以外の時、誰かがロックしている
    • 共有変数にはロックを取得した CPU の CPU番号+1 がセットされる。これで、誰がロックを取得しているかがわかる
      (CPU 番号は 0 から始まるため、誰もロックしていない状態と区別するために、1 を足す)

arch_spin_lock()

zircon/kernel/arch/arm64/spinlock.cc
 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.h
    typedef 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;
  • valCPU 番号(0..)+ 1
    ロック取得成功時に、この値を共有変数 lock->value にセットする

  • arch_curr_cpu_num() は、実行中 CPU の固有情報 struct arm64_percpucpu_num メンバー(CPU 番号を表す)を返す

    zircon/kernel/arch/arm64/include/arch/arm64/mp.h
    static inline cpu_num_t arch_curr_cpu_num() {
      return arm64_read_percpu_u32(offsetof(struct arm64_percpu, cpu_num));
    }
    
    zircon/kernel/arch/arm64/include/arch/arm64/mp.h
    static 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 でコンパイラによる最適化を抑制
    • ldaxrAcquire により、CPU によるアウトオブオーダーを抑制
  • 1:2: はローカルラベル。
    1b2b で、当該命令より前に定義した 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] として扱う。値はレジスタ上に格納される
  • : "cc", "memory"ccmemory が変更されることをコンパイラに伝える。
    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.h
    static 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() は前述の通り

アセンブラ

llvm-objdump -D -C --no-leading-addr --no-show-raw-insn out/default/kernel_arm64/zircon.elf
<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)が stxrlock->value の書き換えに成功すると、起床する

省電力対応という意味で、x86-64pause に対応。

 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 レジスタ(変数 valcpu_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_spinlocksw8 レジスタに読み込みます。
w8 レジスタの値をインクリメント。
num_spinlocksw8 レジスタの値を書き込みます。

percpu 構造体のデータは CPU ごとに存在するので、(共有変数 lock->value とは異なり)アクセスに排他制御は必要ありません。

12:  ret

リンクレジスタ(x30)が指すアドレスに無条件に分岐します。
つまり、呼び出し元の関数に戻ります。

arch_spin_trylock()

zircon/kernel/arch/arm64/spinlock.cc
 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;
  • valCPU 番号(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"ccmemory が変更されることをコンパイラに伝える。
    cc はフラグレジスタが変更されることを表す。しかし、フラグレジスタを変更する命令はアセンブラ中に含まれていない(はず)。
    memory&lock->value アドレスの参照先へアクセスすることを表す

14:  if (out == 0) {
15:    WRITE_PERCPU_FIELD32(num_spinlocks, READ_PERCPU_FIELD32(num_spinlocks) + 1);
16:  }

outstxr の結果が入っています。
stxr 成功時(ロック取得成功時)、out は 0 となります。
WRITE_PERCPU_FIELD32() については、arch_spin_lock() を参照。

17:  return out;

out、すなわち stxr の結果が入っています。

  • ロック成功時、0(false にキャスト)を返す
  • ロック失敗時、1(true にキャスト)を返す

アセンブラ

llvm-objdump -D -C --no-leading-addr --no-show-raw-insn out/default/kernel_arm64/zircon.elf
<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->valuex8 レジスタに読み込みます。
詳細は、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, neneZフラグ == 0 を確認)

    • Z フラグが立っているとき w0 レジスタに 0 が入る
    • Z フラグが立っていないとき w0 レジスタに 1 が入る

以上より、

  • stxr成功した場合(x8 レジスタが 0)、w0 レジスタに 0 が入る
  • stxr失敗した場合(x8 レジスタが 1)、w0 レジスタに 1 が入る
13:  ret

w0 レジスタを戻り値とし、呼び出し元の関数に戻ります。

arch_spin_unlock()

zircon/kernel/arch/arm64/spinlock.cc
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() はコンパイラビルドイン関数。
こちらを参照。

アセンブラ

llvm-objdump -D -C --no-leading-addr --no-show-raw-insn out/default/kernel_arm64/zircon.elf
<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_spinlocksw8 レジスタに読み込みます。
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-qualifiersvolatile のみ

    拡張 asm 文の典型的な使い方は、入力値を操作して出力値を生成することです。しかし、asm 文が副作用を生むこともあります。その場合、volatile 修飾子を使って、ある種の最適化を無効にする必要があるかもしれません。Volatile 参照。

    Qualifiers

  • AssemblerTemplate

    アセンブラコードの雛形となるリテラル文字列です。固定テキストと、入力、出力、および goto パラメータを参照するトークンを組み合わせたものです。AssemblerTemplate 参照。

    Parameters

  • OutputOperands

    AssemblerTemplate の命令によって変更される C 変数のコンマ区切りのリストです。空のリストでも構いません。OutputOperands 参照。

    Parameters

  • InputOperands

    AssemblerTemplate の命令で読み込まれる C 言語の式をカンマで区切ったリストです。空のリストでも構いません。InputOperands 参照。

    Parameters

  • Clobbers

    AssemblerTemplate によって変更されるレジスタやその他の値のコンマ区切りのリストです。空のリストでも構いません。Clobbers and Scratch Registers 参照。

    Parameters

実例

__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: はローカルラベル。
    1b2b で、当該命令より前に定義した 1:2: を参照します。

    ローカルラベルを定義するには、"N:"(N は非負の整数)という形式のラベルを記述します。そのラベルの最新の前の定義を参照するには、ラベルを定義したときと同じ番号を使用して "Nb" と書きます。ローカルラベルの 次の定義を参照するには、"Nf" と書きます。"b" は "backwards" を、"f" は "forward" を表します。

    Symbol Names (Using as)

  • : [temp] "=&r"(temp)OutputOperands(アセンブラ内で変更される変数)

    C 言語部分の変数 temp を、アセンブラ内で %[temp] シンボルとして扱い、値を変更します。
    temp はレジスタ上に確保されます。

    • [temp][asmSymbolicName]

      オペランドのシンボリック名を指定します。アセンブラのテンプレートでは、この名前を角括弧で囲んで参照します(例:'%[Value]')。名前のスコープは、定義を含む asm 文です。C の変数名は、周囲のコードですでに定義されているものも含めて、すべて有効です。同じ asm 文内の 2 つのオペランドに同じシンボリック名を使用することはできません。

      asmSymbolicName を使用しない場合は、アセンブラテンプレートのオペランドリスト内のオペランドの位置(ゼロベース)を使用します。例えば、3 つの出力オペランドがある場合、1 つ目は '%0'、2 つ目は '%1'、3 つ目は '%2' をテンプレート内で使用します。

      6.47.2.3 Output Operands

    • "=&r"constraint

      • constraint

        オペランドの配置に関する制約を指定する文字列定数です。詳細は Constraints を参照してください。

        出力制約は、'=' (既存の値を上書きする変数)または '+' (読み書き時)のいずれかで始めなければなりません。オペランドが入力に関連付けられている場合を除き、'='を使用する場合、asm への入力時にその場所に 既存の値が含まれていると仮定してはいけません(Input Operandsを参照)。

        前置詞の後には、値が存在する場所を示す 1 つ以上の追加制約(Constraints を参照)が必要です。一般的な制約としては、レジスタの「r」、メモリの「m」などがあります。複数の場所を指定した場合(例:「=rm」)、コンパイラは現在のコンテキストに基づいてもっとも効率的な場所を選択します。asm ステートメントで許可されている数だけ候補を挙げれば、オプティマイザーが最適なコードを生成することができます。特定のレジスタを使用しなければならないが、マシン制約では特定のレジスタを選択するための十分な制御ができない場合、ローカルレジスタ変数が解決策となる場合があります(Local Register Variablesを参照)。

        6.47.2.3 Output Operands

      • =

        この命令によって、このオペランドが書き込まれることを意味します:前の値は捨てられ、新しいデータで置き換えられます。

        6.47.3.3 Constraint Modifier Characters

      • &

        このオペランドは、命令が入力オペランドの使用を終了する前に書き込まれるアーリークローバーオペランドであることを意味します。そのため、このオペランドは、命令によって読み込まれるレジスタや、メモリアドレスの一部としては存在しない可能性があります。

        6.47.3.3 Constraint Modifier Characters

        入力と重なってはいけないすべての出力オペランドには、& 制約修飾子(修飾子を参照)を使用してください。そうしないと、GCC はアセンブラコードが入力を消費してから出力を生成すると仮定して、出力オペランドを無関係な入力オペランドと同じレジスタに割り当てることがあります。アセンブラコードが実際に複数の命令で構成されている場合、この仮定は間違っている可能性があります。

        6.47.2.3 Output Operands

        すべての入力オペランドを使う前に出力オペランドを使う場合、その出力オペランドには & をつける必要があります。

        参考:

      • r

        レジスタオペランドは、一般的なレジスタであれば許されます。

        6.47.3.1 Simple Constraints

    • (temp)(cvariablename)

      出力を保持する C 言語の lvalue 式を指定します(通常は変数名)。括弧はこの構文の必須部分です。

      6.47.2.3 Output Operands

  • : [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' を使用します。

        6.47.2.5 Input Operands

      • "r"constraint

        オペランドの配置に関する制約を指定する文字列定数です。

        入力制約の文字列は、'=' または '+' で始まってはいけません。複数の場所を指定した場合 (例: '"irm"')、コンパイラは現在のコンテキストに基づいてもっとも効率的な場所を選択します。特定のレジスタを使用しなければならないが、マシン制約では希望する特定のレジスタを選択するための十分な制御ができない場合、ローカルレジスタ変数が解決策となる場合があります (Local Register Variables を参照)。

        入力制約には数字も使用できます(例:「0」)。これは、指定された入力が、出力制約リストの(ゼロベースの)インデックスの出力制約と同じ場所になければならないことを示します。出力オペランドに asmSymbolicName 構文を使用する場合は、数字の代わりにこれらの名前(括弧「[]」で囲まれている)を使用することができます。

        6.47.2.5 Input Operands

      • (&lock->value)cexpression

        asm ステートメントに入力として渡される C 言語の変数または式です。括弧はこの構文の必須部分です。

        6.47.2.5 Input Operands

    • [val] "r"(val)

      C 言語部分の変数 val をレジスタに格納し、アセンブラ内で %[val] シンボルとして扱う

  • : "cc", "memory"Clobbers(アセンブラ内で変更されるリスト)

    • Clobbers

      コンパイラは出力オペランドに記載されているエントリの変更を認識していますが、インライン asm コードは出力以外にも変更を加える可能性があります。たとえば、計算のために追加のレジスタが必要になったり、特定のアセンブラ命令の副作用でプロセッサがレジスタを上書きしたりすることがあります。このような変更をコンパイラに知らせるために、クローバーリストにリストアップします。クロバーリストの項目は、レジスタ名または特別なクロバー(以下に記載)のいずれかです。クロバーリストの各項目は、二重引用符で囲んでカンマで区切った文字列定数です。

      6.47.2.6 Clobbers and Scratch Registers

    • "cc"

      フラグレジスタが変更されることを表す。
      しかし、sevlwfeldaxrcbnzstxr のいずれもフラグレジスタを変更しないと思われる。
      念の為つけているのか?
      実際、アセンブラでもフラグレジスタ(NZCV)の退避は行われていない。

      "cc" のクローバーは、アセンブラコードがフラグレジスタを変更することを示します。一部のマシンでは、GCC はコンディションコードを特定のハードウェアレジスタとして表現し、"cc" はこのレジスタに名前を付ける役割を果たします。他のマシンでは、コンディションコードの扱いが異なるため、"cc" を指定しても効果はありません。しかし、ターゲットが何であれ、これは有効です。

      6.47.2.6 Clobbers and Scratch Registers

    • "memory"

      &lock->value が指すアドレスにアクセスする

      "memory" クローバーは、アセンブリコードが入出力オペランドに記載されている以外の項目に対してメモリの読み取りまたは書き込みを行うことをコンパイラに伝えます(例えば、入力パラメータの 1 つが指すメモリにアクセスするなど)。メモリに正しい値が含まれていることを確認するために、GCC は asm を実行する前に特定のレジスタ値をメモリにフラッシュする必要があるかもしれません。さらに、コンパイラは asm の前にメモリから読み込んだ値が asm の後も変わらないとは考えず、必要に応じて再読み込みを行います。"memory" クローバーを使用すると、コンパイラーに読み書き可能なメモリバリアが効果的に形成されます。

      ただし、このクローバーを使っても、プロセッサが asm 文以降に投機的な読み込みを行うのを防ぐことはできません。これを防ぐには、プロセッサ固有のフェンス命令が必要です。

      6.47.2.6 Clobbers and Scratch Registers

__asm__ volatile("ldr %w[val], [x20, %[offset]]" : [val] "=r"(val) : [offset] "Ir"(offset));
  • : [offset] "Ir"(offset)

    • "I"

      ADD 命令の即値オペランドとして有効な整数定数。

      6.47.3.4 Constraints for Particular Machines

      C 言語部分の変数 offset を、アセンブラ部分で [offset] として参照し、値を取得する。
      値はアセンブラ内で即値として扱う。よって、おそらくコンパイル時に値が決定していないとエラーとなると思われる

percpu 構造体と x20 レジスタ

x20 レジスタを使用して、常にローカル cpu 構造体を指すようにして、高速アクセスを実現します。
x20 は、clang が(-ffixed-x20 コマンドラインで)固定としてマークすることを許可する、最初に利用可能な callee-saved レジスタです。
PSCI(Power State Coordination Interface) や SMCC(Secure Monitor Call Calling) へのファームウェアコールを行う際に callee で保存されているので、レジスタは自然に保存・復元されます。

zircon/kernel/arch/arm64/include/arch/arm64/mp.h

zircon/kernel/arch/arm64/include/arch/arm64/mp.h
// 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 構造体の実体

zircon/kernel/arch/arm64/mp.cc
// 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 は 16

    zircon/kernel/BUILD.gn
      kernel_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.gni
      if (current_cpu == "arm64") {
        smp_max_cpus = 16
      }
    

    gn のパラメータ設定ファイルで指定しています。

Primary CPU (CPU 0) での x20 レジスタ割当

zircon/kernel/arch/arm64/include/arch/arm64/asm.h
// The kernel is compiled using -ffixed-x20 so the compiler will never use
// this register.
percpu_ptr .req x20

percpu_ptrx20 レジスタのエイリアスです。

name .req register name
This creates an alias for register name called name. For example:
foo .req r0

9.4.4 ARM Machine Directives

zircon/kernel/arch/arm64/start.S
    ...
    // set the per cpu pointer for cpu 0
    adr_global percpu_ptr, arm64_percpu_array
    ...

CPU 0x20 レジスタに arm64_percpu_array 配列の先頭アドレスをセットします。

Secondary CPUs (CPU 1 以降) での x20 レジスタ割当

zircon/kernel/arch/arm64/mp.cc
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 ごとに実行されます。

zircon/kernel/arch/arm64/include/arch/arm64/mp.h
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 命令

sequenceDiagram
    participant CPU0 as CPU 0
    participant CPU1 as CPU 1

    Note over CPU0: arch_spin_lock():ロック取得
    Note over CPU1: arch_spin_lock():wfe で低消費電力状態へ移行
    activate CPU1
    Note over CPU0: arch_spin_unlock():ロック解放<br/>stlr で Wake-up Event 生成
    activate CPU0
    CPU0 ->> CPU1: Wake-up Event
    deactivate CPU0
    Note over CPU1: 低消費電力状態から復帰
    deactivate CPU1
flowchart TD
    classDef noteclass fill:#fff5ad,stroke:#decc93,stroke-width:1px;

    START(ロック取得開始) --> SEVL
    SEVL --> WFE1[WFE]
    SEVL -.- SEVLNOTE[Event Registerを1にする]:::noteclass
    %% subgraph WFE
    WFE1 --> ER{Event Register?}
    ER -->|0| LOW(低消費電力状態で待機)
    ER -->|1| ERto0[Event Registerを0にする]
    %% end
    ERto0 --> LDAXR
    LOW -->|"Wake-up Event 受信で起床<br/>他CPUによりGlobal Exclusives Monitorを<br/>クリアされる<br/>他CPUがlock->valueを書き換えた"| LDAXR
    LDAXR --> CBNZ1
    LDAXR -.- LDAXRNOTE[lock->value読み込み<br/>Global Exclusives Monitorをセット]:::noteclass
    %% subgraph CBNZ1s[CBNZ]
    CBNZ1[CBNZ] --> CBNZ1_{lock->value?}
    %% end
    CBNZ1_ -->|非0<br/>すでに他CPUがロック取得中| WFE1
    CBNZ1_ -->|0<br/>ロックが空いている| STXR
    STXR -.- STXRNOTE[lock->value書き込みを試みる<br/>Global Exclusives Monitorをクリア]:::noteclass
    STXR --> CBNZ2[CBNZ]
    %% subgraph CBNZ2s[CBNZ]
    CBNZ2 --> CBNZ2_{LDAXR-STXR間に他CPUのlock->valueへの<br/>書き込みがあったか?}
    %% 自分がクリアする前に、Global Exclusives Monitorがクリアされていたか?
    %% end
    CBNZ2_ -->|あった<br/>他CPUがロックを取得した<br/>lock->valueへの書き込みに失敗| LDAXR
    CBNZ2_ -->|なかった<br/>lock->valueへの書き込みに成功| END(ロック取得)

SEVL 命令は、初回 WFE 命令を無条件で、待機させないために必要。

Global Exclusives Monitor は 2 つの役割を行います。

  1. 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 への書き込みを行わず、書き込みに失敗する
  2. 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 をセットしません。

How to understand ARMv8 'SEVL' instruction in spin-lock? - Cortex-A / A-Profile forum - Processors - Arm Community

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 によるスピンロックのリファレンス実装

https://github.com/ARM-software/arm-trusted-firmware/blob/master/lib/locks/exclusive/aarch64/spinlock.S

CAS (Compare and Swap) を使ったスピンロックの実装は、別記事で紹介しています。

TA_* マクロ

zircon/kernel/arch/arm64/include/arch/spinlock.h
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

ログインするとコメントできます