TL

RCUの実装詳細(grace period検出とコールバック)

RCUの原理は分かっても「数百コアでgrace periodをどう検出するのか」が謎のまま――Tree RCUの階層集約と状態機械、call_rcuの非同期処理を追えば、その実装の巧みさが腑に落ちます。

応用RCUTree RCULinuxカーネル並行処理grace periodSRCU最終更新: 2026-06-21
TL;DR要点だけ先に
  • 1.Tree RCUはCPUごとのクォースセント状態をrcu_nodeツリーでボトムアップに集約する。全CPUを1つのフラグで監視するとロック競合で破綻するため、扇形に分割してロックの取り合いを各部分木に閉じ込める。
  • 2.grace periodは専用カーネルスレッドrcu_gp_kthreadが回す状態機械で進む。GP開始でツリーを初期化し、各CPUの静止報告がrootまで伝播しきった瞬間にGPを終了、待っていたコールバックを起動する。
  • 3.call_rcuはコールバックをCPUごとのリストに積み、GP完了後にsoftirqまたはrcuocスレッドで一括実行する。SRCUは読み取り区間内でのスリープを許し、Tasks RCUはトランポリン回収のため別の静止条件を使う。

なぜ実装が難しいのか──「全CPUの静止」を数百コアで検出する

RCUの原理は「公開後に全CPUがクォースセント状態(QS、読み取り区間の外にいると保証できる瞬間)を一度ずつ通過すれば、grace period(GP、猶予期間)が完了する」というものです。原理そのものは RCUの原理 で扱いました。本記事はその一段下、「数百コアで、誰がいつ静止したかをどう集約し、いつGPを終わらせ、貯まったコールバックをどう処理するか」 という実装の核心を追います。

素朴に実装するなら、GPごとに「未報告CPUのビットマスク」を1つ用意し、各CPUが静止するたびに自分のビットを下ろし、全ビットが0になったらGP完了とすればよさそうです。しかしこれは メニーコアで即座に破綻 します。1つのマスクを保護する1つのロックを、数百コアが静止のたびに奪い合うため、そのキャッシュラインがコア間を行き来し続ける(バウンシング)。RCUは読み取りを限界まで軽くするのが目的なのに、GP検出の側で別のスケーラビリティ問題を作ってしまうのです。

本記事が扱うのは「古典」ではなく現行のTree RCU

初期のLinuxには単一グローバルロックで全CPUを監視するClassic RCUがありましたが、コア数増加で破綻し、2008年にPaul McKenneyらが Tree RCU へ置き換えました。現在のカーネルでQS集約・GP検出・コールバック処理を担うのはすべてTree RCUです。本記事の「RCU」は断りのない限りこのTreeRCUを指します。

rcu_nodeツリー──ロック競合を部分木に閉じ込める

Tree RCUの中心的アイデアは、CPU群を木構造に分割し、ロックの取り合いを各部分木の内側に閉じ込める ことです。1枚のフラットなビットマスクの代わりに、rcu_node 構造体を多分木(典型的な扇出は64、CONFIG_RCU_FANOUT)に組みます。

                 [ root rcu_node ]          ← 全体のGP状態を集約
                /        |        \
        [ rcu_node ] [ rcu_node ] [ rcu_node ]   ← 中間ノード(大規模時)
         /   |   \
   CPU0  CPU1 ... CPU63                       ← 葉ノード1つが最大64CPUを束ねる
   (各CPUは rcu_data を1つ持つ)

rcu_node は、自分の子(下位ノードまたはCPU)のうち まだ当該GPで静止していないもの をビットマスク qsmask で持ちます。CPUが静止すると、まず自分が属する 葉ノードのロックだけ を取って自分のビットを下ろす。これで全ビットが0になったら、初めて1つ上の親へ「この部分木は全員静止した」と報告し、親のビットを下ろしにいきます。

ロックは「葉単位」で分散する

この設計の妙は、ほとんどのQS報告が 葉ノードのロックで完結 する点です。64CPUを束ねる葉ノードのロックを取り合うのは最大でも64コアで、しかも親のロックに触れるのは「葉が全員静止した最後の1回」だけ。ロックのバウンシングが部分木に閉じ込められ、rootロックの競合は劇的に減ります。コア数が増えても木を深く・広くするだけでスケールします。

