Linux カーネル・インフラストラクチャ

最終更新: 2025-06-13 07:55:23 +0800 +0800
tags: Kernel | Linux
原文は英語で書かれています。あなたはAI翻訳版を読んでいます。

サブシステムのコードは、汎用ヘッダー、アーキテクチャ固有のオーバーライド、生成ファイル、コア実装ディレクトリにまたがるインフラストラクチャに依存します。以下のメモでは、単一のデバイスやプロトコルサブシステムに属さない複数の機構、すなわち診断、エラー値ポインター、分岐ヒントとパッチ可能な分岐、one-copy アクセス、SLUB のアロケーションパスを整理します。

診断とエラー報告 #

カーネルの診断では、重大度ごとの pr_*() マクロと、デバイスを認識する dev_*() マクロを使用します。

c
if (revision < minimum_revision) {
    dev_err(dev, "unsupported revision %#x\n", revision);
    return -ENODEV;
}

pr_debug("queue depth: %u\n", depth);

dev_err() はデバイスコンテキストを含みます。デバイスが存在しない場合は pr_err() が適切です。dynamic debug を使うと、恒久的なログノイズを増やすことなく、選択した pr_debug() および dev_dbg() の呼び出し箇所を有効化できます。pr_warn_ratelimited() などのレート制限付きバリアントは、繰り返し発生する状態によってログが埋め尽くされることを防ぎます。

予期される失敗は、負のエラー番号として返します。WARN_ON() と WARN_ON_ONCE() は破られた不変条件を報告して処理を継続するため、呼び出し側はその結果生じる状態を引き続き処理しなければなりません。BUG() は現在の実行パスを終了させ、システムを不安定化させる可能性があります。したがって、カーネルのガイダンスでは、実行を継続できない場合に限定して使用します。条件をアサートせずに診断コンテキストが必要な場合、dump_stack() はコールチェーンを記録します。

エラー値ポインター #

通常はポインターを返す関数でも、ERR_PTR() を使って負のエラー番号をエンコードできます。

c
struct object *object_create(unsigned long flags)
{
    if (flags & ~OBJECT_VALID_FLAGS)
        return ERR_PTR(-EINVAL);

    /* Allocate and initialize the object. */
}

struct object *object = object_create(flags);
if (IS_ERR(object))
    return PTR_ERR(object);

include/linux/err.h は ERR_PTR()、PTR_ERR()、IS_ERR()、IS_ERR_OR_NULL()、PTR_ERR_OR_ZERO() を定義しています。現在の汎用コードでは、MAX_ERRNO が 4095 である -MAX_ERRNO までのエラー値を予約していますが、呼び出し側はその表現を不透明なものとして扱わなければなりません。関数の契約では、有効なポインター、NULL、エラーポインターを区別する必要があります。!ptr だけを検査すると、エンコードされたエラーを見逃します。

分岐予測、プレフィックス、アーキテクチャフック #

汎用の likely(x) および unlikely(x) マクロは、分岐プロファイリングが無効な場合に __builtin_expect() を使用します。

c
if (unlikely(!object))
    return -ENOMEM;

このヒントは、期待される値をコンパイラーに伝えます。ブロックの配置や、どのエッジがフォールスルーになるかに影響する可能性がありますが、ソースコードから x86 の分岐ヒントプレフィックスが出力されるとは限りません。現代のコンパイラーとプロセッサーの動作では、特定のプレフィックスは実装詳細です。命令レベルの主張を行う前に、生成されたオブジェクトを調べてください。

bash
objdump -drS path/to/object.o

CONFIG_TRACE_BRANCH_PROFILING を有効にすると、include/linux/compiler.h はこれらのマクロを __branch_check__() 経由で展開します。この展開は、専用セクションにファイル、関数、行のデータを記録し、ftrace_likely_update() を呼び出します。一方、likely_notrace() はインストルメンテーションされない形式を維持します。各分岐に手書きのログ呼び出しを追加するのではなく、生成されたメタデータによって分岐ヒットの診断が可能になります。

static key は別の問題を解決します。static_branch_likely() と関連 API は、実行頻度は低く変化するものの、ホットパスで評価される条件を表します。汎用の jump-label コードは、arch_static_branch() などのアーキテクチャフックを呼び出します。x86 では、arch/x86/include/asm/jump_label.h がパッチサイトと __jump_table エントリーを出力します。実行時のパッチ適用によって NOP がジャンプに、またはその逆に変更されます。したがって、無効化されたパスでは、繰り返し評価されるロードと条件分岐を回避できます。

任意の可変ブール値の代わりに static key を使用しないでください。更新では実行可能テキストにパッチを適用するため、最適化された読み取りパスを評価するより意図的に高コストです。

汎用インターフェースとアーキテクチャオーバーライド #

