このページの目次

RCU (Read-Copy-Update)

カバー範囲: RCUのコアメカニズム → グレース期間 → rcu_read_lock/unlock → call_rcu/kfree_rcu → SRCU → Tasks RCU → RCUデバッグ カーネルバージョン: 2.6 ~ 6.x

概要

RCUは「全員が去ってから回収する」という同期メカニズムです。これは、ロックを取得せずに共有データにアクセスできることを意味します。読者は rcu_read_lock()rcu_read_unlock() のみを必要とします(これらの操作は、多くの構成でゼロオーバーヘッドです)。書き込み側はデータをコピーし、コピーを修正し、その後、すべての読者が古いデータのアクセスを終了するのを待ってから、古いデータを回収します。

RCUはLinuxで最もユニークな同期プリミティブであり、VFS (dentry)、ネットワーク (ルーティングテーブル)、ファイルシステム (inode)、cgroup などのコアデータ構造で広く使用されています。

コアイディア

読者 (ロックなし、非ブロッキング):
  rcu_read_lock();
  p = rcu_dereference(ptr);  // ポインタの読み取り
  // ... pを使用 ...
  rcu_read_unlock();

書き込み側:
  old = ptr;
  new = copy(old);           // コピー
  modify(new);               // コピーを修正
  rcu_assign_pointer(ptr, new);  // ポインタをアトミックに切り替え
  synchronize_rcu();         // すべての読者が退出するのを待つ (ブロック)
  // または: call_rcu(&old->rcu, callback);  // 非同期回収

重要な制約: 読者は rcu_read_lock/unlock の間スリープしてはなりません (rcu_read_lock はプリエンプションを無効にします。スリープすると、他のタスクがクリティカルセクション内にある可能性があり、RCUの保証が壊れます)。

グレース期間

グレース期間 = rcu_assign_pointer から、それ以前に存在したすべての読者が完了するまでの時間窓

CPU 0 (書き込み側):  CPU 1 (読者):        CPU 2 (読者):
  rcu_assign_...                   
  |               rcu_read_lock()       
  |               |--- 読者が古いデータ上で ---|
  |                                          rcu_read_lock()
  |                                          |-- 新しいデータ上で --|
  |               rcu_read_unlock()
  |               // CPU 1 が退出
  synchronize_rcu() が返す
  // この時点: どのCPUの読者も old を見ることができない
  // → 安全な回収

重要な不変条件⁠: グレース期間中、すべてのCPUは少なくとも1回のコンテキストスイッチ(静穏状態)を経験します。RCUはスケジューラのコンテキストスイッチを「読者が退出した」というシグナルとして利用します。これが rcu_read_lock がプリエンプションを無効化する理由です。

実装

// kernel/rcu/tree.c
// 主要な実装: Tree RCU (数千CPUまで拡張可能)
// CPUの静穏状態レポートを木構造で整理

// rcu_read_lock():
//   preempt_disable()  ← 多くの場合、これがすべて
//   実際: プリエンプションを無効化 = RCUに「クリティカルセクションにいる」と伝える

// rcu_read_unlock():
//   preempt_enable()   ← これが最後の preempt_disable の場合、RCUは静穏状態を記録

call_rcu / kfree_rcu

// include/linux/rcupdate.h
// 非同期回収: コールバックを登録し、グレース期間後に呼び出す
call_rcu(&p->rcu_head, my_callback);
// → RCUはグレース期間後に my_callback(p) を呼び出す

// 一般的なパターン: オブジェクトの解放
kfree_rcu(p, rcu_head);  // call_rcu(..., (void(*))kfree) と同等

synchronize_rcu

// 同期待ち: すべての既存の読者が退出するまでブロック
synchronize_rcu();  // 数百ミリ秒スリープする可能性がある!
// 割り込みコンテキストからは呼び出せない

RCUのバリエーション

バリエーション読者のブロックタイプ適用シーン
Classic RCUプリエンプション無効化 (rcu_read_lock)ほとんどのカーネルコード
SRCU (Sleepable RCU)明示的 srcu_read_lock (スリープ可能)ファイルシステム, VFS
Tasks RCUプリエンプション無効化 (トレース専用)ftrace, kprobes

SRCU

// include/linux/srcu.h
// 読者はスリープ可能 (mutexを取得する状況でRCUデータを保護するのに適している)
srcu_read_lock(&ss);
// ... スリープ可能 ...
srcu_read_unlock(&ss, idx);

// 書き込み側:
synchronize_srcu(&ss);
// synchronize_rcu よりオーバーヘッドが高い

デバッグ

# RCU統計
cat /sys/kernel/debug/rcu/rcu_expedited  # 高速グレース期間が使用されたか
cat /sys/kernel/debug/rcu/rcugp          # グレース期間の状態

# RCUストール検出 (CPUが rcu_read_lock 内で長時間経過)
#   → カーネルが "INFO: rcu_sched detected stalls on CPUs/tasks" を出力
#   → 特定のCPUが読者から長時間退出していない = ループまたは長時間のプリエンプション無効化

# ストール検出のタイムアウト
cat /sys/module/rcupdate/parameters/rcu_cpu_stall_timeout

参考

  • カーネルドキュメント⁠: Documentation/RCU/ ディレクトリ (最も包括的なRCUドキュメント)
  • LWN: "What is RCU?" シリーズ, "The RCU API"
  • ソースコード⁠: kernel/rcu/tree.c, include/linux/rcupdate.h, kernel/rcu/srcutree.c

キーワード: RCU, グレース期間, rcu_read_lock, call_rcu, kfree_rcu, SRCU, quiescent state, rcu_dereference