データ構造の役割分担は次のとおりです。

構造体粒度主な役割
rcu_dataCPUごと(per-CPU)そのCPUのQS報告状態・コールバックリスト・進行中GP番号を保持
rcu_nodeCPU群(扇出単位)子のqsmaskを集約。GP番号や待ちCPUを部分木単位で管理
rcu_state(global)システム全体現在のGP番号gp_seqやrcu_gp_kthreadへの参照など、GP全体の制御点

QSの検出──いつ「このCPUは静止した」と言えるか

葉ノードのビットを下ろす前提として、そのCPUが本当にQSを通過したか を判定する必要があります。非プリエンプトRCUでは コンテキストスイッチ・ユーザ空間実行・アイドルがQSの目印でした。Tree RCUはこれを能動的に回収する仕組みを持ちます。

  • スケジューラティック(rcu_check_callbacks 相当):周期タイマ割り込みのたびに、そのCPUがユーザ空間にいたか、あるいはアイドルだったかを調べ、QSなら記録する。
  • コンテキストスイッチプロセススケジューリング が走るたびにQSとして報告される。
  • プリエンプタブルRCUCONFIG_PREEMPT_RCU):読み取り区間内でのプリエンプションを許すため、QSの定義が変わる。rcu_read_lock() がカウンタを上げ、区間内でプリエンプトされたタスクは葉ノードの blkd_tasks リストに繋がれ、そのタスクが rcu_read_unlock() を実行するまでQSにならない。区間内で寝てよくなった代償として、ブロック中タスクの追跡が要るわけです。
静止しないCPUはこちらから「小突く」

計算ループに入ったまま一度もスケジューラに戻らないCPUがあると、QSを報告できずGPが永遠に終わりません。これを避けるため、GPが長引くとRCUは resched_cpu() で未報告CPUへ 再スケジュール要求のプロセッサ間割り込み(IPI) を送り、強制的にスケジューラを通過させてQSを取りにいきます。nohz_full(ティックレス)CPUの扱いも同様に特別な配慮が要る箇所で、Tree RCU実装の複雑さの多くはこの「静止しないCPUをどう静止させるか」に費やされています。

grace period状態機械──rcu_gp_kthreadが回す

GPの開始・監視・終了は、システムに1つだけある専用カーネルスレッド rcu_gp_kthread が状態機械として駆動します。CPUごとに勝手にGPを進めるのではなく、1点に制御を集約することで、GP番号の整合とツリー初期化を一貫させています。GPは大きく3フェーズで進みます。

(待機)── コールバックが現れる ──▶ [GP-INIT]
   [GP-INIT]  グローバルgp_seqを進め、ツリー全rcu_nodeのqsmaskを
              「全CPU未報告」に初期化(新しいGPの開始を宣言)
        │
        ▼
   [QS-WAIT]  各CPUがQSを報告 → 葉のビットdown → 全部0なら親へ伝播
              rcu_gp_kthreadは周期的にforce_quiescent_state()で
              停滞CPUへIPIを送り、報告を促す
        │  rootのqsmaskが空になった=全CPUが静止を通過
        ▼
   [GP-CLEANUP]  gp_seqを「完了」へ進め、各CPUのrcu_dataに
                 「このGPは完了した」と記録。待っていたコールバックを
                 「実行可能」へ移し、softirq/rcuocを起こす
        │
        ▼
(待機に戻る。保留コールバックが残っていれば次のGPを即開始)

ここで効くのが gp_seq(GPシーケンス番号) です。GP番号を単調増加のカウンタで持ち、下位ビットで「GP進行中か完了済みか」の位相を表します。各CPUの rcu_data は「自分が待っているGP番号」を覚えており、グローバルな gp_seq がそれを追い越したかを比較するだけで「自分の待ちGPは完了したか」を ロックなしで判定 できます。個々のコールバックにGP番号の刻印(スタンプ)を付け、番号比較で完了を見るこの方式が、コールバック処理全体の土台です。

GP番号の比較は「ラップアラウンド安全」に書く

gp_seq は単調増加ですが有限ビットなので、いつかは一周(ラップアラウンド)します。完了判定を seq_a > seq_b のような素朴な大小比較で書くと、ラップ直後に逆転して 「完了したGPを未完了と誤判定」 しかねません。カーネルは符号付き差分((s32)(a - b) < 0 相当)でこれを安全に比較します。実装を読む際は、GP番号の比較が必ずこの差分形式になっている点に注目すると、なぜ単純な不等号でないのかが腑に落ちます。

