Linux Kernel Synchronization and Locking
Linux Kernel 同期とロック機構 - 包括的技術ガイド
文書情報
- 対象カーネルバージョン: Linux 6.x 系
- 対象読者: カーネル開発者、システムプログラマ、SRE エンジニア
- 最終更新: 2026年4月
目次
- はじめに
- カーネルにおける並行性の基礎
- プリエンプションモデル
- アトミック操作
- スピンロック
- リーダー・ライタースピンロック
- ミューテックス
- セマフォ
- Read-Copy-Update (RCU)
- RCU の変種
- シーケンシャルロック
- 完了変数 (Completion Variables)
- Per-CPU 変数
- メモリバリア
- ロック順序とデッドロック防止
- Lockdep - ロック依存関係バリデータ
- RT-Mutex と優先度継承
- PREEMPT_RT パッチセット
- ロック競合分析
- ツール: lockstat, lockdep, perf lock
- まとめとベストプラクティス
はじめに
Linux カーネルは、現代のマルチコアプロセッサ上で動作する大規模な並行システムである。数千のプロセスが同時に実行され、割り込みが非同期に発生し、複数の CPU コアが共有データ構造に同時にアクセスする環境において、データの整合性を保証する同期メカニズムは不可欠である。
本ドキュメントでは、Linux カーネルが提供する全ての主要な同期プリミティブについて、その設計思想、実装の詳細、使用方法、パフォーマンス特性を網羅的に解説する。
なぜカーネル同期が重要か
CPU 0 CPU 1
───── ─────
read shared_counter read shared_counter
increment value increment value
write shared_counter write shared_counter
期待結果: shared_counter += 2
実際の結果: shared_counter += 1 (レースコンディション)
上記のような単純な例でも、適切な同期なしではデータ破壊が発生する。カーネル空間では、このような問題がシステム全体のクラッシュ、データ損失、セキュリティ脆弱性に直結する。
並行性の発生源
Linux カーネルにおける並行性は、以下の複数の発生源を持つ:
| 発生源 | 説明 | 影響範囲 |
|---|---|---|
| SMP (対称型マルチプロセッシング) | 複数の CPU が同時にカーネルコードを実行 | グローバル |
| プリエンプション | カーネルタスクが他のタスクに割り込まれる | ローカル CPU |
| ハードウェア割り込み | 非同期にハードウェアイベントが発生 | ローカル CPU |
| ソフト割り込み (softirq) | 遅延処理メカニズム | ローカル/グローバル |
| ワークキュー | カーネルスレッドによる非同期処理 | グローバル |
| タイマー | 時間ベースのコールバック | グローバル |
カーネルにおける並行性の基礎
SMP (対称型マルチプロセッシング)
SMP システムでは、複数の CPU コアが同一のメモリ空間を共有し、同時にカーネルコードを実行する。これは最も根本的な並行性の発生源である。
/* SMP 環境での典型的な競合状態 */
struct shared_data {
int counter;
struct list_head items;
unsigned long flags;
};
static struct shared_data global_data;
/* CPU 0 で実行 */
void cpu0_function(void)
{
global_data.counter++; /* 読み取り-変更-書き込み */
list_add(&new_item, &global_data.items); /* リスト操作 */
}
/* CPU 1 で同時に実行 */
void cpu1_function(void)
{
global_data.counter++; /* 同じデータへの同時アクセス */
list_del(&old_item); /* リストの整合性が破壊される */
}
SMP の構成確認:
# CPU コア数の確認
$ nproc
8
# CPU トポロジの詳細確認
$ lscpu
Architecture: x86_64
CPU(s): 8
Thread(s) per core: 2
Core(s) per socket: 4
Socket(s): 1
NUMA node(s): 1
# カーネルの SMP 設定確認
$ grep -i smp /boot/config-$(uname -r)
CONFIG_SMP=y
CONFIG_NR_CPUS=8192
CONFIG_SCHED_SMT=y
# /proc からの CPU 情報
$ cat /proc/cpuinfo | grep "processor" | wc -l
8
# NUMA トポロジの確認
$ numactl --hardware
available: 2 nodes (0-1)
node 0 cpus: 0 1 2 3
node 0 size: 16384 MB
node 1 cpus: 4 5 6 7
node 1 size: 16384 MB
node distances:
node 0 1
0: 10 20
1: 20 10
プリエンプション
カーネルプリエンプションにより、カーネルコード実行中のタスクが他の高優先度タスクに CPU を譲ることがある。これにより、ユニプロセッサシステムでも並行性の問題が発生する。
/* プリエンプションによる競合の例 */
void problematic_function(void)
{
/* タスク A がここでプリエンプトされると... */
temp = global_variable;
/* ...タスク B が global_variable を変更する可能性がある */
temp++;
global_variable = temp; /* タスク B の変更が失われる */
}
/* プリエンプションを無効化して保護 */
void safe_function(void)
{
preempt_disable();
temp = global_variable;
temp++;
global_variable = temp;
preempt_enable();
}
プリエンプションカウンタの仕組み:
/*
* include/linux/preempt.h
* プリエンプションカウンタの構造
*
* ビットレイアウト:
* bits 0-7: プリエンプション無効カウント
* bits 8-15: ソフト IRQ カウント
* bits 16-19: ハード IRQ カウント
* bit 20: NMI
* bit 21: PREEMPT_NEED_RESCHED
*/
#define PREEMPT_BITS 8
#define SOFTIRQ_BITS 8
#define HARDIRQ_BITS 4
#define NMI_BITS 1
#define preempt_count() (current_thread_info()->preempt_count)
static __always_inline void preempt_disable(void)
{
preempt_count_inc();
barrier();
}
static __always_inline void preempt_enable(void)
{
barrier();
if (unlikely(preempt_count_dec_and_test()))
__preempt_schedule();
}
# 現在のプリエンプションモデルの確認
$ grep PREEMPT /boot/config-$(uname -r)
CONFIG_PREEMPT_NONE=y
# CONFIG_PREEMPT_VOLUNTARY is not set
# CONFIG_PREEMPT is not set
# 実行時のプリエンプション状態確認
$ cat /sys/kernel/debug/sched/preempt
preempt_count: 0
割り込みコンテキスト
割り込みは現在実行中のコードを中断して発生するため、特別な注意が必要である。割り込みハンドラ内ではスリープする操作は許可されない。
/*
* 割り込みコンテキストの種類と制約
*
* ハード IRQ コンテキスト:
* - スリープ不可
* - スピンロックのみ使用可能
* - メモリ割り当ては GFP_ATOMIC のみ
* - 実行時間は最小限に
*
* ソフト IRQ コンテキスト:
* - スリープ不可
* - 同一 softirq は複数 CPU で同時実行可能
* - スピンロックのみ使用可能
*
* プロセスコンテキスト:
* - スリープ可能
* - 全てのロック機構が使用可能
* - ミューテックス、セマフォが推奨
*/
/* コンテキスト判定マクロ */
#define in_irq() (hardirq_count())
#define in_softirq() (softirq_count())
#define in_interrupt() (irq_count())
#define in_task() (!(in_interrupt()))
/* 割り込みハンドラの例 */
static irqreturn_t my_interrupt_handler(int irq, void *dev_id)
{
struct my_device *dev = dev_id;
unsigned long flags;
/* ハード IRQ コンテキスト - スリープ不可 */
spin_lock_irqsave(&dev->lock, flags);
/* デバイスレジスタの読み取り */
status = readl(dev->regs + STATUS_REG);
/* データの処理 */
if (status & DATA_READY) {
dev->data = readl(dev->regs + DATA_REG);
dev->data_available = true;
}
spin_unlock_irqrestore(&dev->lock, flags);
/* 下半分処理のスケジュール */
if (status & DATA_READY)
tasklet_schedule(&dev->tasklet);
return IRQ_HANDLED;
}
割り込み関連の情報確認:
# 割り込み統計の確認
$ cat /proc/interrupts
CPU0 CPU1 CPU2 CPU3
0: 42 0 0 0 IO-APIC 2-edge timer
1: 3 0 0 0 IO-APIC 1-edge i8042
8: 0 0 0 0 IO-APIC 8-edge rtc0
9: 0 0 0 0 IO-APIC 9-fasteoi acpi
16: 28374 0 0 0 IO-APIC 16-fasteoi ehci_hcd
24: 0 0 0 0 PCI-MSI 49152-edge nvme0q0
LOC: 1234567 1234568 1234569 1234570 Local timer interrupts
RES: 45678 45679 45680 45681 Rescheduling interrupts
# softirq 統計の確認
$ cat /proc/softirqs
CPU0 CPU1 CPU2 CPU3
HI: 0 0 0 0
TIMER: 234567 234568 234569 234570
NET_TX: 1234 1235 1236 1237
NET_RX: 56789 56790 56791 56792
BLOCK: 9012 9013 9014 9015
IRQ_POLL: 0 0 0 0
TASKLET: 345 346 347 348
SCHED: 123456 123457 123458 123459
HRTIMER: 0 0 0 0
RCU: 67890 67891 67892 67893
# IRQ アフィニティの確認と設定
$ cat /proc/irq/24/smp_affinity
0f
$ echo 01 > /proc/irq/24/smp_affinity # CPU 0 のみに固定
コンテキスト別ロック使用ガイド
以下の表は、各コンテキストで使用可能な同期プリミティブをまとめたものである:
+-------------------+----------+---------+----------+---------+--------+-----+
| コンテキスト | spinlock | mutex | semaphore| RCU read | atomic | pcpu|
+-------------------+----------+---------+----------+---------+--------+-----+
| ハード IRQ | Yes | No | No | Yes | Yes | Yes |
| ソフト IRQ | Yes | No | No | Yes | Yes | Yes |
| Tasklet | Yes | No | No | Yes | Yes | Yes |
| ワークキュー | Yes | Yes | Yes | Yes | Yes | Yes |
| プロセスコンテキスト| Yes | Yes | Yes | Yes | Yes | Yes |
+-------------------+----------+---------+----------+---------+--------+-----+
プリエンプションモデル
Linux カーネルは4つのプリエンプションモデルをサポートしており、ワークロードに応じて選択できる。
CONFIG_PREEMPT_NONE
サーバーやスループット重視のシステム向け。カーネルコードは明示的にスケジューラを呼び出すまでプリエンプトされない。
/* PREEMPT_NONE の動作 */
/*
* カーネルコードのプリエンプションポイント:
* - 明示的な schedule() 呼び出し
* - ユーザー空間への復帰時
* - ブロッキング操作 (wait_event, mutex_lock 等)
*
* メリット:
* - 最高のスループット
* - 最小のロックオーバーヘッド
*
* デメリット:
* - レイテンシが予測不能
* - リアルタイム処理には不向き
*/
# PREEMPT_NONE でのカーネルビルド
$ cd /usr/src/linux
$ make menuconfig
# General setup -> Preemption Model -> No Forced Preemption (Server)
# .config の内容
CONFIG_PREEMPT_NONE=y
# CONFIG_PREEMPT_VOLUNTARY is not set
# CONFIG_PREEMPT is not set
CONFIG_PREEMPT_VOLUNTARY
デスクトップシステム向け。カーネルコード中に明示的なプリエンプションポイント (might_sleep() 呼び出し) を追加する。
/* PREEMPT_VOLUNTARY の実装 */
#ifdef CONFIG_PREEMPT_VOLUNTARY
#define might_resched() _cond_resched()
#else
#define might_resched() do { } while (0)
#endif
/*
* cond_resched() - 自発的なプリエンプションポイント
* 長時間実行ループに挿入して、応答性を改善する
*/
int cond_resched(void)
{
if (should_resched(0)) {
preempt_schedule_common();
return 1;
}
return 0;
}
/* 使用例: 長時間ループ内 */
void long_running_function(void)
{
int i;
for (i = 0; i < 1000000; i++) {
/* 重い処理 */
process_item(i);
/* プリエンプションポイントの挿入 */
if (!(i % 100))
cond_resched();
}
}
CONFIG_PREEMPT (フルプリエンプション)
低レイテンシが必要なデスクトップ/組み込みシステム向け。スピンロック保持中を除き、カーネルコードのほぼ全ての箇所でプリエンプション可能。
/* CONFIG_PREEMPT の動作原理 */
/*
* プリエンプションが無効になるケース:
* 1. spin_lock() 保持中
* 2. preempt_disable() セクション内
* 3. 割り込み無効化中
* 4. RCU 読み取り側クリティカルセクション内
*
* プリエンプション可能な箇所:
* - spin_unlock() 直後
* - preempt_enable() 呼び出し時
* - 割り込みハンドラからの復帰時 (カーネルモード)
*/
/* preempt_enable() でのリスケジュールチェック */
#define preempt_enable() \
do { \
barrier(); \
if (unlikely(preempt_count_dec_and_test())) \
__preempt_schedule(); \
} while (0)
# CONFIG_PREEMPT でのビルド
CONFIG_PREEMPT=y
CONFIG_PREEMPTION=y
CONFIG_PREEMPT_COUNT=y
CONFIG_PREEMPT_DYNAMIC=y # 6.x 以降で動的切り替え可能
CONFIG_PREEMPT_RT (リアルタイムプリエンプション)
リアルタイムシステム向け。スピンロックをミューテックスに変換し、割り込みハンドラをカーネルスレッドとして実行する。詳細は後述のセクションで解説する。
# プリエンプションモデルの動的切り替え (6.x 以降)
$ cat /sys/kernel/debug/sched/preempt
$ echo full > /sys/kernel/debug/sched/preempt
# スケジューラレイテンシの確認
$ cat /proc/sys/kernel/sched_latency_ns
6000000
$ cat /proc/sys/kernel/sched_min_granularity_ns
750000
プリエンプションモデルの比較
┌──────────────────┬────────────┬──────────┬──────────────────┐
│ モデル │ レイテンシ │ スループ │ 用途 │
│ │ │ ット │ │
├──────────────────┼────────────┼──────────┼──────────────────┤
│ PREEMPT_NONE │ ~数十ms │ 最高 │ サーバー │
│ PREEMPT_VOLUNTARY│ ~数ms │ 高い │ デスクトップ │
│ PREEMPT (FULL) │ ~数百us │ やや低下 │ 低レイテンシ用途 │
│ PREEMPT_RT │ ~数十us │ 最も低下 │ リアルタイム │
└──────────────────┴────────────┴──────────┴──────────────────┘
アトミック操作
アトミック操作は、ロックを使用せずに単一の変数に対するスレッドセーフな操作を提供する。CPU のアトミック命令を直接利用するため、非常に高速である。
atomic_t (32ビット)
#include <linux/atomic.h>
/*
* atomic_t の定義
* アーキテクチャに依存するが、基本的には int のラッパー
*/
typedef struct {
int counter;
} atomic_t;
/* 初期化 */
atomic_t my_counter = ATOMIC_INIT(0);
/* または動的に */
atomic_t dynamic_counter;
atomic_set(&dynamic_counter, 42);
/* 基本操作 */
void atomic_examples(void)
{
atomic_t v = ATOMIC_INIT(0);
/* 値の読み取り */
int val = atomic_read(&v); /* val = 0 */
/* 値の設定 */
atomic_set(&v, 10); /* v = 10 */
/* 加算 */
atomic_add(5, &v); /* v = 15 */
/* 減算 */
atomic_sub(3, &v); /* v = 12 */
/* インクリメント */
atomic_inc(&v); /* v = 13 */
/* デクリメント */
atomic_dec(&v); /* v = 12 */
/* 加算して結果を返す */
int result = atomic_add_return(8, &v); /* v = 20, result = 20 */
/* デクリメントしてゼロかテスト */
bool is_zero = atomic_dec_and_test(&v); /* v = 19, is_zero = false */
/* 比較して交換 (CAS) */
int old = atomic_cmpxchg(&v, 19, 100); /* v = 100, old = 19 */
/* 交換 */
old = atomic_xchg(&v, 42); /* v = 42, old = 100 */
/* 負かテスト */
atomic_set(&v, -1);
bool neg = atomic_add_negative(0, &v); /* neg = true */
}
atomic64_t (64ビット)
#include <linux/atomic.h>
typedef struct {
s64 counter;
} atomic64_t;
/* 64ビットアトミック操作 */
void atomic64_examples(void)
{
atomic64_t v = ATOMIC64_INIT(0);
atomic64_set(&v, 0x100000000LL);
s64 val = atomic64_read(&v);
atomic64_add(0x200000000LL, &v);
atomic64_sub(0x100000000LL, &v);
atomic64_inc(&v);
atomic64_dec(&v);
s64 old = atomic64_cmpxchg(&v, val, 0);
}
ビットアトミック操作
#include <linux/bitops.h>
/*
* ビット単位のアトミック操作
* ロックフリーのフラグ管理に最適
*/
void bitop_examples(void)
{
unsigned long flags = 0;
/* ビットのセット */
set_bit(0, &flags); /* flags |= (1 << 0) */
set_bit(3, &flags); /* flags |= (1 << 3) */
/* ビットのクリア */
clear_bit(0, &flags); /* flags &= ~(1 << 0) */
/* ビットのテストとセット (旧値を返す) */
int old = test_and_set_bit(5, &flags); /* flags |= (1 << 5), old = 0 */
/* ビットのテストとクリア */
old = test_and_clear_bit(5, &flags); /* flags &= ~(1 << 5), old = 1 */
/* ビットの反転 */
change_bit(3, &flags); /* flags ^= (1 << 3) */
/* ビットのテスト (非アトミック - 読み取りのみ) */
bool is_set = test_bit(3, &flags);
}
/* 実用例: デバイスステートフラグ */
#define DEVICE_RUNNING 0
#define DEVICE_SUSPENDED 1
#define DEVICE_ERROR 2
struct my_device {
unsigned long state;
/* ... */
};
int start_device(struct my_device *dev)
{
/* すでに実行中ならエラー */
if (test_and_set_bit(DEVICE_RUNNING, &dev->state))
return -EBUSY;
clear_bit(DEVICE_SUSPENDED, &dev->state);
clear_bit(DEVICE_ERROR, &dev->state);
/* デバイスの起動処理 */
return 0;
}
void stop_device(struct my_device *dev)
{
clear_bit(DEVICE_RUNNING, &dev->state);
}
アトミック操作の x86 実装
/*
* x86 アーキテクチャでのアトミック操作の実装
* LOCK プレフィックスによりバスロックまたはキャッシュロックを実行
*/
/* arch/x86/include/asm/atomic.h より */
static __always_inline void arch_atomic_add(int i, atomic_t *v)
{
asm volatile(LOCK_PREFIX "addl %1,%0"
: "+m" (v->counter)
: "ir" (i)
: "memory");
}
static __always_inline int arch_atomic_cmpxchg(atomic_t *v, int old, int new)
{
return arch_cmpxchg(&v->counter, old, new);
}
/*
* LOCK プレフィックスの動作:
* - キャッシュラインがローカル CPU に排他的にある場合:
* キャッシュロック (キャッシュラインを排他状態に保持)
* - それ以外:
* バスロック (#LOCK シグナルをアサート)
*
* いずれの場合も、操作がアトミックであることを保証する
*/
atomic_t の refcount 利用パターン
/*
* 参照カウントパターン
* カーネル内で最も一般的なアトミック操作の使用方法
*/
struct my_object {
atomic_t refcount;
void *data;
/* ... */
};
struct my_object *my_object_alloc(void)
{
struct my_object *obj = kmalloc(sizeof(*obj), GFP_KERNEL);
if (!obj)
return NULL;
atomic_set(&obj->refcount, 1); /* 初期参照カウント = 1 */
return obj;
}
void my_object_get(struct my_object *obj)
{
atomic_inc(&obj->refcount);
}
void my_object_put(struct my_object *obj)
{
if (atomic_dec_and_test(&obj->refcount)) {
/* 参照カウントが 0 になった - オブジェクトを解放 */
kfree(obj->data);
kfree(obj);
}
}
/*
* 注意: Linux 4.11 以降では refcount_t の使用が推奨される
* refcount_t はオーバーフロー/アンダーフロー検出機能を持つ
*/
#include <linux/refcount.h>
struct safe_object {
refcount_t refcount;
void *data;
};
void safe_object_init(struct safe_object *obj)
{
refcount_set(&obj->refcount, 1);
}
void safe_object_get(struct safe_object *obj)
{
refcount_inc(&obj->refcount);
/* refcount が 0 から増加しようとすると WARN を発生 */
}
bool safe_object_put(struct safe_object *obj)
{
if (refcount_dec_and_test(&obj->refcount)) {
/* 解放処理 */
return true;
}
return false;
}
スピンロック
スピンロックは、カーネルで最も基本的なロック機構である。ロックを取得できない場合、タスクはスリープせずにビジーウェイト (スピン) する。短いクリティカルセクションに最適であり、割り込みコンテキストでも使用可能である。
基本的なスピンロック
#include <linux/spinlock.h>
/* 静的初期化 */
static DEFINE_SPINLOCK(my_lock);
/* 動的初期化 */
spinlock_t dynamic_lock;
spin_lock_init(&dynamic_lock);
/* 基本的な使用パターン */
void basic_spinlock_example(void)
{
spin_lock(&my_lock);
/* クリティカルセクション */
/* ここではスリープ操作は禁止 */
shared_data.field1 = value1;
shared_data.field2 = value2;
spin_unlock(&my_lock);
}
スピンロックの変種
/*
* spin_lock() - 基本形
* プリエンプションを無効化してロックを取得
* プロセスコンテキスト間の排他に使用
*/
spin_lock(&lock);
/* クリティカルセクション */
spin_unlock(&lock);
/*
* spin_lock_irq() / spin_unlock_irq()
* 割り込みとプリエンプションの両方を無効化
* 割り込みハンドラとプロセスコンテキスト間の排他
* 注意: 呼び出し前の割り込み状態を保存しない
*/
spin_lock_irq(&lock);
/* クリティカルセクション (割り込み無効) */
spin_unlock_irq(&lock);
/*
* spin_lock_irqsave() / spin_unlock_irqrestore()
* 割り込みフラグを保存してから割り込みを無効化
* 最も安全な形式 - 割り込みの状態が不明な場合に使用
* 推奨: ほとんどの場合、spin_lock_irq() より安全
*/
unsigned long flags;
spin_lock_irqsave(&lock, flags);
/* クリティカルセクション */
spin_unlock_irqrestore(&lock, flags);
/*
* spin_lock_bh() / spin_unlock_bh()
* ソフト IRQ (Bottom Half) を無効化
* ソフト IRQ とプロセスコンテキスト間の排他
* ネットワークコードで頻繁に使用
*/
spin_lock_bh(&lock);
/* クリティカルセクション */
spin_unlock_bh(&lock);
/*
* spin_trylock() - ノンブロッキング
* ロック取得を試み、失敗したら即座に 0 を返す
* デッドロック回避やパフォーマンス最適化に使用
*/
if (spin_trylock(&lock)) {
/* ロック取得成功 */
/* クリティカルセクション */
spin_unlock(&lock);
} else {
/* ロック取得失敗 - 代替処理 */
}
スピンロック使用時の注意事項
/*
* スピンロックのルール:
*
* 1. クリティカルセクションは短く保つ
* - スピン中は CPU リソースを消費する
* - 目安: 数百命令以内
*
* 2. スリープ操作は禁止
* - kmalloc(GFP_KERNEL) は不可 -> GFP_ATOMIC を使用
* - copy_from_user() / copy_to_user() は不可
* - mutex_lock() は不可
*
* 3. 再帰的ロックは禁止
* - 同じ CPU で同じロックを2回取得するとデッドロック
*
* 4. 割り込みハンドラで使用するロックは irqsave が必要
* - spin_lock() のみでは割り込み中にデッドロック
*/
/* 悪い例: スピンロック内でスリープ */
void bad_example(void)
{
spin_lock(&my_lock);
/* 危険! kmalloc(GFP_KERNEL) はスリープする可能性がある */
buffer = kmalloc(4096, GFP_KERNEL); /* BUG! */
spin_unlock(&my_lock);
}
/* 正しい例 */
void good_example(void)
{
/* スピンロック外でメモリを確保 */
buffer = kmalloc(4096, GFP_KERNEL);
if (!buffer)
return -ENOMEM;
spin_lock(&my_lock);
/* 短いクリティカルセクション */
dev->buffer = buffer;
dev->buffer_size = 4096;
spin_unlock(&my_lock);
}
/* または GFP_ATOMIC を使用 */
void alternative_example(void)
{
spin_lock(&my_lock);
buffer = kmalloc(4096, GFP_ATOMIC); /* スリープしない */
if (!buffer) {
spin_unlock(&my_lock);
return -ENOMEM;
}
/* ... */
spin_unlock(&my_lock);
}
スピンロックの実装 (x86)
/*
* Linux カーネルのスピンロックは queued spinlock (qspinlock) を使用
* ticket spinlock の改良版で、NUMA システムでのスケーラビリティが向上
*/
/* include/asm-generic/qspinlock_types.h */
typedef struct qspinlock {
union {
atomic_t val;
struct {
u8 locked;
u8 pending;
};
struct {
u16 locked_pending;
u16 tail;
};
};
} arch_spinlock_t;
/*
* qspinlock の動作:
*
* 1. ロック取得試行 (fast path):
* val が 0 なら、atomic に 1 にセットして取得成功
*
* 2. ロック競合時 (slow path):
* MCS キューに自分のノードを追加してスピン
* MCS ロック: 各 CPU が自分のローカルなメモリ位置でスピン
* -> キャッシュバウンスを削減
*
* 3. ロック解放:
* val を 0 にクリア
* キューの次のウェイターに通知
*/
/* スピンロック取得の fast path */
static __always_inline void queued_spin_lock(struct qspinlock *lock)
{
int val = 0;
if (likely(atomic_try_cmpxchg_acquire(&lock->val, &val, _Q_LOCKED_VAL)))
return;
queued_spin_lock_slowpath(lock, val);
}
実用的なスピンロック使用例
/* デバイスドライバでの使用例 */
struct my_network_device {
spinlock_t tx_lock; /* 送信キュー用ロック */
spinlock_t stats_lock; /* 統計情報用ロック */
struct sk_buff_head tx_queue;
struct net_device_stats stats;
unsigned long state;
};
/* 送信処理 - プロセスコンテキスト */
netdev_tx_t my_start_xmit(struct sk_buff *skb, struct net_device *dev)
{
struct my_network_device *priv = netdev_priv(dev);
spin_lock(&priv->tx_lock);
if (skb_queue_len(&priv->tx_queue) >= TX_QUEUE_MAX) {
spin_unlock(&priv->tx_lock);
return NETDEV_TX_BUSY;
}
skb_queue_tail(&priv->tx_queue, skb);
trigger_hardware_tx(priv);
spin_unlock(&priv->tx_lock);
return NETDEV_TX_OK;
}
/* 送信完了割り込みハンドラ */
static irqreturn_t tx_complete_irq(int irq, void *dev_id)
{
struct my_network_device *priv = dev_id;
unsigned long flags;
/* 割り込みコンテキストなので irqsave を使用 */
spin_lock_irqsave(&priv->tx_lock, flags);
/* 送信完了したパケットをキューから除去 */
struct sk_buff *skb = skb_dequeue(&priv->tx_queue);
spin_unlock_irqrestore(&priv->tx_lock, flags);
if (skb) {
/* 統計更新 */
spin_lock_irqsave(&priv->stats_lock, flags);
priv->stats.tx_packets++;
priv->stats.tx_bytes += skb->len;
spin_unlock_irqrestore(&priv->stats_lock, flags);
dev_kfree_skb_irq(skb);
}
return IRQ_HANDLED;
}
リーダー・ライタースピンロック
リーダー・ライタースピンロックは、読み取り操作が書き込み操作よりも圧倒的に多い場合に有効な同期機構である。複数の読み取り操作は同時に実行でき、書き込み操作は排他的アクセスを取得する。
基本的な使用方法
#include <linux/rwlock.h>
/* 静的初期化 */
static DEFINE_RWLOCK(my_rwlock);
/* 動的初期化 */
rwlock_t dynamic_rwlock;
rwlock_init(&dynamic_rwlock);
/* 読み取り操作 */
void read_operation(void)
{
read_lock(&my_rwlock);
/* 複数のリーダーが同時に実行可能 */
value = shared_data.field1;
other = shared_data.field2;
read_unlock(&my_rwlock);
}
/* 書き込み操作 */
void write_operation(void)
{
write_lock(&my_rwlock);
/* ライターは排他的アクセス */
/* 他のリーダー・ライターはブロックされる */
shared_data.field1 = new_value1;
shared_data.field2 = new_value2;
write_unlock(&my_rwlock);
}
変種
/* 割り込みセーフな変種 */
unsigned long flags;
/* 読み取り + 割り込み無効化 */
read_lock_irqsave(&my_rwlock, flags);
/* ... */
read_unlock_irqrestore(&my_rwlock, flags);
/* 書き込み + 割り込み無効化 */
write_lock_irqsave(&my_rwlock, flags);
/* ... */
write_unlock_irqrestore(&my_rwlock, flags);
/* ソフト IRQ 無効化版 */
read_lock_bh(&my_rwlock);
/* ... */
read_unlock_bh(&my_rwlock);
write_lock_bh(&my_rwlock);
/* ... */
write_unlock_bh(&my_rwlock);
/* trylock 版 */
if (read_trylock(&my_rwlock)) {
/* 読み取り処理 */
read_unlock(&my_rwlock);
}
if (write_trylock(&my_rwlock)) {
/* 書き込み処理 */
write_unlock(&my_rwlock);
}
rwlock の制限と注意事項
/*
* rwlock の問題点:
*
* 1. ライター飢餓 (Writer Starvation)
* - リーダーが常に存在すると、ライターがロックを取得できない
* - Linux の rwlock はライターに優先権を与えない
*
* 2. 再帰的な読み取りロック
* - 同一 CPU での再帰的な read_lock() は安全
* - ただし、write_lock が待機中の場合にデッドロックの可能性
*
* 3. パフォーマンス
* - SMP システムでは、read_lock もキャッシュラインの共有が必要
* - 高競合時のスケーラビリティは制限的
*
* 推奨: 多くの場合、RCU の使用を検討すべき
*/
/* rwlock vs RCU の使い分け */
/*
* rwlock を使うべきケース:
* - 書き込み頻度が高い
* - 読み取りクリティカルセクションが短い
* - 構造がシンプルな場合
*
* RCU を使うべきケース:
* - 読み取りが圧倒的に多い
* - 読み取りパフォーマンスが最優先
* - ポインタベースのデータ構造
*/
実用例: ルーティングテーブル
/* rwlock を使ったルーティングテーブルの例 */
struct route_entry {
__be32 dest;
__be32 gateway;
__be32 netmask;
int ifindex;
struct list_head list;
};
struct routing_table {
rwlock_t lock;
struct list_head routes;
int count;
};
static struct routing_table rt_table = {
.lock = __RW_LOCK_UNLOCKED(rt_table.lock),
.routes = LIST_HEAD_INIT(rt_table.routes),
.count = 0,
};
/* ルート検索 - 読み取りロック */
struct route_entry *route_lookup(__be32 dest)
{
struct route_entry *entry, *best = NULL;
read_lock(&rt_table.lock);
list_for_each_entry(entry, &rt_table.routes, list) {
if ((dest & entry->netmask) == entry->dest) {
best = entry;
break;
}
}
read_unlock(&rt_table.lock);
return best;
}
/* ルート追加 - 書き込みロック */
int route_add(struct route_entry *new_route)
{
write_lock(&rt_table.lock);
list_add_tail(&new_route->list, &rt_table.routes);
rt_table.count++;
write_unlock(&rt_table.lock);
return 0;
}
ミューテックス
ミューテックス (Mutual Exclusion) は、プロセスコンテキストで使用される最も一般的なスリーピングロックである。ロックを取得できない場合、タスクはスリープ状態に入り、ロックが利用可能になると起床する。
基本的な使用方法
#include <linux/mutex.h>
/* 静的初期化 */
static DEFINE_MUTEX(my_mutex);
/* 動的初期化 */
struct mutex dynamic_mutex;
mutex_init(&dynamic_mutex);
/* 基本的な使用パターン */
void mutex_example(void)
{
mutex_lock(&my_mutex);
/* クリティカルセクション */
/* スリープ操作が許可される */
buffer = kmalloc(4096, GFP_KERNEL);
if (copy_from_user(buffer, user_buf, count)) {
kfree(buffer);
mutex_unlock(&my_mutex);
return -EFAULT;
}
process_data(buffer);
mutex_unlock(&my_mutex);
}
ミューテックスの変種
/*
* mutex_lock() - ブロッキング取得
* ロックが取得できるまでスリープ
* シグナルでは中断されない (TASK_UNINTERRUPTIBLE)
*/
mutex_lock(&my_mutex);
/* ... */
mutex_unlock(&my_mutex);
/*
* mutex_lock_interruptible() - 割り込み可能な取得
* シグナルで中断可能 (TASK_INTERRUPTIBLE)
* 戻り値: 0 = 成功, -EINTR = シグナルにより中断
* ユーザー空間の要求を処理する場合に推奨
*/
if (mutex_lock_interruptible(&my_mutex)) {
return -EINTR; /* シグナルにより中断 */
}
/* ... */
mutex_unlock(&my_mutex);
/*
* mutex_lock_killable() - 致命的シグナルでのみ中断
* SIGKILL でのみ中断可能 (TASK_KILLABLE)
* SIGINT/SIGTERM では中断されない
*/
if (mutex_lock_killable(&my_mutex)) {
return -EINTR;
}
/* ... */
mutex_unlock(&my_mutex);
/*
* mutex_trylock() - ノンブロッキング取得
* 即座にロック取得を試みる
* 戻り値: 1 = 成功, 0 = 失敗
* 割り込みコンテキストでは使用不可 (スリープしないが制約がある)
*/
if (mutex_trylock(&my_mutex)) {
/* ロック取得成功 */
/* ... */
mutex_unlock(&my_mutex);
} else {
/* ロック取得失敗 */
}
ミューテックスの制約
/*
* ミューテックスのルール (spinlock との違い):
*
* 1. プロセスコンテキストでのみ使用可能
* - 割り込みコンテキストでは使用不可
* - softirq, tasklet からは使用不可
*
* 2. ロック保持者のみがアンロック可能
* - 他のタスクがアンロックすることは不正
*
* 3. 再帰的ロックは禁止
* - 同一タスクが2回 mutex_lock() するとデッドロック
*
* 4. ロック保持中にタスクが終了してはならない
*
* 5. ロックした状態でメモリ領域を解放してはならない
*
* 6. 再初期化は不正
* - mutex_init() をロック保持中に呼んではならない
*/
/* ミューテックス vs スピンロックの選択基準 */
/*
* ミューテックスを選ぶべきケース:
* - クリティカルセクションが長い
* - クリティカルセクション内でスリープが必要
* - プロセスコンテキストからのみアクセス
*
* スピンロックを選ぶべきケース:
* - クリティカルセクションが短い
* - 割り込みコンテキストからのアクセスが必要
* - スリープ不要
*/
ミューテックスの内部実装
/*
* Linux のミューテックスは最適化された実装を持つ:
*
* Fast path: アトミック操作でロック取得 (競合なし)
* Mid path: optimistic spinning (ロック保持者が実行中ならスピン)
* Slow path: ウェイトキューでスリープ
*/
struct mutex {
atomic_long_t owner; /* ロック所有者のタスク構造体ポインタ */
raw_spinlock_t wait_lock; /* ウェイトキュー保護用スピンロック */
struct optimistic_spin_queue osq; /* MCS ベースの最適化スピンキュー */
struct list_head wait_list; /* ウェイトキュー */
#ifdef CONFIG_DEBUG_MUTEXES
void *magic;
#endif
#ifdef CONFIG_DEBUG_LOCK_ALLOC
struct lockdep_map dep_map;
#endif
};
/*
* Optimistic Spinning (楽観的スピン):
*
* ミューテックスの保持者が現在 CPU 上で実行中の場合、
* スリープせずにスピンする方が効率的なことがある。
* これは、保持者がすぐにロックを解放する可能性が高いためである。
*
* 条件:
* - ロック保持者が CPU 上で実行中
* - 他にプリエンプションが必要なタスクがない
* - need_resched() == false
*/
実用例: ファイルシステムの inode 操作
/* ミューテックスを使った inode 操作の例 */
struct my_inode_info {
struct mutex data_mutex; /* データ領域保護 */
struct mutex truncate_mutex; /* ファイルサイズ変更保護 */
void *data;
size_t data_size;
struct inode vfs_inode;
};
/* ファイル読み取り */
ssize_t my_read(struct file *file, char __user *buf,
size_t count, loff_t *pos)
{
struct my_inode_info *info = MY_I(file_inode(file));
ssize_t ret;
/* 割り込み可能なミューテックスを使用 */
if (mutex_lock_interruptible(&info->data_mutex))
return -EINTR;
if (*pos >= info->data_size) {
ret = 0; /* EOF */
goto out;
}
count = min(count, info->data_size - (size_t)*pos);
/* copy_to_user はスリープ可能 - ミューテックス内で安全 */
if (copy_to_user(buf, info->data + *pos, count)) {
ret = -EFAULT;
goto out;
}
*pos += count;
ret = count;
out:
mutex_unlock(&info->data_mutex);
return ret;
}
/* ファイル書き込み */
ssize_t my_write(struct file *file, const char __user *buf,
size_t count, loff_t *pos)
{
struct my_inode_info *info = MY_I(file_inode(file));
ssize_t ret;
if (mutex_lock_interruptible(&info->data_mutex))
return -EINTR;
/* 必要に応じてバッファ拡張 */
if (*pos + count > info->data_size) {
void *new_data = krealloc(info->data, *pos + count, GFP_KERNEL);
if (!new_data) {
ret = -ENOMEM;
goto out;
}
info->data = new_data;
info->data_size = *pos + count;
}
if (copy_from_user(info->data + *pos, buf, count)) {
ret = -EFAULT;
goto out;
}
*pos += count;
ret = count;
out:
mutex_unlock(&info->data_mutex);
return ret;
}
セマフォ
セマフォは、カウンティングセマフォとして実装されており、指定した数のタスクが同時にリソースにアクセスすることを許可する。ミューテックスと異なり、異なるコンテキストでの取得と解放が可能である。
基本的な使用方法
#include <linux/semaphore.h>
/* セマフォの定義 */
struct semaphore {
raw_spinlock_t lock; /* ウェイトキュー保護用 */
unsigned int count; /* 利用可能なリソース数 */
struct list_head wait_list; /* 待機タスクリスト */
};
/* 静的初期化 */
static DEFINE_SEMAPHORE(my_sem); /* count = 1 (バイナリセマフォ) */
/* カウント指定の初期化 */
struct semaphore pool_sem;
sema_init(&pool_sem, 10); /* 最大10の同時アクセスを許可 */
/* セマフォの取得 */
void semaphore_example(void)
{
/* ブロッキング取得 */
down(&pool_sem);
/* リソースの使用 */
use_resource();
/* セマフォの解放 */
up(&pool_sem);
}
セマフォの変種
/*
* down() - 割り込み不可能な取得
* TASK_UNINTERRUPTIBLE でスリープ
*/
down(&sem);
/* ... */
up(&sem);
/*
* down_interruptible() - 割り込み可能な取得
* 戻り値: 0 = 成功, -EINTR = シグナルにより中断
*/
if (down_interruptible(&sem)) {
return -EINTR;
}
/* ... */
up(&sem);
/*
* down_killable() - 致命的シグナルでのみ中断
*/
if (down_killable(&sem)) {
return -EINTR;
}
/* ... */
up(&sem);
/*
* down_trylock() - ノンブロッキング取得
* 戻り値: 0 = 成功, 非0 = 失敗
*/
if (down_trylock(&sem) == 0) {
/* 取得成功 */
up(&sem);
}
/*
* down_timeout() - タイムアウト付き取得
* 指定時間内に取得できなければ -ETIME を返す
*/
if (down_timeout(&sem, msecs_to_jiffies(1000))) {
/* 1秒以内に取得できなかった */
return -ETIMEDOUT;
}
/* ... */
up(&sem);
セマフォ vs ミューテックス
/*
* ミューテックスを使うべきケース (ほとんどの場合):
* - バイナリ排他制御 (count = 1)
* - 所有者の概念が必要
* - パフォーマンスが重要 (optimistic spinning)
* - lockdep によるデバッグ支援が必要
*
* セマフォを使うべきケース:
* - カウンティングセマフォが必要 (count > 1)
* - 取得者と解放者が異なるコンテキスト
* - レガシーコードとの互換性
*
* 注意: 新しいコードではセマフォの使用は推奨されない
* ミューテックス + 完了変数で代替可能なケースが多い
*/
/* カウンティングセマフォの実用例: 接続プール */
#define MAX_CONNECTIONS 10
struct connection_pool {
struct semaphore available;
struct mutex pool_lock;
struct connection *connections[MAX_CONNECTIONS];
bool in_use[MAX_CONNECTIONS];
};
void pool_init(struct connection_pool *pool)
{
sema_init(&pool->available, MAX_CONNECTIONS);
mutex_init(&pool->pool_lock);
for (int i = 0; i < MAX_CONNECTIONS; i++) {
pool->connections[i] = create_connection();
pool->in_use[i] = false;
}
}
struct connection *pool_acquire(struct connection_pool *pool)
{
struct connection *conn = NULL;
int i;
/* セマフォで空きスロットを待つ */
if (down_interruptible(&pool->available))
return ERR_PTR(-EINTR);
/* プール内の空きコネクションを探す */
mutex_lock(&pool->pool_lock);
for (i = 0; i < MAX_CONNECTIONS; i++) {
if (!pool->in_use[i]) {
pool->in_use[i] = true;
conn = pool->connections[i];
break;
}
}
mutex_unlock(&pool->pool_lock);
return conn;
}
void pool_release(struct connection_pool *pool, struct connection *conn)
{
int i;
mutex_lock(&pool->pool_lock);
for (i = 0; i < MAX_CONNECTIONS; i++) {
if (pool->connections[i] == conn) {
pool->in_use[i] = false;
break;
}
}
mutex_unlock(&pool->pool_lock);
/* セマフォをインクリメント */
up(&pool->available);
}
Read-Copy-Update (RCU)
RCU (Read-Copy-Update) は、Linux カーネルにおける最も重要かつ革新的な同期メカニズムの一つである。読み取り側のオーバーヘッドをほぼゼロにしながら、書き込み側が安全にデータを更新できる仕組みを提供する。
RCU の基本概念
RCU の3つの基本操作:
┌──────────────────────────────────────────────────────────────┐
│ │
│ 1. rcu_read_lock() / rcu_read_unlock() │
│ 読み取り側クリティカルセクションの境界を宣言 │
│ 実質的にはプリエンプション無効化/有効化のみ │
│ オーバーヘッド: ほぼゼロ │
│ │
│ 2. rcu_dereference() │
│ RCU 保護されたポインタの読み取り │
│ 必要なメモリバリアを挿入 │
│ │
│ 3. synchronize_rcu() / call_rcu() │
│ 全ての既存の読み取り側クリティカルセクションの完了を待つ │
│ (Grace Period) │
│ │
│ + rcu_assign_pointer() │
│ RCU 保護されたポインタの更新 │
│ 必要なメモリバリアを挿入 │
│ │
└──────────────────────────────────────────────────────────────┘
RCU の基本的な使用方法
#include <linux/rcupdate.h>
#include <linux/rculist.h>
/* RCU 保護されたデータ構造 */
struct my_data {
int value;
char *name;
struct rcu_head rcu; /* RCU コールバック用 */
};
static struct my_data __rcu *global_data;
/* 読み取り操作 - 極めて高速 */
void rcu_read_example(void)
{
struct my_data *data;
rcu_read_lock(); /* プリエンプション無効化のみ */
data = rcu_dereference(global_data);
if (data) {
pr_info("value = %d, name = %s\n", data->value, data->name);
}
rcu_read_unlock();
/* ここ以降は data ポインタを使用してはならない */
}
/* 更新操作 - Copy + Update + Replace */
void rcu_update_example(int new_value, const char *new_name)
{
struct my_data *new_data, *old_data;
/* 1. 新しいデータのコピーを作成 */
new_data = kmalloc(sizeof(*new_data), GFP_KERNEL);
if (!new_data)
return;
new_data->value = new_value;
new_data->name = kstrdup(new_name, GFP_KERNEL);
/* 2. ポインタをアトミックに更新 */
old_data = rcu_dereference_protected(global_data,
lockdep_is_held(&update_mutex));
rcu_assign_pointer(global_data, new_data);
/* 3. 古いデータの解放 (Grace Period 後) */
synchronize_rcu(); /* 全 CPU の RCU 読み取り完了を待つ */
kfree(old_data->name);
kfree(old_data);
}
/* 非同期的な古いデータの解放 */
static void rcu_free_callback(struct rcu_head *head)
{
struct my_data *data = container_of(head, struct my_data, rcu);
kfree(data->name);
kfree(data);
}
void rcu_update_async(int new_value, const char *new_name)
{
struct my_data *new_data, *old_data;
new_data = kmalloc(sizeof(*new_data), GFP_KERNEL);
new_data->value = new_value;
new_data->name = kstrdup(new_name, GFP_KERNEL);
old_data = rcu_dereference_protected(global_data,
lockdep_is_held(&update_mutex));
rcu_assign_pointer(global_data, new_data);
/* Grace Period 後にコールバックを呼び出す */
call_rcu(&old_data->rcu, rcu_free_callback);
/* ここですぐに戻る - 解放は後で非同期に実行される */
}
RCU リストの操作
#include <linux/rculist.h>
struct my_entry {
int id;
char data[64];
struct list_head list;
struct rcu_head rcu;
};
static LIST_HEAD(my_list);
static DEFINE_MUTEX(list_mutex); /* 更新側の排他用 */
/* リスト検索 (RCU 読み取り側) */
struct my_entry *find_entry_rcu(int id)
{
struct my_entry *entry;
rcu_read_lock();
list_for_each_entry_rcu(entry, &my_list, list) {
if (entry->id == id) {
rcu_read_unlock();
return entry; /* 注意: RCU 保護外でのポインタ使用は危険 */
}
}
rcu_read_unlock();
return NULL;
}
/* より安全なパターン: RCU 読み取り内で処理を完結 */
int read_entry_data(int id, char *buf, size_t bufsize)
{
struct my_entry *entry;
int ret = -ENOENT;
rcu_read_lock();
list_for_each_entry_rcu(entry, &my_list, list) {
if (entry->id == id) {
strscpy(buf, entry->data, bufsize);
ret = 0;
break;
}
}
rcu_read_unlock();
return ret;
}
/* リストへの追加 (更新側) */
int add_entry(int id, const char *data)
{
struct my_entry *entry;
entry = kmalloc(sizeof(*entry), GFP_KERNEL);
if (!entry)
return -ENOMEM;
entry->id = id;
strscpy(entry->data, data, sizeof(entry->data));
mutex_lock(&list_mutex);
list_add_rcu(&entry->list, &my_list);
mutex_unlock(&list_mutex);
return 0;
}
/* リストからの削除 */
int remove_entry(int id)
{
struct my_entry *entry, *tmp;
int ret = -ENOENT;
mutex_lock(&list_mutex);
list_for_each_entry_safe(entry, tmp, &my_list, list) {
if (entry->id == id) {
list_del_rcu(&entry->list);
mutex_unlock(&list_mutex);
/* Grace Period を待ってから解放 */
synchronize_rcu();
kfree(entry);
return 0;
}
}
mutex_unlock(&list_mutex);
return ret;
}
/* 非同期削除 */
static void entry_free_rcu(struct rcu_head *head)
{
struct my_entry *entry = container_of(head, struct my_entry, rcu);
kfree(entry);
}
int remove_entry_async(int id)
{
struct my_entry *entry, *tmp;
mutex_lock(&list_mutex);
list_for_each_entry_safe(entry, tmp, &my_list, list) {
if (entry->id == id) {
list_del_rcu(&entry->list);
call_rcu(&entry->rcu, entry_free_rcu);
mutex_unlock(&list_mutex);
return 0;
}
}
mutex_unlock(&list_mutex);
return -ENOENT;
}
Grace Period の仕組み
Grace Period の概念:
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
時間 ─────────────────────────────────────────────────────>
CPU 0: ──[RCU read]──────────| 完了
CPU 1: ────────[RCU read]────|────── 完了
CPU 2: ──────────────────────|────[RCU read]── 新しいデータを読む
CPU 3: ────[RCU read]────────| 完了
▲ ▲
ポインタ更新 Grace Period 完了
rcu_assign_pointer synchronize_rcu 復帰
|<- Grace Period ->|
全ての「既存の」RCU 読み取りが完了するまで待つ
Grace Period 開始後に始まった読み取りは
新しいデータを見るので待つ必要がない
注: 各 CPU が Quiescent State (静止状態) を通過すると、
その CPU の RCU 読み取りは完了したとみなされる。
Quiescent State: コンテキストスイッチ、ユーザー空間復帰、
アイドル状態など
/*
* RCU Grace Period の実装概要
*
* Tree RCU (CONFIG_TREE_RCU):
* - 大規模 SMP システム向け
* - rcu_node のツリー構造で管理
* - 各 CPU が Quiescent State を報告
* - リーフからルートへ伝播
*
* Grace Period の検出:
* 1. GP 開始: gp_seq をインクリメント
* 2. 各 CPU: Quiescent State を rcu_node に報告
* 3. 全 CPU 報告完了: GP 完了
* 4. コールバック実行
*/
/* Grace Period 関連のカーネルパラメータ */
/*
* rcutree.jiffies_till_first_fqs : 最初の強制 QS チェックまでの jiffies
* rcutree.jiffies_till_next_fqs : 以降の強制 QS チェック間隔
* rcutree.rcu_kick_kthreads : スタックした GP をキックするか
* rcutree.gp_preinit_delay : GP 初期化前の遅延
*/
# RCU の統計情報確認
$ cat /sys/kernel/debug/rcu/rcu_preempt/rcudata
0 c=74321 g=74322 cnq=1/0:0 dt=12501/140000000000000/0 df=2001
1 c=74321 g=74322 cnq=1/0:0 dt=12502/140000000000000/0 df=2002
# RCU Grace Period の統計
$ cat /sys/kernel/debug/rcu/rcu_preempt/rcudata
# c: completed grace periods
# g: started grace periods
# cnq: callbacks not yet ready / callbacks ready
# RCU コールバック数の確認
$ cat /sys/kernel/debug/rcu/rcu_preempt/rcudata | awk '{print $1, $4}'
# RCU ストール警告のタイムアウト設定
$ cat /sys/module/rcupdate/parameters/rcu_cpu_stall_timeout
21
# RCU ストール警告の抑制解除
$ echo 0 > /sys/module/rcupdate/parameters/rcu_cpu_stall_suppress
RCU の変種
Linux カーネルは、異なるユースケースに対応するために複数の RCU 変種を提供している。
SRCU (Sleepable RCU)
#include <linux/srcu.h>
/*
* SRCU: RCU 読み取り側クリティカルセクション内でスリープ可能
*
* 通常の RCU ではプリエンプション無効化により
* スリープが禁止されるが、SRCU ではスリープ可能
*
* トレードオフ:
* - 読み取りオーバーヘッドが通常の RCU より大きい
* - Grace Period の検出が遅くなる可能性
* - ドメインごとに srcu_struct が必要
*/
/* SRCU ドメインの定義 */
DEFINE_SRCU(my_srcu);
/* または動的初期化 */
struct srcu_struct dynamic_srcu;
init_srcu_struct(&dynamic_srcu);
/* SRCU 読み取り側 */
void srcu_read_example(void)
{
int idx;
struct my_data *data;
idx = srcu_read_lock(&my_srcu);
data = srcu_dereference(global_data, &my_srcu);
if (data) {
/* スリープ可能! */
msleep(100);
process_data(data);
}
srcu_read_unlock(&my_srcu, idx);
}
/* SRCU 更新側 */
void srcu_update_example(void)
{
struct my_data *old_data;
/* データの更新 */
old_data = rcu_dereference_protected(global_data,
lockdep_is_held(&update_lock));
rcu_assign_pointer(global_data, new_data);
/* SRCU Grace Period の完了を待つ */
synchronize_srcu(&my_srcu);
kfree(old_data);
}
/* 非同期 SRCU */
void srcu_update_async(void)
{
/* ... ポインタ更新 ... */
call_srcu(&my_srcu, &old_data->rcu, srcu_free_callback);
}
/* クリーンアップ */
void cleanup(void)
{
cleanup_srcu_struct(&dynamic_srcu);
}
Tasks RCU
/*
* Tasks RCU: トランポリンやBPFプログラムの更新に使用
*
* Quiescent State = 自発的なコンテキストスイッチ
* プリエンプションによるコンテキストスイッチは含まない
*
* 主な用途:
* - トランポリンコード (ftrace, kprobes)
* - BPF プログラムの置き換え
*/
/* Tasks RCU の使用 */
void tasks_rcu_example(void)
{
/* 古いトランポリンを新しいものに置き換え */
rcu_assign_pointer(trampoline_ptr, new_trampoline);
/* 全てのタスクが古いトランポリンから抜けるのを待つ */
synchronize_rcu_tasks();
/* 古いトランポリンを解放 */
free_trampoline(old_trampoline);
}
/*
* Tasks Trace RCU:
* BPF プログラムのスリープ可能なバリアント用
* CONFIG_TASKS_TRACE_RCU
*/
synchronize_rcu_tasks_trace();
/*
* Tasks Rude RCU:
* 全 CPU で強制的に IPI を送信して
* 即座に Grace Period を完了させる
*/
synchronize_rcu_tasks_rude();
RCU フレーバーの比較
┌────────────────────┬───────────────┬────────────┬──────────────────┐
│ RCU フレーバー │ 読み取り側で │ Grace │ 主な用途 │
│ │ スリープ可能 │ Period │ │
├────────────────────┼───────────────┼────────────┼──────────────────┤
│ Classic RCU │ No │ 高速 │ 一般的なデータ構造│
│ SRCU │ Yes │ やや遅い │ I/O待ち等が必要 │
│ Tasks RCU │ Yes (自発的) │ 遅い可能性 │ トランポリン │
│ Tasks Trace RCU │ Yes │ 中程度 │ BPF プログラム │
│ Tasks Rude RCU │ N/A │ 即座 (IPI) │ 緊急更新 │
└────────────────────┴───────────────┴────────────┴──────────────────┘
シーケンシャルロック
シーケンシャルロック (seqlock) は、書き込みが稀で読み取りが頻繁なデータに対する軽量な同期メカニズムである。読み取り側はロックを取得せず、代わりにシーケンス番号を使って一貫性を検証する。
基本的な使用方法
#include <linux/seqlock.h>
/* seqlock の初期化 */
static DEFINE_SEQLOCK(my_seqlock);
/* または */
seqlock_t dynamic_seqlock;
seqlock_init(&dynamic_seqlock);
/* 読み取り操作 */
void seqlock_read_example(void)
{
unsigned int seq;
int val1, val2;
do {
seq = read_seqbegin(&my_seqlock);
/* データの読み取り (ロックなし) */
val1 = shared_data.field1;
val2 = shared_data.field2;
} while (read_seqretry(&my_seqlock, seq));
/* シーケンス番号が変わっていたらリトライ */
/* ここで val1, val2 は一貫した値 */
use_values(val1, val2);
}
/* 書き込み操作 */
void seqlock_write_example(void)
{
write_seqlock(&my_seqlock);
/* シーケンス番号がインクリメントされる (奇数 = 書き込み中) */
shared_data.field1 = new_value1;
shared_data.field2 = new_value2;
write_sequnlock(&my_seqlock);
/* シーケンス番号が再度インクリメントされる (偶数 = 安定) */
}
seqlock の動作原理
seqlock のシーケンス番号:
sequence = 0 (偶数 = 安定状態)
Reader: Writer:
seq = read_seqbegin()
-> seq = 0 write_seqlock()
read field1 -> sequence = 1 (奇数 = 書き込み中)
read field2 write field1
write field2
write_sequnlock()
-> sequence = 2
read_seqretry(seq=0)
-> sequence(2) != seq(0) → リトライ!
seq = read_seqbegin()
-> seq = 2
read field1 (新しい値)
read field2 (新しい値)
read_seqretry(seq=2)
-> sequence(2) == seq(2) → OK!
seqlock の変種
/* 割り込みセーフ版 */
unsigned long flags;
write_seqlock_irqsave(&my_seqlock, flags);
/* 書き込み処理 */
write_sequnlock_irqrestore(&my_seqlock, flags);
write_seqlock_bh(&my_seqlock);
/* 書き込み処理 */
write_sequnlock_bh(&my_seqlock);
/* seqcount - 書き込み側のロックを別途管理する場合 */
seqcount_t my_seqcount;
seqcount_init(&my_seqcount);
/* 読み取り側 */
unsigned int seq;
do {
seq = read_seqcount_begin(&my_seqcount);
/* ... */
} while (read_seqcount_retry(&my_seqcount, seq));
/* 書き込み側 (外部ロックで保護済みが前提) */
spin_lock(&external_lock);
write_seqcount_begin(&my_seqcount);
/* ... */
write_seqcount_end(&my_seqcount);
spin_unlock(&external_lock);
/* seqcount with associated lock (6.x) */
seqcount_spinlock_t sc;
seqcount_spinlock_init(&sc, &my_spinlock);
seqcount_mutex_t scm;
seqcount_mutex_init(&scm, &my_mutex);
実用例: システム時刻
/*
* Linux カーネルにおける最も有名な seqlock の使用例は
* システム時刻の管理である (jiffies, xtime)
*
* kernel/time/timekeeping.c
*/
/* 時刻データ構造 (簡略化) */
struct timekeeper {
struct clocksource *clock;
u64 cycle_last;
u64 xtime_sec;
u64 xtime_nsec;
/* ... */
};
static struct {
seqcount_raw_spinlock_t seq;
struct timekeeper timekeeper;
} tk_core;
/* 時刻の読み取り */
void ktime_get_ts64(struct timespec64 *ts)
{
struct timekeeper *tk = &tk_core.timekeeper;
unsigned int seq;
do {
seq = read_seqcount_begin(&tk_core.seq);
ts->tv_sec = tk->xtime_sec;
ts->tv_nsec = timekeeping_get_ns(tk);
} while (read_seqcount_retry(&tk_core.seq, seq));
}
/* 時刻の更新 (タイマー割り込みから) */
void timekeeping_update(struct timekeeper *tk)
{
raw_spin_lock(&timekeeper_lock);
write_seqcount_begin(&tk_core.seq);
tk->xtime_sec = new_sec;
tk->xtime_nsec = new_nsec;
write_seqcount_end(&tk_core.seq);
raw_spin_unlock(&timekeeper_lock);
}
完了変数
完了変数 (Completion Variables) は、あるタスクが別のタスクの処理完了を待つための同期メカニズムである。生産者-消費者パターンやイベント通知に使用される。
基本的な使用方法
#include <linux/completion.h>
/* 静的初期化 */
static DECLARE_COMPLETION(my_completion);
/* 動的初期化 */
struct completion dynamic_completion;
init_completion(&dynamic_completion);
/* 待機側 */
void waiter_function(void)
{
pr_info("Waiting for completion...\n");
/* 完了を待つ */
wait_for_completion(&my_completion);
pr_info("Completed!\n");
}
/* 通知側 */
void completer_function(void)
{
/* 何らかの処理 */
do_work();
/* 待機中のタスクを起床 */
complete(&my_completion);
}
完了変数の変種
/*
* wait_for_completion() - 無条件待機
* 完了するまで TASK_UNINTERRUPTIBLE でスリープ
*/
wait_for_completion(&comp);
/*
* wait_for_completion_interruptible() - シグナルで中断可能
* 戻り値: 0 = 完了, -ERESTARTSYS = 中断
*/
ret = wait_for_completion_interruptible(&comp);
/*
* wait_for_completion_timeout() - タイムアウト付き
* 戻り値: 0 = タイムアウト, >0 = 残りジフィーズ
*/
unsigned long remaining;
remaining = wait_for_completion_timeout(&comp, msecs_to_jiffies(5000));
if (!remaining) {
pr_err("Timed out!\n");
}
/*
* wait_for_completion_interruptible_timeout() - 両方
*/
long ret;
ret = wait_for_completion_interruptible_timeout(&comp,
msecs_to_jiffies(5000));
if (ret == 0)
pr_err("Timeout\n");
else if (ret < 0)
pr_err("Interrupted\n");
/*
* try_wait_for_completion() - ノンブロッキング
* 戻り値: true = 完了済み, false = 未完了
*/
if (try_wait_for_completion(&comp)) {
/* 完了済み */
}
/*
* complete_all() - 全ての待機者を起床
*/
complete_all(&comp);
/*
* reinit_completion() - 再利用のために初期化
*/
reinit_completion(&comp);
実用例: デバイスドライバの初期化
/* 非同期デバイス初期化の例 */
struct my_device {
struct completion firmware_loaded;
struct completion init_done;
const struct firmware *fw;
int init_result;
};
/* ファームウェアロードコールバック */
static void firmware_cb(const struct firmware *fw, void *context)
{
struct my_device *dev = context;
dev->fw = fw;
complete(&dev->firmware_loaded);
}
/* 初期化スレッド */
static int init_thread(void *data)
{
struct my_device *dev = data;
int ret;
/* 非同期ファームウェアロードを要求 */
ret = request_firmware_nowait(THIS_MODULE, true,
"my_firmware.bin",
&dev->pdev->dev, GFP_KERNEL,
dev, firmware_cb);
if (ret) {
dev->init_result = ret;
complete(&dev->init_done);
return ret;
}
/* ファームウェアのロード完了を待つ (タイムアウト付き) */
if (!wait_for_completion_timeout(&dev->firmware_loaded,
msecs_to_jiffies(30000))) {
dev->init_result = -ETIMEDOUT;
complete(&dev->init_done);
return -ETIMEDOUT;
}
/* ファームウェアをデバイスに書き込み */
ret = upload_firmware(dev, dev->fw);
release_firmware(dev->fw);
dev->init_result = ret;
complete(&dev->init_done);
return ret;
}
/* probe 関数 */
int my_device_probe(struct platform_device *pdev)
{
struct my_device *dev;
dev = devm_kzalloc(&pdev->dev, sizeof(*dev), GFP_KERNEL);
init_completion(&dev->firmware_loaded);
init_completion(&dev->init_done);
/* バックグラウンドで初期化開始 */
kthread_run(init_thread, dev, "my_dev_init");
/* 初期化完了を待つ */
wait_for_completion(&dev->init_done);
return dev->init_result;
}
Per-CPU 変数
Per-CPU 変数は、CPU ごとに独立したコピーを持つ変数である。ロックなしでアクセスできるため、高い並列性を実現する。
基本的な使用方法
#include <linux/percpu.h>
/* 静的 Per-CPU 変数の定義 */
static DEFINE_PER_CPU(int, my_counter);
static DEFINE_PER_CPU(struct statistics, cpu_stats);
/* 動的 Per-CPU 変数の割り当て */
int __percpu *dynamic_counter;
dynamic_counter = alloc_percpu(int);
if (!dynamic_counter)
return -ENOMEM;
/* Per-CPU 変数へのアクセス */
void percpu_example(void)
{
int cpu;
/* 方法1: get_cpu()/put_cpu() - プリエンプション無効化 */
cpu = get_cpu(); /* プリエンプション無効化 & CPU 番号取得 */
per_cpu(my_counter, cpu)++;
per_cpu(cpu_stats, cpu).packets++;
put_cpu(); /* プリエンプション有効化 */
/* 方法2: this_cpu_* 操作 - より効率的 */
this_cpu_inc(my_counter);
this_cpu_add(my_counter, 10);
int val = this_cpu_read(my_counter);
/* 方法3: preempt_disable/enable を明示的に */
preempt_disable();
__this_cpu_inc(my_counter); /* プリエンプション無効が前提 */
preempt_enable();
}
/* 全 CPU の合計を取得 */
unsigned long get_total_counter(void)
{
unsigned long total = 0;
int cpu;
for_each_possible_cpu(cpu) {
total += per_cpu(my_counter, cpu);
}
return total;
}
/* 動的 Per-CPU 変数 */
void dynamic_percpu_example(void)
{
int __percpu *counter = alloc_percpu(int);
if (!counter)
return;
/* アクセス */
this_cpu_inc(*counter);
int val = *this_cpu_ptr(counter);
/* 特定 CPU の値を取得 */
val = *per_cpu_ptr(counter, 0); /* CPU 0 の値 */
/* 解放 */
free_percpu(counter);
}
Per-CPU 変数の this_cpu 操作
/*
* this_cpu_* 操作: 単一命令で Per-CPU 変数を操作
* x86 では %gs セグメントレジスタを使用して高速アクセス
*/
/* 読み取り */
int val = this_cpu_read(my_counter);
/* 書き込み */
this_cpu_write(my_counter, 42);
/* 加算 */
this_cpu_add(my_counter, 10);
/* 減算 */
this_cpu_sub(my_counter, 5);
/* インクリメント/デクリメント */
this_cpu_inc(my_counter);
this_cpu_dec(my_counter);
/* AND/OR/XOR */
this_cpu_and(my_flags, ~FLAG_MASK);
this_cpu_or(my_flags, FLAG_MASK);
/* 比較して交換 */
int old = this_cpu_cmpxchg(my_counter, expected, new_val);
/* 交換 */
old = this_cpu_xchg(my_counter, new_val);
/*
* __ プレフィックス版: プリエンプション無効化が保証されている場合
* プリエンプションチェックを省略するため若干速い
*/
preempt_disable();
__this_cpu_inc(my_counter);
__this_cpu_add(my_counter, 10);
preempt_enable();
実用例: ネットワーク統計
/* Per-CPU を使ったネットワークデバイス統計 */
struct pcpu_net_stats {
u64 rx_packets;
u64 rx_bytes;
u64 tx_packets;
u64 tx_bytes;
struct u64_stats_sync syncp; /* 32ビット環境での 64ビット一貫性 */
};
struct my_net_device {
struct net_device *netdev;
struct pcpu_net_stats __percpu *stats;
};
/* デバイス初期化 */
int my_net_init(struct my_net_device *dev)
{
dev->stats = alloc_percpu(struct pcpu_net_stats);
if (!dev->stats)
return -ENOMEM;
return 0;
}
/* パケット受信時 */
void my_net_rx(struct my_net_device *dev, struct sk_buff *skb)
{
struct pcpu_net_stats *stats = this_cpu_ptr(dev->stats);
u64_stats_update_begin(&stats->syncp);
stats->rx_packets++;
stats->rx_bytes += skb->len;
u64_stats_update_end(&stats->syncp);
}
/* 統計取得 */
void my_net_get_stats64(struct net_device *netdev,
struct rtnl_link_stats64 *tot)
{
struct my_net_device *dev = netdev_priv(netdev);
int cpu;
memset(tot, 0, sizeof(*tot));
for_each_possible_cpu(cpu) {
struct pcpu_net_stats *stats = per_cpu_ptr(dev->stats, cpu);
u64 rx_packets, rx_bytes, tx_packets, tx_bytes;
unsigned int start;
do {
start = u64_stats_fetch_begin(&stats->syncp);
rx_packets = stats->rx_packets;
rx_bytes = stats->rx_bytes;
tx_packets = stats->tx_packets;
tx_bytes = stats->tx_bytes;
} while (u64_stats_fetch_retry(&stats->syncp, start));
tot->rx_packets += rx_packets;
tot->rx_bytes += rx_bytes;
tot->tx_packets += tx_packets;
tot->tx_bytes += tx_bytes;
}
}
メモリバリア
メモリバリア (Memory Barriers) は、CPU とコンパイラによるメモリアクセスの並べ替えを制御するための命令である。マルチプロセッサシステムにおいてデータの可視性と順序を保証するために不可欠である。
なぜメモリバリアが必要か
問題: CPU とコンパイラはパフォーマンス最適化のため
メモリアクセスを並べ替える
CPU 0: CPU 1:
a = 1; while (flag == 0) ;
flag = 1; assert(a == 1); /* 失敗する可能性! */
CPU 0 の最適化により flag の書き込みが a より先に
他の CPU から見える可能性がある:
CPU 0 (実際の実行順): CPU 1:
flag = 1; ← 先に見える flag == 1 を検出
a = 1; ← まだ見えない a を読む → 0! (assert 失敗)
解決策:
CPU 0: CPU 1:
a = 1; while (flag == 0) ;
smp_wmb(); ← 書き込み順序保証 smp_rmb(); ← 読み取り順序保証
flag = 1; assert(a == 1); /* 常に成功 */
Linux カーネルのメモリバリア
#include <asm/barrier.h>
/*
* コンパイラバリア
* コンパイラの最適化による並べ替えを防止
* CPU レベルの並べ替えには効果なし
*/
barrier(); /* asm volatile("" ::: "memory"); */
/*
* 汎用メモリバリア
* 読み書き両方の順序を保証
* SMP/UP 共通
*/
mb(); /* フルメモリバリア */
rmb(); /* 読み取りメモリバリア */
wmb(); /* 書き込みメモリバリア */
/*
* SMP メモリバリア
* SMP カーネルでのみ有効 (UP では nop)
*/
smp_mb(); /* SMP フルメモリバリア */
smp_rmb(); /* SMP 読み取りメモリバリア */
smp_wmb(); /* SMP 書き込みメモリバリア */
/*
* I/O メモリバリア
* デバイスレジスタアクセスの順序保証
*/
dma_mb(); /* DMA メモリバリア */
dma_rmb(); /* DMA 読み取りメモリバリア */
dma_wmb(); /* DMA 書き込みメモリバリア */
/*
* READ_ONCE / WRITE_ONCE
* 単一変数への最適化を防止
* ティアリング (部分的読み書き) を防止
*/
int val = READ_ONCE(shared_var);
WRITE_ONCE(shared_var, new_val);
/*
* smp_store_release / smp_load_acquire
* ペアで使用する順序付きアクセス
* より軽量なメモリバリア
*/
/* 生産者 */
WRITE_ONCE(data, new_data);
smp_store_release(&flag, 1); /* data の書き込み完了後に flag をセット */
/* 消費者 */
if (smp_load_acquire(&flag)) { /* flag を先に読む */
use(READ_ONCE(data)); /* data は必ず新しい値 */
}
メモリバリアの x86 実装
/*
* x86 はストア順序を保証する (TSO: Total Store Order)
* そのため、一部のバリアは軽量
*/
/* x86 での実装 */
#define mb() asm volatile("mfence" ::: "memory")
#define rmb() asm volatile("lfence" ::: "memory")
#define wmb() asm volatile("sfence" ::: "memory")
/* x86 の TSO により smp_wmb() はコンパイラバリアで十分 */
#define smp_wmb() barrier()
#define smp_rmb() barrier()
/* しかし smp_mb() は完全なバリアが必要 */
#define smp_mb() asm volatile("lock; addl $0,-4(%%rsp)" ::: "memory", "cc")
/*
* ARM は弱いメモリモデルのため、
* 全てのバリアが実際の命令に展開される
*/
/* ARM64 での実装 */
#define mb() asm volatile("dsb sy" ::: "memory")
#define rmb() asm volatile("dsb ld" ::: "memory")
#define wmb() asm volatile("dsb st" ::: "memory")
#define smp_mb() asm volatile("dmb ish" ::: "memory")
#define smp_rmb() asm volatile("dmb ishld" ::: "memory")
#define smp_wmb() asm volatile("dmb ishst" ::: "memory")
メモリバリアの使用パターン
/* パターン1: フラグベースの通知 */
struct work_item {
int data;
int ready; /* フラグ */
};
/* 生産者 */
void producer(struct work_item *item)
{
item->data = compute_result();
smp_wmb(); /* data の書き込みが先に完了 */
WRITE_ONCE(item->ready, 1);
}
/* 消費者 */
void consumer(struct work_item *item)
{
while (!READ_ONCE(item->ready))
cpu_relax();
smp_rmb(); /* ready の後に data を読む */
process(item->data);
}
/* パターン2: リングバッファ */
struct ring_buffer {
unsigned int head; /* 生産者が更新 */
unsigned int tail; /* 消費者が更新 */
void *data[RING_SIZE];
};
/* 生産者 */
bool ring_push(struct ring_buffer *rb, void *item)
{
unsigned int head = rb->head;
unsigned int next = (head + 1) % RING_SIZE;
if (next == READ_ONCE(rb->tail))
return false; /* バッファフル */
rb->data[head] = item;
smp_wmb(); /* データの書き込み後に head を更新 */
WRITE_ONCE(rb->head, next);
return true;
}
/* 消費者 */
void *ring_pop(struct ring_buffer *rb)
{
unsigned int tail = rb->tail;
if (tail == READ_ONCE(rb->head))
return NULL; /* バッファ空 */
smp_rmb(); /* head を確認後にデータを読む */
void *item = rb->data[tail];
smp_mb(); /* データ読み取り後に tail を更新 */
WRITE_ONCE(rb->tail, (tail + 1) % RING_SIZE);
return item;
}
/* パターン3: acquire/release セマンティクス */
/* より現代的で推奨されるパターン */
struct message {
int payload;
int sequence;
};
/* 生産者 */
void send_message(struct message *msg, int payload, int seq)
{
WRITE_ONCE(msg->payload, payload);
smp_store_release(&msg->sequence, seq);
/* payload が確実に書き込まれた後に sequence が更新される */
}
/* 消費者 */
int receive_message(struct message *msg, int expected_seq)
{
if (smp_load_acquire(&msg->sequence) != expected_seq)
return -EAGAIN;
/* sequence を読んだ後に payload を読む */
return READ_ONCE(msg->payload);
}
メモリバリアの注意事項
/*
* 重要な注意事項:
*
* 1. メモリバリアはペアで使用する
* - smp_wmb() と smp_rmb() は対応関係で使う
* - 片方だけでは意味がない
*
* 2. ロック操作には暗黙のバリアが含まれる
* - spin_lock() = acquire バリア
* - spin_unlock() = release バリア
* - ロック内ではバリアは通常不要
*
* 3. アトミック操作にもバリアが含まれる場合がある
* - atomic_add_return() はフルバリア
* - atomic_add() はバリアなし
* - _acquire, _release サフィックスで明示指定可能
*
* 4. READ_ONCE/WRITE_ONCE は並べ替え防止ではない
* - ティアリング防止とコンパイラ最適化防止のみ
* - CPU レベルの並べ替えには smp_*mb() が必要
*
* 5. 過剰なバリアはパフォーマンスを低下させる
* - 特に ARM 等の弱いメモリモデルでは顕著
*/
ロック順序とデッドロック防止
デッドロックの基本
デッドロックの4条件 (Coffman 条件):
1. 相互排他: リソースは一度に一つのプロセスのみ使用
2. 保持して待機: リソースを保持したまま別のリソースを要求
3. 横取り不可: リソースは自発的にしか解放されない
4. 循環待ち: プロセスが循環的にリソースを待つ
典型的なデッドロック (ABBA デッドロック):
タスク A: タスク B:
lock(A); lock(B);
lock(B); ← B を待つ lock(A); ← A を待つ
// デッドロック! // デッドロック!
ロック順序規則
/*
* デッドロック防止の基本原則:
* 全てのロックに対してグローバルな順序を定義し、
* 常にその順序でロックを取得する
*
* ロック順序の決定方法:
* 1. 機能的階層: 上位→下位 (例: ファイルシステム→ブロック→デバイス)
* 2. アドレス順: ポインタ値の小さい方から
* 3. ロック番号: ID の小さい方から
*/
/* 方法1: 固定的なロック順序 */
/*
* ロック取得順序:
* 1. filesystem_lock
* 2. inode->i_mutex
* 3. block_device_lock
* 4. driver_lock
*
* この順序を全てのコードパスで守る
*/
/* 正しい順序 */
void correct_locking(void)
{
mutex_lock(&filesystem_lock); /* 順序 1 */
mutex_lock(&inode->i_mutex); /* 順序 2 */
mutex_lock(&block_device_lock); /* 順序 3 */
/* 処理 */
mutex_unlock(&block_device_lock);
mutex_unlock(&inode->i_mutex);
mutex_unlock(&filesystem_lock);
}
/* 間違った順序 - デッドロックの危険 */
void incorrect_locking(void)
{
mutex_lock(&block_device_lock); /* 順序 3 - 間違い! */
mutex_lock(&filesystem_lock); /* 順序 1 - デッドロック! */
/* ... */
}
/* 方法2: アドレス順 (同種のロックを複数取得する場合) */
void lock_two_inodes(struct inode *inode1, struct inode *inode2)
{
if (inode1 < inode2) {
mutex_lock(&inode1->i_mutex);
mutex_lock(&inode2->i_mutex);
} else if (inode1 > inode2) {
mutex_lock(&inode2->i_mutex);
mutex_lock(&inode1->i_mutex);
} else {
/* 同一 inode */
mutex_lock(&inode1->i_mutex);
}
}
/* 方法3: trylock によるデッドロック回避 */
bool try_lock_both(spinlock_t *lock_a, spinlock_t *lock_b)
{
spin_lock(lock_a);
if (!spin_trylock(lock_b)) {
spin_unlock(lock_a);
/* バックオフしてリトライ */
cpu_relax();
return false;
}
return true; /* 両方取得成功 */
}
/* 方法4: lock_class による順序付け */
static struct lock_class_key fs_lock_key;
static struct lock_class_key blk_lock_key;
void init_locks(void)
{
lockdep_set_class(&fs_lock, &fs_lock_key);
lockdep_set_class(&blk_lock, &blk_lock_key);
}
ネストレベルの管理
/*
* lockdep サブクラス:
* 同じロッククラスを異なるネストレベルで使用する場合
*/
/* 例: ページテーブルのレベル別ロック */
enum page_table_level {
PGD_LEVEL = 0,
PUD_LEVEL = 1,
PMD_LEVEL = 2,
PTE_LEVEL = 3,
};
void lock_page_table(spinlock_t *lock, enum page_table_level level)
{
spin_lock_nested(lock, level);
}
/* inode の親子関係でのロック順序 */
void lock_parent_child(struct inode *parent, struct inode *child)
{
inode_lock(parent);
inode_lock_nested(child, I_MUTEX_CHILD);
}
/*
* lockdep アノテーション:
* lockdep_set_class() - ロッククラスの設定
* lockdep_set_subclass() - サブクラスの設定
* lock_set_class() - クラスとサブクラスの設定
* lockdep_set_novalidate_class() - 検証を無効化
*/
Lockdep - ロック依存関係バリデータ {#lockdep}
Lockdep は、Linux カーネルに組み込まれたロック依存関係バリデータである。デッドロックの可能性を実行時に検出し、問題のあるロック順序を報告する。
Lockdep の有効化
# カーネル設定で Lockdep を有効化
CONFIG_LOCKDEP=y
CONFIG_DEBUG_LOCK_ALLOC=y
CONFIG_PROVE_LOCKING=y
CONFIG_DEBUG_LOCKDEP=y
CONFIG_LOCK_STAT=y
# 追加のデバッグオプション
CONFIG_DEBUG_MUTEXES=y
CONFIG_DEBUG_SPINLOCK=y
CONFIG_DEBUG_ATOMIC_SLEEP=y
CONFIG_DEBUG_RT_MUTEXES=y
Lockdep の動作原理
Lockdep の検証:
1. ロッククラスの追跡:
各ロック初期化時に「クラス」を割り当て
同じソースコード位置のロックは同じクラス
2. 依存関係グラフの構築:
ロック A を保持中にロック B を取得 → A→B のエッジ追加
3. 循環検出:
新しいエッジ追加時にグラフの循環をチェック
循環 = デッドロックの可能性
例:
タスク1: lock(A) → lock(B) → エッジ A→B
タスク2: lock(B) → lock(A) → エッジ B→A を追加しようとする
→ 循環 A→B→A 検出! → 警告を出力!
注意: 実際にデッドロックが発生する前に検出する
「このロック順序を続けると将来デッドロックする可能性がある」
Lockdep の出力例
=============================================
[ INFO: possible recursive locking detected ]
6.1.0-debug #1 Tainted: G
---------------------------------------------
cat/1234 is trying to acquire lock:
ffff8881234abcd0 (&sb->s_type->i_mutex_key#4){+.+.}-{3:3}, at: vfs_create+0x56/0x180
but task is already holding lock:
ffff8881234abce0 (&sb->s_type->i_mutex_key#4){+.+.}-{3:3}, at: lookup_open+0x175/0x3b0
other info that might help us debug this:
Possible unsafe locking scenario:
CPU0
----
lock(&sb->s_type->i_mutex_key#4);
lock(&sb->s_type->i_mutex_key#4);
*** DEADLOCK ***
May be due to missing lock nesting notation
2 locks held by cat/1234:
#0: ffff8881234abcd0 (&sb->s_type->i_mutex_key#4){+.+.}-{3:3}, ...
#1: ffff8881234abce0 (&type->i_mutex_dir_key){+.+.}-{3:3}, ...
stack backtrace:
CPU: 0 PID: 1234 Comm: cat Not tainted 6.1.0-debug #1
Call Trace:
dump_stack+0x76/0xa0
__lock_acquire+0x1234/0x1890
lock_acquire+0xd5/0x2b0
__mutex_lock+0x9b/0x950
vfs_create+0x56/0x180
...
Lockdep の状態表記
Lockdep の状態マーク:
{+.+.} の各位置の意味:
位置1: hardirq-safe / hardirq-unsafe
位置2: hardirq-read-safe / hardirq-read-unsafe
位置3: softirq-safe / softirq-unsafe
位置4: softirq-read-safe / softirq-read-unsafe
記号:
'+' : 使用あり (safe/unsafe として)
'-' : 使用なし
'.' : 不明/未追跡
'?' : 読み取りロックとしてのみ使用
例:
{+...} : hardirq-safe コンテキストで使用
{..+.} : softirq-safe コンテキストで使用
{+.+.} : hardirq-safe と softirq-safe の両方で使用
{-.-.} : どちらの IRQ コンテキストでも使用されていない
IRQ 安全性の整合性チェック:
ロック A が hardirq-safe で使用され、
かつ hardirq-unsafe でも使用された場合:
→ 警告! (割り込み中にデッドロックの可能性)
Lockdep 情報の確認
# lockdep 統計の確認
$ cat /proc/lockdep_stats
lock-classes: 1423
direct dependencies: 5678
indirect dependencies: 12345
all direct dependencies: 9012
dependency chains: 3456
dependency chain hlocks used: 7890
dependency chain hlocks lost: 0
in-hardirq chains: 234
in-softirq chains: 567
in-process chains: 2345
stack-trace entries: 45678
combined max locking depth: 15
max locking depth: 10
max bfs queue depth: 234
# ロック依存関係の表示
$ cat /proc/lockdep
# 現在保持されているロックの確認
$ cat /proc/lockdep_chains
# lockdep の警告メッセージ確認
$ dmesg | grep -A 20 "possible"
# debugfs 経由の詳細情報
$ cat /sys/kernel/debug/lockdep/info
RT-Mutex と優先度継承 {#rt-mutex-と優先度継承}
RT-Mutex (Real-Time Mutex) は、優先度逆転問題を解決するための優先度継承プロトコルを実装したミューテックスである。
優先度逆転問題
優先度逆転 (Priority Inversion):
時間 →
高優先度タスク H: ─────────[ロック待ち]──────────────────
中優先度タスク M: ────────────────[実行中]────────────────
低優先度タスク L: ──[ロック保持]──[M にプリエンプトされる]──
問題:
L がロックを保持 → H がロック待ち → M が L をプリエンプト
→ H は M の完了を間接的に待つ (H > M なのに!)
→ 無限に続く可能性
優先度継承による解決:
L がロック保持中に H が待ち始める
→ L の優先度を H と同じレベルに一時的に引き上げ
→ L は M にプリエンプトされなくなる
→ L がすぐにロックを解放 → H が実行再開
時間 →
H: ──────────[待ち]──[実行]───────────
M: ─────────────────────[実行]────────
L: ──[ロック保持(優先度=H)]──[解放]───
RT-Mutex の使用
#include <linux/rtmutex.h>
/* RT-Mutex の定義 */
struct rt_mutex my_rt_mutex;
rt_mutex_init(&my_rt_mutex);
/* 基本的な使用 */
void rt_mutex_example(void)
{
rt_mutex_lock(&my_rt_mutex);
/* クリティカルセクション */
/* 優先度継承が自動的に処理される */
rt_mutex_unlock(&my_rt_mutex);
}
/* 割り込み可能版 */
int ret = rt_mutex_lock_interruptible(&my_rt_mutex);
if (ret)
return ret;
/* ... */
rt_mutex_unlock(&my_rt_mutex);
/* trylock 版 */
if (rt_mutex_trylock(&my_rt_mutex)) {
/* ... */
rt_mutex_unlock(&my_rt_mutex);
}
RT-Mutex の実装詳細
/*
* RT-Mutex の内部構造
*/
struct rt_mutex_base {
raw_spinlock_t wait_lock; /* ウェイターリスト保護用 */
struct rb_root_cached waiters; /* 優先度順の赤黒木 */
struct task_struct *owner; /* 現在の所有者 */
};
struct rt_mutex {
struct rt_mutex_base rtmutex;
#ifdef CONFIG_DEBUG_LOCK_ALLOC
struct lockdep_map dep_map;
#endif
};
/*
* 優先度継承チェーン:
*
* タスク H がロック L1 を待つ
* → L1 の所有者 M の優先度を H に引き上げ
* → M がロック L2 を待っている場合
* → L2 の所有者の優先度も引き上げ (チェーン伝播)
*
* PI チェーンの深さ制限: デフォルト 1024
* /proc/sys/kernel/max_lock_depth で設定可能
*/
# PI (Priority Inheritance) 関連の設定
$ cat /proc/sys/kernel/max_lock_depth
1024
# futex の PI 対応確認
$ grep PI /boot/config-$(uname -r)
CONFIG_RT_MUTEXES=y
CONFIG_FUTEX_PI=y
PREEMPT_RT パッチセット {#preempt_rt-パッチセット}
PREEMPT_RT は、Linux カーネルをハードリアルタイム OS に変換するパッチセットである。Linux 5.15 以降、段階的にメインラインに統合されている。
PREEMPT_RT の主な変更点
/*
* PREEMPT_RT の主要な変更:
*
* 1. スピンロック → スリーピングスピンロック (RT-Mutex ベース)
* spin_lock() → rt_mutex_lock()
* プリエンプション可能になる
*
* 2. 割り込みハンドラ → スレッド化割り込み
* 全てのハード IRQ ハンドラがカーネルスレッドとして実行
* スケジューリング対象になる
*
* 3. raw_spinlock_t
* 本当のスピンロックが必要な場合に使用
* (スケジューラ、割り込み管理等の低レベルコード)
*
* 4. local_lock
* Per-CPU データの保護用
* RT ではスリーピングロックに変換
*/
/* PREEMPT_RT でのロック変換 */
#ifdef CONFIG_PREEMPT_RT
/* spin_lock -> sleeping lock (RT-Mutex) */
typedef struct {
struct rt_mutex_base lock;
/* ... */
} spinlock_t;
/* raw_spinlock_t -> 本当のスピンロック */
typedef struct {
arch_spinlock_t raw_lock;
/* ... */
} raw_spinlock_t;
#else
/* 通常カーネルでは spinlock_t も raw_spinlock_t も同じ */
typedef struct {
arch_spinlock_t raw_lock;
/* ... */
} spinlock_t;
#endif
/* local_lock の使用例 */
#include <linux/local_lock.h>
static DEFINE_PER_CPU(struct local_lock, my_local_lock);
void local_lock_example(void)
{
/* 非 RT: preempt_disable() */
/* RT: スリーピングロック */
local_lock(&my_local_lock);
/* Per-CPU データの操作 */
this_cpu_inc(my_counter);
local_unlock(&my_local_lock);
}
PREEMPT_RT のビルドと設定
# PREEMPT_RT カーネルのビルド
$ git clone git://git.kernel.org/pub/scm/linux/kernel/git/rt/linux-rt-devel.git
$ cd linux-rt-devel
$ git checkout v6.6-rt
# 設定
$ make menuconfig
# General Setup -> Preemption Model -> Fully Preemptible Kernel (Real-Time)
# 重要な設定項目
CONFIG_PREEMPT_RT=y
CONFIG_HIGH_RES_TIMERS=y
CONFIG_NO_HZ_FULL=y
CONFIG_EXPERT=y
# ビルド
$ make -j$(nproc)
$ sudo make modules_install install
# PREEMPT_RT カーネルの確認
$ uname -v
#1 SMP PREEMPT_RT ...
$ cat /sys/kernel/realtime
1
# リアルタイムスレッドの優先度設定
$ chrt -f 99 ./my_rt_application
$ chrt -p 99 $(pidof irq/24-nvme0)
# IRQ スレッドの確認
$ ps aux | grep irq/
root 123 0.0 0.0 0 0 ? S Jan01 0:00 [irq/24-nvme0]
root 124 0.0 0.0 0 0 ? S Jan01 0:00 [irq/25-eth0]
# RT スレッドの優先度確認
$ chrt -p 123
pid 123's current scheduling policy: SCHED_FIFO
pid 123's current scheduling priority: 50
PREEMPT_RT でのレイテンシ測定
# cyclictest によるレイテンシ測定
$ sudo cyclictest -p 99 -t 4 -n -m -l 10000000
# -p 99: SCHED_FIFO 優先度 99
# -t 4: 4スレッド
# -n: nanosleep 使用
# -m: メモリロック (mlockall)
# -l 10000000: 1000万回ループ
# 出力例 (PREEMPT_RT):
# /dev/cpu_dma_latency set to 0us
T: 0 ( 1234) P:99 I:1000 C:10000000 Min: 1 Act: 3 Avg: 3 Max: 21
T: 1 ( 1235) P:99 I:1500 C:10000000 Min: 1 Act: 3 Avg: 3 Max: 18
T: 2 ( 1236) P:99 I:2000 C:10000000 Min: 1 Act: 4 Avg: 3 Max: 23
T: 3 ( 1237) P:99 I:2500 C:10000000 Min: 1 Act: 3 Avg: 3 Max: 19
# 出力例 (非 RT カーネル):
T: 0 ( 1234) P:99 I:1000 C:10000000 Min: 1 Act: 5 Avg: 7 Max: 1234
# ^^^^ 桁違い!
# hwlatdetect: ハードウェアレイテンシの検出
$ sudo hwlatdetect --duration=60
hwlatdetect: test duration 60 seconds
parameters:
Rone: 0.001000(ms)
Rone: 0.001000(ms)
test_duration: 60
detector: tracer
Max Coverage: 99.9982%
max: 3us
count: 5
# RT レイテンシのリアルタイム表示
$ sudo latencytop
ロック競合分析 {#ロック競合分析}
ロック競合は、パフォーマンスの主要なボトルネックの一つである。Linux カーネルは、ロック競合を分析するための複数のツールを提供している。
lockstat
# lockstat の有効化
CONFIG_LOCK_STAT=y
# lockstat の有効化/無効化
$ echo 1 > /proc/sys/kernel/lock_stat
$ echo 0 > /proc/sys/kernel/lock_stat
# lockstat の統計表示
$ cat /proc/lock_stat
# 出力例:
# lock_stat version 0.4
#-------------------------------------------------------------------
# class name con-bounces contentions waittime-min waittime-max waittime-total waittime-avg acq-bounces acquisitions holdtime-min holdtime-max holdtime-total holdtime-avg
#-------------------------------------------------------------------
#
&rq->__lock: 12345 5678 0.12 1234.56 98765.43 17.38 98765 234567 0.05 45.67 567890.12 2.42
#-------------------------------------------------------------------
&rq->__lock/1: 234 123 0.45 567.89 12345.67 100.37 4567 12345 0.08 23.45 56789.01 4.60
#-------------------------------------------------------------------
# 列の説明:
# con-bounces: キャッシュバウンス発生回数 (競合時)
# contentions: ロック競合回数
# waittime-min: 最小待ち時間 (us)
# waittime-max: 最大待ち時間 (us)
# waittime-total: 合計待ち時間 (us)
# waittime-avg: 平均待ち時間 (us)
# acq-bounces: キャッシュバウンス発生回数 (取得時)
# acquisitions: ロック取得回数
# holdtime-min: 最小保持時間 (us)
# holdtime-max: 最大保持時間 (us)
# holdtime-total: 合計保持時間 (us)
# holdtime-avg: 平均保持時間 (us)
# 統計のリセット
$ echo 0 > /proc/lock_stat
# 競合の多いロックの上位表示
$ cat /proc/lock_stat | sort -k3 -rn | head -20
perf lock
# perf lock - ロック競合のプロファイリング
# ロックイベントの記録
$ sudo perf lock record -- sleep 10
# または特定のコマンドに対して
$ sudo perf lock record -- dd if=/dev/zero of=/dev/null bs=1M count=1000
# ロック統計の表示
$ sudo perf lock report
# 出力例:
# Name acquired contended avg wait total wait max wait
#
# &rq->__lock 123456 5678 2.34 us 13.29 ms 45.67 us
# &p->alloc_lock 98765 234 1.23 us 0.29 ms 12.34 us
# dcache_lock 87654 567 3.45 us 1.96 ms 56.78 us
# files_lock 76543 123 0.98 us 0.12 ms 8.90 us
# 競合の詳細表示
$ sudo perf lock report -t
# -t: タイプ別表示
# ロック競合のフレームグラフ
$ sudo perf lock record -g -- sleep 10
$ sudo perf lock report -g
# BPF ベースのロック競合分析 (perf lock contention)
$ sudo perf lock contention
# Linux 5.19+ で利用可能
# 出力例:
# contended total wait max wait avg wait type caller
#
# 5678 13.29 ms 45.67 us 2.34 us spinlock schedule+0x56
# 234 0.29 ms 12.34 us 1.23 us mutex do_open+0x78
# 567 1.96 ms 56.78 us 3.45 us spinlock dentry_lock+0x23
# 特定期間のロック競合記録
$ sudo perf lock contention --duration 30
# コールスタック付き
$ sudo perf lock contention -g
ftrace によるロック分析
# ftrace を使ったロック追跡
# 利用可能なロックトレーサの確認
$ cat /sys/kernel/debug/tracing/available_tracers
function function_graph irqsoff preemptoff preemptirqsoff wakeup wakeup_rt
# irqsoff トレーサ: 割り込み無効化の最大時間を追跡
$ echo irqsoff > /sys/kernel/debug/tracing/current_tracer
$ echo 1 > /sys/kernel/debug/tracing/tracing_on
$ sleep 10
$ echo 0 > /sys/kernel/debug/tracing/tracing_on
$ cat /sys/kernel/debug/tracing/trace
# preemptoff トレーサ: プリエンプション無効化の最大時間を追跡
$ echo preemptoff > /sys/kernel/debug/tracing/current_tracer
# preemptirqsoff: 両方を追跡
$ echo preemptirqsoff > /sys/kernel/debug/tracing/current_tracer
# 出力例:
# # tracer: irqsoff
# # irqsoff latency trace v1.1.5 on 6.1.0
# # latency: 123 us, #45/67, CPU#2 | (M:preempt VP:0, KP:0, SP:0 HP:0)
# # -----------------
# # | task: kworker/2:1-1234 (uid:0 nice:0 policy:0 rt_prio:0)
# # -----------------
# # => started at: _raw_spin_lock_irqsave
# # => ended at: _raw_spin_unlock_irqrestore
# #
# # _------=> CPU#
# # / _-----=> irqs-off
# # | / _----=> need-resched
# # || / _---=> hardirq/softirq
# # ||| / _--=> preempt-depth
# # |||| / _-=> migrate-disable
# # ||||| / delay
# # cmd pid |||||| time | caller
# # \ / |||||| \ | /
# kworker-1234 2d.h1. 1us : _raw_spin_lock_irqsave
# kworker-1234 2d.h1. 2us : process_one_work
# kworker-1234 2d.h1. 123us : _raw_spin_unlock_irqrestore
# kworker-1234 2d.h1. 124us : trace_irqsoff_hit
# ロックイベントの tracepoint
$ cat /sys/kernel/debug/tracing/available_events | grep lock
lock:lock_acquire
lock:lock_release
lock:lock_contended
lock:lock_acquired
# 特定のロックイベントを有効化
$ echo 1 > /sys/kernel/debug/tracing/events/lock/lock_contended/enable
$ echo 1 > /sys/kernel/debug/tracing/events/lock/lock_acquired/enable
$ cat /sys/kernel/debug/tracing/trace_pipe
# kworker/0:1-1234 [000] d... 12345.678901: lock_contended: ...
# kworker/0:1-1234 [000] d... 12345.679001: lock_acquired: ...
BCC/bpftrace ツール
# bpftrace を使ったロック分析
# スピンロック競合時間のヒストグラム
$ sudo bpftrace -e '
tracepoint:lock:lock_contended {
@start[tid] = nsecs;
}
tracepoint:lock:lock_acquired / @start[tid] / {
@wait_ns = hist(nsecs - @start[tid]);
delete(@start[tid]);
}
END {
clear(@start);
}'
# 出力例:
# @wait_ns:
# [256, 512) 12345 |@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@|
# [512, 1K) 8901 |@@@@@@@@@@@@@@@@@@@@@@@@@@@@@ |
# [1K, 2K) 5678 |@@@@@@@@@@@@@@@@@@@ |
# [2K, 4K) 3456 |@@@@@@@@@@@@ |
# [4K, 8K) 2345 |@@@@@@@@ |
# [8K, 16K) 1234 |@@@@ |
# [16K, 32K) 567 |@@ |
# [32K, 64K) 234 |@ |
# [64K, 128K) 89 | |
# [128K, 256K) 12 | |
# mutex 保持時間の追跡
$ sudo bpftrace -e '
kprobe:mutex_lock {
@start[tid] = nsecs;
@lock[tid] = arg0;
}
kprobe:mutex_unlock / @start[tid] && @lock[tid] == arg0 / {
$dur = nsecs - @start[tid];
@hold_time = hist($dur);
if ($dur > 1000000) {
printf("Long mutex hold: %d ns by %s (PID %d)\n",
$dur, comm, pid);
}
delete(@start[tid]);
delete(@lock[tid]);
}'
# BCC の klockstat ツール
$ sudo /usr/share/bcc/tools/klockstat
ツール: lockstat, lockdep, perf lock {#ツール}
総合的なロック分析ワークフロー
# ステップ 1: システムのロック状態概観
$ cat /proc/lock_stat | head -50
# ステップ 2: 競合の多いロックの特定
$ cat /proc/lock_stat | \
awk 'NR>3 && $3>0 {print $3, $1}' | \
sort -rn | head -10
# ステップ 3: perf lock で詳細分析
$ sudo perf lock record -- sleep 30
$ sudo perf lock report --sort contended
# ステップ 4: 特定のロックのコールスタック分析
$ sudo perf lock contention -g --lock-filter "&rq->__lock"
# ステップ 5: lockdep で安全性検証
$ dmesg | grep -i "lockdep\|deadlock\|circular"
# ステップ 6: ftrace で時間軸分析
$ echo irqsoff > /sys/kernel/debug/tracing/current_tracer
$ echo 1 > /sys/kernel/debug/tracing/tracing_on
$ sleep 60
$ echo 0 > /sys/kernel/debug/tracing/tracing_on
$ cat /sys/kernel/debug/tracing/trace | head -100
カスタムロック統計モジュール
/* ロック統計収集カーネルモジュールの例 */
#include <linux/module.h>
#include <linux/proc_fs.h>
#include <linux/seq_file.h>
#include <linux/spinlock.h>
#include <linux/ktime.h>
struct lock_stats {
atomic64_t acquisitions;
atomic64_t contentions;
atomic64_t total_hold_ns;
atomic64_t max_hold_ns;
atomic64_t total_wait_ns;
atomic64_t max_wait_ns;
};
static struct lock_stats my_stats;
static DEFINE_SPINLOCK(monitored_lock);
/* ラッパー関数 */
static void instrumented_lock(void)
{
ktime_t start, acquired;
s64 wait_ns, hold_ns;
start = ktime_get();
if (!spin_trylock(&monitored_lock)) {
atomic64_inc(&my_stats.contentions);
spin_lock(&monitored_lock);
}
acquired = ktime_get();
wait_ns = ktime_to_ns(ktime_sub(acquired, start));
atomic64_inc(&my_stats.acquisitions);
atomic64_add(wait_ns, &my_stats.total_wait_ns);
/* max の更新 (CAS ループ) */
s64 old_max;
do {
old_max = atomic64_read(&my_stats.max_wait_ns);
if (wait_ns <= old_max)
break;
} while (atomic64_cmpxchg(&my_stats.max_wait_ns,
old_max, wait_ns) != old_max);
}
static int stats_show(struct seq_file *m, void *v)
{
seq_printf(m, "Acquisitions: %lld\n",
atomic64_read(&my_stats.acquisitions));
seq_printf(m, "Contentions: %lld\n",
atomic64_read(&my_stats.contentions));
seq_printf(m, "Avg Wait: %lld ns\n",
atomic64_read(&my_stats.acquisitions) ?
atomic64_read(&my_stats.total_wait_ns) /
atomic64_read(&my_stats.acquisitions) : 0);
seq_printf(m, "Max Wait: %lld ns\n",
atomic64_read(&my_stats.max_wait_ns));
return 0;
}
まとめとベストプラクティス
同期プリミティブ選択フローチャート
ロック選択の指針:
データへのアクセスパターンは?
│
├─ 読み取りのみ ───────────────> RCU (読み取りオーバーヘッドほぼゼロ)
│
├─ 読み取り多数、書き込み少数
│ ├─ ポインタベースの構造 ─────> RCU
│ ├─ 値型の構造 ───────────────> seqlock
│ └─ その他 ───────────────────> rwlock
│
├─ 単一変数の更新 ──────────────> atomic_t / atomic64_t
│
├─ Per-CPU データ ──────────────> Per-CPU 変数 + local_lock
│
└─ 一般的な排他制御
│
├─ 割り込みコンテキストからアクセス?
│ ├─ Yes ──────────────────> spinlock (irqsave)
│ └─ No
│ │
│ ├─ クリティカルセクションでスリープが必要?
│ │ ├─ Yes ────────────> mutex
│ │ └─ No
│ │ │
│ │ ├─ 短い (<数百命令)?
│ │ │ └─ Yes ──────> spinlock
│ │ └─ 長い
│ │ └───────────-> mutex
│ │
│ └─ リアルタイム要件あり?
│ └─ Yes ────────────> rt_mutex (優先度継承)
│
└─ 複数リソースの同時アクセス制御?
└─ Yes ──────────────────> semaphore (カウンティング)
ベストプラクティス一覧
1. ロックの粒度
- 粗すぎるロック: 並列性の低下
- 細すぎるロック: オーバーヘッドとデッドロックリスクの増大
- 適切な粒度を見つけることが重要
2. ロック順序
- 常に一定の順序でロックを取得
- lockdep を有効にして検証
3. クリティカルセクションの最小化
- ロック保持時間を最短にする
- ロック外で可能な処理はロック外で行う
4. 適切なプリミティブの選択
- スピンロック: 短い、スリープ不可、割り込みコンテキスト
- ミューテックス: 長い、スリープ可能、プロセスコンテキスト
- RCU: 読み取り主体、最高の読み取りパフォーマンス
5. デバッグオプションの活用
- CONFIG_PROVE_LOCKING
- CONFIG_DEBUG_LOCK_ALLOC
- CONFIG_LOCK_STAT
6. パフォーマンス監視
- lockstat で競合の多いロックを特定
- perf lock で詳細分析
- ftrace で時間軸分析
7. PREEMPT_RT への対応
- raw_spinlock_t と spinlock_t の使い分けを意識
- local_lock を使用して Per-CPU データを保護
- スリープ不可のコンテキストを最小化
同期プリミティブの比較表
┌─────────────┬──────────┬──────────┬──────────┬─────────┬──────────────┐
│ プリミティブ │ スリープ │ 割り込み │ 読み取り │ 競合時 │ オーバー │
│ │ 可能 │ コンテキ │ スケーラ │ コスト │ ヘッド │
│ │ │ ストOK │ ビリティ │ │ │
├─────────────┼──────────┼──────────┼──────────┼─────────┼──────────────┤
│ atomic_t │ N/A │ Yes │ N/A │ 最小 │ 最小 │
│ spinlock │ No │ Yes │ 低 │ CPU消費 │ 低 │
│ rwlock │ No │ Yes │ 中 │ CPU消費 │ 低 │
│ mutex │ Yes │ No │ 低 │ スリープ│ 中 │
│ semaphore │ Yes │ No │ 低 │ スリープ│ 中 │
│ RCU │ No(*) │ Yes │ 最高 │ 最小 │ ほぼゼロ(読) │
│ SRCU │ Yes │ No │ 高 │ 低 │ 低 │
│ seqlock │ No │ Yes │ 高 │ リトライ│ 低 │
│ Per-CPU │ N/A │ Yes │ 最高 │ なし │ 最小 │
│ rt_mutex │ Yes │ No │ 低 │ PI付き │ 中 │
└─────────────┴──────────┴──────────┴──────────┴─────────┴──────────────┘
(*) SRCU ではスリープ可能
参考資料
公式ドキュメント:
- Documentation/locking/ カーネルソース内ロックドキュメント
- Documentation/RCU/ RCU ドキュメント
- Documentation/memory-barriers.txt メモリバリアガイド
- Documentation/locking/lockdep-design.rst Lockdep 設計ドキュメント
- Documentation/locking/rt-mutex-design.rst RT-Mutex 設計
書籍:
- "Linux Kernel Development" - Robert Love
- "Understanding the Linux Kernel" - Bovet & Cesati
- "Linux Device Drivers" - Corbet, Rubini & Kroah-Hartman
- "Is Parallel Programming Hard, And, If So, What Can You Do About It?"
- Paul E. McKenney (RCU の創始者)
オンラインリソース:
- https://www.kernel.org/doc/html/latest/locking/
- https://lwn.net/Articles/ (LWN.net - カーネル開発の最新情報)
- kernel/locking/ ディレクトリのソースコード
文書履歴
- 2026-04-10: 初版作成