カーネルのロックは実行コンテキストから始まります。プロセスコンテキスト間では正しい ロックでも、同じデータへ softirq、ハード割り込み、マスク不能割り込み(NMI)から アクセスするとデッドロックする可能性があります。プリミティブとそのサフィックスは、 保護対象の状態にアクセスするすべてのコンテキストに一致させる必要があります。
スピンロックのセマンティクス #
Declare and initialize ordinary spin locks with:
static DEFINE_SPINLOCK(queue_lock);
struct queue {
spinlock_t lock;
};
spin_lock_init(&queue->lock);PREEMPT_RT でない SMP カーネルでは、spin_lock() はローカル CPU のプリエンプション
を無効にし、排他的所有権を取得するまでスピンします。ローカル割り込みや softirq は
無効にしません。保持者は sleep、明示的なスケジュール、ブロッキングする割り当て、
sleep するロックを取得する可能性があるコードの呼び出しを行ってはなりません。
非 RT の単一プロセッサービルドでは、他 CPU との競合がないため実装の一部がプリエンプ ション制御やコンパイラー制御にコンパイルされることがあります。正しさをその最適化に 依存させず、同じソースが SMP でも有効でなければなりません。
PREEMPT_RT は spinlock_t と rwlock_t のセマンティクスを変更し、RT mutex ベースの
実装を使ってプリエンプト可能にします。RT と非 RT の両方で真にスピンする非プリエンプト
可能なロックが必要なコードは raw_spinlock_t を使いますが、raw クリティカルセクション
は短く保ち、RT で sleep するコードを呼んではなりません。この置換はロックヘッダーと
コアコード内で実装されるため、アプリケーションコードで設定条件を再現しないでください。
プロセス、softirq、hardirq コンテキストを一致させる #
同じ CPU への再入を防ぐ最も狭いバリアントを選びます。
| Data Is Accessed From | Process-Context Acquisition | Interrupt-Side Acquisition |
|---|---|---|
| Process context only | spin_lock() | Not applicable |
| Process context and softirq/tasklet | spin_lock_bh() | spin_lock() in the softirq |
| Process context and hardirq | spin_lock_irqsave() | spin_lock() in the handler |
| Hardirq contexts with known IRQ state | spin_lock_irq() or spin_lock_irqsave() | According to the caller’s state |
spin_lock_bh() はロック取得前にローカル bottom half を無効にします。
spin_lock_irq() はローカルのマスク可能割り込みを無効にします。
spin_lock_irqsave(lock, flags) は以前の割り込み状態も保存し、
spin_unlock_irqrestore(lock, flags) はその状態を正確に復元します。
unsigned long flags;
spin_lock_irqsave(&device->lock, flags);
update_device_state(device);
spin_unlock_irqrestore(&device->lock, flags);1 つの字句上の取得・解放ペアでは同じ flags 変数を使い、入口で割り込みが有効だった
と仮定しないでください。ローカル割り込みを無効にすると、保持者の CPU 上の割り込み
が自分のロックを待つことを防ぎます。スピンロック自体は他 CPU で実行されるハンドラー
を排除します。
データが hardirq インスタンス間だけで共有される場合、割り込みラインが同じ CPU に
再入できなければハンドラーは spin_lock() を使えます。プロセスコンテキストの
アクセサーは引き続き IRQ 無効化バリアントを使う必要があります。
NMI コンテキストには別の設計が必要 #
local_irq_disable() と spin_lock_irqsave() は NMI をマスクしません。NMI ハンドラー
が、割り込まれたコードが同じ CPU で保持できるロックを取得すると、割り込まれた保持者
が再開できないため NMI は永遠にスピンします。
したがって NMI 共有データには NMI 向けに設計されたプロトコルが必要です。
- NMI セーフなアトミック操作によるロックレスまたは CPU ごとの記録。
- NMI リーダーによってライターがデッドロックする形でプリエンプトされないシーケンスカウンター方式。
- NMI 側の観測を破棄してよい場合の
raw_spin_trylock()。 - すべての取得経路が NMI 再入不能を証明する場合だけ raw ロックを使う。
「raw」は PREEMPT_RT 上でもロックがスピンのセマンティクスを保持することを意味し、 任意のクリティカルセクションを NMI セーフにするものではありません。
取得と解放の順序付け #
ロック取得には acquire セマンティクスがあり、アンロックには release セマンティクスが あります。そのためクリティカルセクション内の操作は、同じロックで同じデータを保護 する別 CPU に対して順序付けられます。
spin_lock(&state->lock);
state->payload = payload;
state->ready = true;
spin_unlock(&state->lock);後から成功した取得者は、アンロック前に保護された書き込みを観測します。一部の
リーダーが意図的にロックなしでデータへアクセスする場合、ロックはプロトコルの代替
ではありません。そのようなリーダーには READ_ONCE()、release/acquire 公開、RCU、
明示的なバリアが必要になることがあります。Linux のメモリバリア文書は、ロックの
acquire/release 順序付けと一般的な完全メモリバリアを区別しています。クリティカル
セクション外で順序付けが必要なコードは、文書化されたバリアプリミティブを使います。
ロック順序は別の要件です。ある経路が A、次に B を取得するなら、別の経路は B、
次に A を取得してはなりません。IRQ 状態バリアントはローカル割り込みの逆転を防ぎ
ますが、ロック間の循環は防ぎません。
アーキテクチャ実装 #
公開 spinlock_t API は汎用ロックヘッダーで定義され、raw ロックコードを経由して
アーキテクチャ操作へ対応付けられます。SMP では raw_spinlock_t が
arch_spinlock_t と、任意のデバッグ・lockdep 状態を含みます。各アーキテクチャは、
アトミック命令と仮想化環境に適した実装を選択します。
x86 は queued-spinlock 基盤を使います。競合のない経路はコンパクトなロックワードで 動作し、競合する呼び出し側は 1 つのキャッシュラインを繰り返し無効化する代わりに キューを形成します。準仮想化フックは待機と起床動作の一部を置き換えられます。 アーキテクチャコードは順序付けとアトミックプリミティブを提供し、汎用コードは API の一貫性、計測、設定依存の変換を提供します。
Source layout is not an ABI. Verify a specific kernel through include/linux/spinlock*.h, kernel/locking/, and arch/<arch>/include/asm/ from the same revision.
Contention and Debugging #
Contention wastes CPU time and moves the lock’s cache line among processors. Keep spin-locked sections bounded, move allocations and slow I/O outside them, partition unrelated state, and avoid taking one global lock for per-CPU data. Replacing one lock with several can reduce contention but adds ordering and lifetime complexity, so measure before changing the design.
Kernel facilities help establish facts:
CONFIG_LOCKDEPand lockdep annotations detect observed and potential dependency cycles, incorrect IRQ usage, and context mismatches.CONFIG_DEBUG_SPINLOCKadds ownership and state checks.CONFIG_PROVE_LOCKINGenables broader locking validation.lockstatandperf lockreport contention and acquisition behavior.
Debug builds alter timing and overhead. A clean lockdep run increases confidence but cannot prove that unexecuted paths have a correct lifetime or memory-ordering protocol.
Reader-Writer Spin Locks #
rwlock_t permits concurrent readers or one writer:
static DEFINE_RWLOCK(routes_lock);
unsigned long flags;
read_lock_irqsave(&routes_lock, flags);
route = lookup_route(key);
read_unlock_irqrestore(&routes_lock, flags);
write_lock_irqsave(&routes_lock, flags);
replace_route(key, replacement);
write_unlock_irqrestore(&routes_lock, flags);Reader-writer spin locks require more atomic operations than a plain spin lock. They provide no portable fairness guarantee; a workload and implementation can delay a writer behind readers or delay readers behind a writer. A read lock cannot be upgraded safely to a write lock. If an operation might modify the structure, release and retry under the write lock or acquire the write lock from the start.
New uses deserve scrutiny:
- Use a mutex when all access is in sleepable process context, especially when a holder may block.
- Use RCU when reads dominate, readers can tolerate its access model, and updates can defer reclamation until after a grace period.
- Use a sequence lock/count for small snapshots when readers can retry and writers can be serialized.
- Use a plain spin lock when critical sections are short; its lower reader-side complexity can outperform
rwlock_t.
Selection depends on context, hold time, contention, read/write ratio, and object lifetime rather than on the abstract appeal of parallel readers.
コメント
GitHub で閲覧・コメントする(投稿にはログインが必要です)