call_rcuとコールバック処理──貯めて、待って、一括で流す

更新側が call_rcu(head, func) を呼ぶと、funcその場では実行されません。GP完了後に非同期で呼ぶのがこのAPIの役目で、割り込みハンドラ のように待てない文脈から安全に解放を予約できます。処理の流れはこうです。

  1. 登録call_rcu はコールバックを 呼び出したCPUの rcu_data のリスト へ繋ぐ。グローバルなロックを取らずper-CPUで完結するため、登録は極めて軽い。
  2. GPとの紐付け:そのコールバックには「現在進行中(または次の)GP番号」が割り当てられる。rcu_segcblist(セグメント化コールバックリスト)が、コールバックを「DONE(実行可能)/WAIT(このGPを待つ)/NEXT(次のGPを待つ)」のセグメントに区分けして管理する。
  3. GP完了の波及rcu_gp_kthread がGPを終えると、各CPUのリストで「そのGPを待っていた」セグメントが DONEへ昇格 する。
  4. 実行:DONEになったコールバックを、RCU_SOFTIRQ(ソフト割り込み文脈)で、あるいは設定により専用スレッド rcuoc(callback-offload kthread) が、一括して呼び出す。
/* 更新側:call_rcu は即座に戻り、解放はGP後に肩代わりされる */
void replace(struct foo *new) {
    struct foo *old = rcu_dereference_protected(gp, lockdep_is_held(&lock));
    rcu_assign_pointer(gp, new);   /* 新版を公開 */
    call_rcu(&old->rcu, free_foo); /* old は GP 完了後に free_foo で解放 */
    /* ここで戻る。ブロックしない */
}
バッチングこそ効率の源

1コールバックごとに1GPを回していては割に合いません。Tree RCUは 1回のGPで、その間に貯まった全コールバックをまとめて処理 します。GPはミリ秒オーダーで「重い」一方、その1回の待ちに数千個のコールバックを相乗りさせれば、コールバック1個あたりのコストは償却されて小さくなる。これがRCUを高頻度な解放にも耐えさせる鍵です。call_rcu が即座に戻り、実処理を後ろにまとめるのは、このバッチングを成立させるためです。

コールバックを貯めすぎると暴発を防ぐブレーキが要る

GPが追いつかないほど call_rcu が殺到すると、未処理コールバックがメモリを食いつぶします。これを防ぐため、保留数が閾値を超えると GPを急かす(force quiescent state を早める) とともに、極端な場合は登録側を一時的に律速します。rcu_barrier() は「いま登録済みの全コールバックが完了するまで待つ」APIで、モジュール解放時などに「自分の予約した解放が全部終わったか」を保証するのに使います。

RCUの派生──SRCUとTasks RCU

基本のTree RCUには「読み取り区間内で寝てはいけない」「QSはスケジューラ事象で取る」という制約があります。用途に合わせてQSの定義そのものを差し替えたのが派生RCUです。

種類読み取り区間内のスリープQS/GPの定義主な用途
Tree RCU(非プリエンプト)不可コンテキストスイッチ・ユーザ空間・アイドルカーネル全般のホットパス
SRCU(Sleepable RCU)ドメインごとのper-CPUカウンタ2組で『旧世代の読み手』が尽きたかを判定区間内で寝うる経路(一部のドライバ・通知チェーン)
Tasks RCU可(自発的スリープ込み)各タスクが一度でも自発的にコンテキストスイッチ/ユーザ空間/アイドルを通過ftrace等のトランポリン回収

SRCU はGPを ドメイン(srcu_struct)ごとに分離 するのが核心です。srcu_read_lock() はper-CPUカウンタを上げ、unlock で下げる。GP検出は「世代を切り替え、旧世代のカウンタ合計が読み手の入った数と一致した(=旧世代の読み手が全員抜けた)」ことで行います。区間内で寝てもよい代わりに、読み手は軽量ながら 実際にper-CPUカウンタを操作する(純粋なTree RCUの読み手は何も書かない)点が対価です。ドメインを分けることで、ある経路の長い読み手が無関係な経路のGPを巻き込んで遅らせる事態を避けられます。