カーネルヘッダーは安定した汎用 API を公開し、<asm/...> がアーキテクチャ実装を提供することがよくあります。一般的な構成は次のとおりです。

  1. サブシステムが汎用 Linux ヘッダーをインクルードする。
  2. そのヘッダーが arch/<arch>/include/asm を通じてアーキテクチャ選択済みヘッダーをインクルードする。
  3. アーキテクチャヘッダーがフックを実装するか、asm-generic のフォールバックをインクルードする。

例として、バリア、アトミック操作、スピンロック、jump label、one-copy アクセスヘルパーがあります。公開マクロとそのアーキテクチャフックの両方を検索してください。ラッパーで止めると、重要な命令シーケンスを見落とす可能性があります。

生成ヘッダーとテーブルも別の層を形成します。Kconfig は構成ヘッダーを生成し、システムコールおよびオフセット関連の仕組みは ABI 依存の定数やテーブルを生成します。また、リンカーは initcall、トレースメタデータ、jump label、エクスポートシンボルに使用される、特別な名前を持つセクションを収集します。include/generated/ にある生成物はビルド結果の証拠であり、権威あるソースではありません。編集する前に、それを生成したスクリプト、入力テーブル、Kconfig シンボル、またはリンカースクリプトを特定してください。

One-Copy アクセス #

READ_ONCE() と WRITE_ONCE() は、アーキテクチャおよび汎用の rwonce ヘッダーを通じて定義されます。これらは、マークされたアクセスをコンパイラーが統合、生成、再フェッチすることを防ぎ、適切な one-copy 操作間でコンパイラーから見える順序を保持します。

c
if (READ_ONCE(state->ready))
    consume(READ_ONCE(state->value));

これらだけでは、CPU 間の順序付け、相互排他、アトミックな read-modify-write 操作は提供されません。データの公開には、smp_store_release() や smp_load_acquire() などの release/acquire 操作、またはロックが必要になることがよくあります。単に race detector を黙らせるために READ_ONCE() を追加するのではなく、並行性プロトコルからプリミティブを選択してください。

include/asm-generic/rwonce.h の汎用ソースでは、中心的なユースケースとして、1 CPU 上のプロセスコードと割り込み/NMI コード間の通信、および明示的なバリアまたはアトミック命令によって順序付けが提供されるアクセスの 2 つを説明しています。

SLUB アロケーションパスの追跡 #

SLUB は主に mm/slub.c に実装され、共通の slab インターフェースは mm/slab.h に、公開アロケーション API は <linux/slab.h> にあります。呼び出し側は、次のようなインターフェースを通じてオブジェクトを要求します。

c
object = kmem_cache_alloc_node(cache, GFP_KERNEL, node);
if (!object)
    return -ENOMEM;

/* Use object. */

kmem_cache_free(cache, object);

公開呼び出しは、インライン関数、アロケーションフック、デバッグまたはサニタイザーのインストルメンテーション、そして高速パスまたは低速パスを通過します。正確な内部動作は、SLUB デバッグや KASAN などの構成シンボルに依存します。現在の mm/slub.c は、オーバーヘッドの低い CPU ごとのキャッシュ、利用可能な場合は compare-and-exchange による、ほぼロックレスな低速パスでの解放、部分 slab のプールに対する集中管理について説明しています。別のカーネルバージョンに由来する過去のコメントを、現在のアルゴリズム仕様として使用してはなりません。

アロケーションフラグは呼び出しパスの契約の一部です。GFP_KERNEL は reclaim とスリープを許可します。アトミックコンテキストでは、適切な非スリープのアロケーション戦略が必要です。ノード選択は NUMA 上の優先指定であり、ポリシーと利用可能性の影響を受けます。したがって、アロケーションが決してフォールバックしないという保証ではありません。

ソース位置を特定するワークフロー #

不慣れな内部機構を調べる場合は、コンパイルされたパスを追跡します。

  1. 呼び出し箇所から開始し、ローカルの Makefile と Kconfig にあるビルド条件を記録する。
  2. git grep で公開シンボルを検索し、マクロエイリアス、static inline、関数ポインター、アーキテクチャフックを追跡する。
  3. 生成された構成と対象アーキテクチャを確認する。ソース中の無効な分岐が権威あるものに見える場合があるためである。
  4. プリプロセスまたは命令生成が問題となる場合は、make path/to/file.i または .s を使用する。
  5. リンクされたオブジェクトを nm、readelf、または objdump で調べる。ftrace、tracepoint、perf probe は、関連する実行時パスを特定してから使用する。
  6. 同じカーネルリビジョンのドキュメントおよびソースと比較する。

ソースパスは有用な座標ですが、永続的な API 契約ではありません。内部構造とヘルパー名はリリース間で変更される可能性があります。そのため、正確なレイアウトが重要な場合、記事やデバッグメモでは、文書化されたインターフェースまたはバージョン付きのソースリビジョンにリンクしてください。

参考資料 #

コメント

GitHub で閲覧・コメントする(投稿にはログインが必要です)