RCU: パブリッシュ、グレース期間、および回収

このページの目次

構成ポインタは新しいオブジェクトに置き換えられていますが、古い読者はまだ終了していません。RCU はエントリ更新と古いメモリの回収を分離して処理します。

参照目標読書位置
パブリッシュとグレース期間第1~3節
構成の置換とコールバックの空欄化第4~5節
SRCU、エスケープ参照、およびストール第6~8節

RCU は「共有構造からオブジェクトを削除する」と「オブジェクトのメモリを解放する」を分離します。読者は低い同期オーバーヘッドでパブリッシュされたオブジェクトにアクセスできます。更新者は古いオブジェクトへの到達可能なエントリを無効にした後、適切なグレース期間を待ってから、古いメモリの回収を許可します。

RCU は、更新のたびに構造全体をコピーすることを要求するものではなく、読者に複数のオブジェクトにわたる一貫性のあるグローバル最新スナップショットを自動的に提供するものでもありません。RCU はパブリッシュと寿命に関する一連のプロトコルに似ており、データ構造自体の更新ルールと共同で設計する必要があります。

1. 3つの責任はそれぞれ個別に履行する必要があります

責任一般的なメカニズム責任を負わないもの
パブリッシュ初期化後のオブジェクトrcu_assign_pointer など複数の更新者を直列化しない
読側でのポインタの取得と使用rcu_read_lock + rcu_dereference他のメカニズムによってオブジェクトフィールドが永続的に変更されないことを保証しない
無効化後の遅延回収synchronize_rcu / call_rcu / kfree_rcuエスケープした参照がすべて消滅することを保証しない

__rcu 注釈は静的チェックに役立ちますが、実行時の自動参照カウントではありません。rcu_dereference() は、対応する読側または保護された条件に従って使用しなければならず、このマクロを使用しただけで任意のオブジェクトが安全であると見なしてはいけません。

旧 pointer を外し、既存読者を待って回収 読者 A 更新者 読者 B 旧 object を保持 読側区間を終了 新 pointer を公開 新 object を読める 必要な GP 後に旧対象を解放 全将来読者を待たない。公開・書者直列化・旧 object 回収は別責務。

2. グレース期間の待機は、世界中のすべての読者を待つことではありません

古いオブジェクトを削除した後、適切なグレース期間は、関連する、以前は古いオブジェクトを保持していた可能性のある読側クリティカルセクションが終了したことを保証します。新しい読者は引き続き進入できます。システムは、「すべての CPU で同時に RCU 読者が存在しない」という瞬間を待つ必要はありません。

オブジェクトは、まず関連するすべてのパブリッシュエントリから無効にする必要があり、読者も契約されたエントリを通じてのみそれを取得する必要があります。別のリンクリストがまだ古いオブジェクトを指している場合、または読者が生ポインタをクリティカルセクションの外に持ち出して引き続き使用している場合、単に 1 つのグレース期間を待っても、寿命の誤りは修正されません。

グレース期間は、ポインタ代入の命令から固定のタイマーを開始するものでも、固定のミリ秒数でもありません。スケジューリング、構成、読側の持続時間、および実装が待機に影響を与えます。同期待機はスリープできます。非同期コールバックは、更新者が同期的にブロックされるのを防ぎますが、回収されていないオブジェクトが蓄積される可能性があります。

固定の所要 ms はありません。synchronize_rcu は sleep 可能で、call_rcu は回収を後へ予約します。更新が回収を上回ると保持メモリが増えます。

図を準備しています
猶予期間はどの読取区間を待つ?

旧入口の撤去、読取区間待ち、回収コールバックは別境界です。新読者は入れ、同じタスク名でも同じ世代の区間とは限りません。

3. プリエンプション対応 RCU とクエシェントステート

rcu_read_lock() が常に preempt_disable() であるとは一概に言えません。プリエンプション対応 RCU 構成では、読側タスクはプリエンプション可能であり、カーネルは関連する読者を追跡します。また、異なる実装では読側の命令オーバーヘッドも異なります。

通常の RCU 読側は、任意にブロックできない API ルールに従う必要があります。スケジューラによってプリエンプションされることと、明示的にスリープ可能な操作を呼び出すことは別物です。読側で正常にスリープする必要がある場合は、SRCU などの適切なインターフェースを検討し、自行で schedule を挿入しないでください。

クエシェントステートは、関連する読側がすでに通過したことを確認するための証拠であり、ユーザー空間、アイドル、コンテキストスイッチなどの状態を含む可能性があります。すべての CPU で、各グレース期間内に必ず 1 回のコンテキストスイッチが発生する必要があるわけではありません。プリエンプションされた RCU 読者についても、CPU が切り替わっただけで終了したとは見なされません。What is RCU を参照してください。

4. 置換可能な構成オブジェクト

以下は、登録エントリを含まないカーネルコンポーネントの独立した翻訳ユニットの例です。オブジェクトがパブリッシュされた後、value は原地で変更されず、書き込み側は mutex で直列化され、読者はスカラー値のみをコピーします。

#include <linux/errno.h>
#include <linux/mutex.h>
#include <linux/rcupdate.h>
#include <linux/slab.h>

struct kb_config {
    int value;
    struct rcu_head rcu;
};

static DEFINE_MUTEX(config_lock);
static struct kb_config __rcu *current_config;

int kb_config_get(int *value);
int kb_config_replace(int value);
void kb_config_shutdown(void);

static void free_config(struct rcu_head *head)
{
    struct kb_config *p = container_of(head, struct kb_config, rcu);
    kfree(p);
}

int kb_config_get(int *value)
{
    struct kb_config *p;
    int ret = -ENOENT;

    rcu_read_lock();
    p = rcu_dereference(current_config);
    if (p) {
        *value = p->value;
        ret = 0;
    }
    rcu_read_unlock();
    return ret;
}