Tasks RCU は、call_rcu では追えない対象——具体的には ftraceがコード書き換えで挿入する「トランポリン」 の安全な回収のために生まれました。トランポリンは rcu_read_lock() で囲めない素のコード片なので、QSを「各タスクが一度でも自発的にスケジューラを通過したか」と再定義し、現在トランポリン上を走っているタスクがいなくなった ことを保証します。

試験・面接で問われる核心
  • Tree RCU はQSを rcu_node 多分木で ボトムアップ集約 し、ロック競合を部分木に閉じ込めてメニーコアでスケールさせる(フラットな単一マスクの破綻を回避)。
  • GPは専用スレッド rcu_gp_kthread が状態機械(INIT→QS待ち→CLEANUP)で駆動し、進行は gp_seq で表す。完了判定は ラップアラウンド安全な差分比較
  • call_rcu はコールバックをper-CPUリストに積み、GP完了後に RCU_SOFTIRQ/rcuocスレッドバッチで一括実行rcu_barrier() で登録済み全完了を待てる。
  • 派生は SRCU(区間内スリープ可・ドメイン分離・per-CPUカウンタでGP判定)と Tasks RCU(自発スイッチをQSとし、ftraceトランポリンを回収)。

まとめ──分割集約・状態機械・バッチングの三段構え

まとめ

RCUの実装の核心は、原理(全CPUのQS通過でGP完了)を メニーコアで破綻させずに動かす 工夫にあります。Tree RCU はCPUごとのQSを rcu_node 多分木でボトムアップに集約し、ロックの取り合いを葉単位の部分木に閉じ込めることで、単一マスクのロック競合を回避します。GPの開始・監視・終了は専用スレッド rcu_gp_kthread が状態機械として回し、進行と完了は gp_seq(ラップアラウンド安全に比較)で表します。更新側の call_rcu はコールバックをper-CPUリストへ軽量に積むだけで即座に戻り、GP完了後に RCU_SOFTIRQ/rcuocスレッド が貯まったコールバックを バッチで一括処理 することでGPの重さを償却します。さらに SRCU(読み取り区間内スリープ可・ドメイン分離)と Tasks RCU(ftraceトランポリン回収)が、QSの定義を差し替えて適用範囲を広げます。原理の復習は RCUの原理、QS判定の土台は コンテキストスイッチプロセススケジューリング、待ち手とプリエンプションの絡みは カーネルプリエンプション も合わせてどうぞ。

OS Article

RCUの実装詳細(grace period検出とコールバック)を実務で読む

TL;DRは入口です。実際に選ぶ・使う段階では、何を解決するか、何と比較するか、導入後にどこで詰まるかまで見る必要があります。

解決すること

RCU

比較で見る軸

難易度: advanced / カテゴリ: OS / タグ数: 6

導入後に効く点

grace periodは専用カーネルスレッドrcu_gp_kthreadが回す状態機械で進む。GP開始でツリーを初期化し、各CPUの静止報告がrootまで伝播しきった瞬間にGPを終了、待っていたコールバックを起動する。

先に潰すリスク

用語だけ覚えても、設計・実装・運用でどこに効くかを確認しないと判断を誤る。

数字・仕様の読み方
難易度
advanced
カテゴリ
OS
タグ数
6

判断チェックリスト

  • 自社の用途が「RCU / Tree RCU」に近いか確認する。
  • 強みである「Tree RCUはCPUごとのクォースセント状態をrcu_nodeツリーでボトムアップに集約する。全CPUを1つのフラグで監視するとロック競合で破綻するため、扇形に分割してロックの取り合いを各部分木に閉じ込める。」が本当に評価軸になるか確認する。
  • 注意点の「用語だけ覚えても、設計・実装・運用でどこに効くかを確認しないと判断を誤る。」を運用で吸収できるか確認する。
  • 公開値や仕様値は、対象プラン・対象機種・対象リージョンまで確認する。
  • 既存システム、ID、ネットワーク、監視、バックアップとの接続方法を先に洗い出す。
  • 小さく試してから、本番移行、権限設計、障害時手順、コスト監視を決める。

次に確認する観点

RCUTree RCULinuxカーネル並行処理grace periodRCUTree RCULinuxカーネル
参考: 公式情報