int kb_config_replace(int value)
{
    struct kb_config *next, *old;

    next = kmalloc(sizeof(*next), GFP_KERNEL);
    if (!next)
        return -ENOMEM;
    next->value = value;

    mutex_lock(&config_lock);
    old = rcu_replace_pointer(current_config, next,
                              lockdep_is_held(&config_lock));
    mutex_unlock(&config_lock);
    if (old)
        call_rcu(&old->rcu, free_config);
    return 0;
}

/* 呼び出し元は、すべてのエントリをシャットダウンし、実行中の API 呼び出しの終了を待つ必要があります。 */
void kb_config_shutdown(void)
{
    struct kb_config *old;

    mutex_lock(&config_lock);
    old = rcu_replace_pointer(current_config, NULL,
                              lockdep_is_held(&config_lock));
    mutex_unlock(&config_lock);
    synchronize_rcu();
    kfree(old);
    rcu_barrier(); /* これまでにキューに入れられ、このコンポーネントのコードを実行するコールバックの待機。 */
}

value の出力アドレスは呼び出し元によって有効性が保証されており、ページフォールト処理を必要とするユーザーポインタを指してはいけません。shutdown のエントリクローズと呼び出し終了待機は外部コンポーネントの責任です。config_lock を追加しただけでは、shutdown の後にロック取得位置に達していない replace が再パブリッシュされるのを防ぐことはできません。

書き込み側同士では、RCU は排他制御に代わりません

2人の書き込み側がどちらも古いオブジェクト O を読み取り、それぞれ N1、N2 をパブリッシュすると仮定します。それぞれが読み取った O を各自で回収する場合、同じオブジェクトを重複して回収する可能性があります。例では、更新側の mutex を使用して、「古い値の取得、置換、回収のスケジュール設定」という操作を直列化しています。

5. 同期待機と非同期コールバック

call_rcu() は埋め込まれた rcu_head を受け取り、コールバックパラメータは struct rcu_head * です。container_of を使用して外側のオブジェクトを復元します。kfree_rcu(p, rcu) は一般的な解放インターフェースですが、kfree をコールバックポインタに強制的に変換するものとして解釈してはいけません。両者のパラメータと実装は異なります。

call_rcu() のコールバックは、任意にスリープできない実行制順に従う必要があります。コールバックはグレース期間後に実行資格を得ますが、グレース期間の終了直後に特定の CPU で即座に実行されるとは限りません。

モジュールの終了:読者の終了とコールバックの実行完了は同じ条件ではありません

待機確立される保証
synchronize_rcu読者グレース期間を待機しますが、これまでにすべての call_rcu コールバックが実行されたことを保証しません
新しいコールバックの阻止後、rcu_barrier対応する提出済みコールバックの空欄化を待機し、アンロードコードがまだ呼び出されないようにします

モジュールのコールバックがモジュール自身のコード内にある場合、新しい提出エントリを解決し、コールバックの完了を待ってから、アンロードを行う必要があります。RCU とアンロード可能モジュール を参照してください。

非同期更新レートが高い場合、回収待機メモリの増加を制御する必要があります。expedited grace period は一部の待機を削減できますが、システム干渉を増加させるため、すべての RCU ストールの一般的な修正策として使用することはできません。

6. SRCU と Tasks RCU

SRCU は独立した srcu_struct ドメインを使用します。読者が取得したインデックストークンは、同じドメインの unlock に渡す必要があります。

int idx = srcu_read_lock(&domain);
/* SRCU プロトコルに従ってオブジェクトを取得して使用します。許可された条件下ではスリープ可能です。 */
srcu_read_unlock(&domain, idx);

更新者は synchronize_srcu(&domain) または対応するコールバック API を使用します。自分の同じドメインの読側クリティカルセクション内で、それ自体を含むグレース期間を待機することはできません。また、回収が完了する前にドメインを破棄することもできません。異なるドメインの読者や待機は相互に通用しません。

RCU Tasks、Tasks Rude、Tasks Trace は、特殊なタスク/トレース同期ニーズを対象としており、待機条件が異なります。これらは「trace 専用プリエンプション無効化 RCU」という単一の変体ではありません。対応する基盤となるニーズがない限り、名前に Tasks が含まれているからといって、通常の RCU を置き換えてはいけません。

7. 読側終了後に引き続き使用する場合どうするか

読側終了後にオブジェクトを長期間保持する必要がある場合は、クリティカルセクション内でオブジェクトプロトコルに従って独立した参照の取得を試み、成功を確認します。回収とゼロからの取得のルールは一緒に設計する必要があります。生ポインタをグローバル変数または非同期ワークに単純に保存し、RCU 読側を終了すると、本来の寿命保証が失われます。

RCU は、同じオブジェクトフィールドの並行更新を自動的に直列化するものではありません。不変オブジェクトの置換は推論が容易なパターンです。原地更新が必要な場合は、ロック、アトミック操作、および読者が許容する整合性について追加で説明する必要があります。

8. RCU ストールは証拠に基づいて分析する必要があります

ストールは、RCU が期待された時間内に必要な進捗を得られなかったことを示します。原因としては、長い読側、長時間の割り込み/プリエンプションのマスク、CPU の飢餓、タイマーまたはスケジューリングの異常などが考えられます。アラートが表示されたからといって、必ず rcu_read_unlock の書き漏らしがあるとは断定できません。

レポート内の CPU/タスク、グレース期間、重複スタック、および実行構成と組み合わせて特定します。ストールタイムアウトを引き上げても、レポートの閾値が変わるだけで、進捗の問題は修正されません。RCU ストール警告 と 同期デバッグ を参照してください。