同期デバッグ:依存関係、競合、およびストール

このページの目次

同じくフリーズして見える現象でも、ロック順序の循環、通知の取り逃し、あるいは単にロック保持者が CPU を獲得できないままいるだけの場合がある。まず、どの進捗が不足しているかを明確にし、証拠を選択する。

参照目標参照箇所
依存関係と競合の統計第 1~4 節
データ競合とコンテキスト第 5~6 節
ストールの種類と修正の検証第 7~9 節

同期問題には、デッドロック、データ競合、ライブロック、長時間のロック保持、およびウェイクアップの喪失が含まれる。これらはすべて「フリーズ」として現れる可能性があるが、必要な証拠は異なる。誰が進捗していないか、何を待っているかを判断し、ツールを選択せよ。すべてのアラートを「ロックが解放されていない」として一括りにしてはならない。

1. 各ツールが回答すること

ツール/メカニズム主な回答証明できないこと
lockdep / PROVE_LOCKING観察された依存関係は、ロック順序またはコンテキストの競合を形成する可能性があるか?未実行のパスが必ずしも正しいわけではない
lockstatどのロッククラスが競合が多く、待機時間や保持時間が長いのか?長い待機時間が必ずしもデッドロックを意味するわけではない
KCSANサンプリングで観察されたメモリアクセスは、競合や関連問題を露呈しているか?1回のレポートなしで競合がないわけではない
DEBUG_ATOMIC_SLEEP現在のコンテキストで、スリープする可能性のある操作が誤って呼び出されていないか?すべてのブロッキング API のビジネスロジックによる待機が正しいわけではない
hung task特定の不可分断待機状態にあるタスクが、長期間進捗していないか?待機の原因が必ずしも mutex であるわけではない
soft/hard lockupCPU のスケジューリングまたは割り込みの進捗が異常ではないか?特定のアプリケーションが必ず誤ったロックを保持しているわけではない
RCU stallグレース期間または関連する処理が、長期間進捗していないか?必ず unlock が欠けているわけではない

2. デッドロック発生前に lockdep が報告できる理由

タスクがロッククラス A を保持し、B を取得する際、lockdep は A→B を記録する。別のパスで B→A が発生した場合、それらが実際に同時にフリーズしていなくても、潜在的な循環が形成されている。より長い依存チェーンも同様に分析が必要である。

別々に実行された2つのパスでも、依存関係の循環を露呈できる パス1でまずAを保持 次にBを取得 A → B を記録 次にAを取得 パス2でまずBを保持 B → A を記録 lockdep は観察されたロッククラスの依存関係を統合します。2つのパスが実際に同時にデッドロックするのを待たずに報告します。

lockdep は、hardirq/softirq などの使用状態も追跡する。例えば、あるロックが割り込みハンドラで使用され、別のパスでその割り込みによる割り込みが許可される状態でそれを保持している場合、この CPU 上の自己デッドロック(スピンロックの再帰取得など)が発生する可能性がある。このようなコンテキストの競合は、異なる CPU 間の ABBA 順序をチェックするだけでは発見できない。

過去に観察された取得順序をグラフに1つずつ追加していく。エッジは異なる時間からのものであってもよく、閉路が形成されれば調査が必要な順序の矛盾が存在することになる。2つのスレッドが同時に現場でフリーズするのを待つ必要はない。

図を準備しています
同時にデッドロックしていなくても、依存関係の循環は検出可能

lockdep は観察されたロッククラスの依存関係を統合する。異なる時間に発生した A→B と B→A も潜在的な循環を形成し、実際のタスクが同時にフリーズするのを待たなくてよい。

ロッククラスとロックインスタンス

ロッククラスは通常、「すべての spinlock が1クラス」または「異なる取得関数が必ず異なるクラス」といったものではなく、初期化位置や明示的な class key によって決定される。同じクラスの複数のオブジェクト内のロックは、同じクラスに分類できる。1つのオブジェクトが異なる関数で取得されたとしても、自動的に異なるクラスになるわけではない。

アルゴリズムが確かに親-child の階層に従って同じクラスのオブジェクトロックを同時に取得するのであれば、証明された順序を表現するために適切な subclass/nested API を使用できる。ただし、subclass を追加するのは検証器に情報を追加するだけであり、実際の順序が自動的に正しくなるわけではない。誤った注釈は、真の問題を隠蔽する可能性がある。

レポートには、現在の取得位置、既存の依存関係、および関連するスタックが含まれており、矛盾するパスの再構築に使用される。これは起動以来のすべてのロック保持の完全な履歴録画ではない。lockdep の設計 を参照。

3. lockdep レポートの読み方

レポートに基づいて依存関係を再構築し、最後にトリガーされた位置だけを修正しないでください:

  1. 現在取得しようとしているロックと、すでに保持しているロックを特定する。
  2. 既存の依存関係に沿って、逆方向のパスを1つまたは複数復元する。
  3. オブジェクトインスタンス、並行実行の可能性、および割り込み/プリエンプションの状態を確認する。

これらの条件が、依存関係の循環がどのように形成されるか、およびロック順序を修正すべきかオブジェクトの関係を修正すべきかを決定する。

アラートをトリガーした最後の lock 呼び出しだけを修正してはならない。一般的な修正としては、ロック順序の統一、クリティカルセクションの縮小、段階的な操作の実行、ロック保持中のコールバック待機の回避、またはオブジェクト関係の再設計がある。階層注釈を調整する場合は、同時に保持されるオブジェクトがその階層を必ず満たすという根拠を示す必要がある。

lockdep_assert_held() は、内部関数の「呼び出し元が特定のロックを保持している必要がある」という前提を、チェック可能な契約に変換できる。長時間テストでレポートがない場合でも、検証器が実際に有効であり、早期のエラー後にチェックが停止しておらず、関連するエラーとエグジットパスをカバーしていることを確認する必要がある。

4. Lockstat:競合回数、待機時間、保持時間を組み合わせて見る

/proc/lock_stat は、対応する構成/実行制御が有効になっている場合に、ロッククラスの統計情報をレポートする。これには、取得、競合、待機時間、保持時間などのフィールドが含まれる。時間単位は、バージョンに対応する見出し/ドキュメントを読んで確認すべきであり、固定フィールドのナノ秒表を偽造して2列目に基づいて盲目的にソートしてはならない。

統計的な振る舞い次の検証ステップ
取得が多く、競合が少ないこのロックに最適化の価値があるか
競合が少ないが、1回の待機が非常に長いロック保持者が低速 I/O、プリエンプション、スケジューリングの制限、または別のロックの待機をしているのではないか
競合が多く、1回の保持時間が短いキャッシュラインの共有、ロックの粒度、およびシャーディングの機会

回数と時間は組み合わせて考慮すべきであり、1つの最大値に基づいて根本原因を特定してはならない。

測定がスケジューリング/タイミングを変更する可能性がある場合は、有効化前後の負荷パフォーマンスを比較すること。シャーディングによる競合の軽減は、集約コストと整合性の難易度を増加させる可能性があり、lockstat の数値を小さくすることだけを追求してはならない。lockstat ドキュメント を参照。

5. KCSAN:サンプリング検出であり、すべてのアクセスに完全なタイムスタンプを記録するものではない

KCSAN は、コンパイラインストルメンテーションとソフトウェアウォッチポイント/遅延などのメカニズムを使用して、競合するアクセスをサンプリング観察する。すべてのメモリ操作に対して正確な履歴の happens-before グラフを構築するものではない。構成はサンプリング、遅延、およびアクセスタイプの処理を変更するため、オーバーヘッドが常に10倍であるとは限らない。

レポート内のアドレス、サイズ、読み書きタイプ、および2つのスタックが分析の入り口となる。2つのアクセスに共通のロックがないからといって、必ずしも誤りであるわけではない。それらはアトミック/リリースプロトコルによって同期されている可能性があるからである。逆に、コードに1つのロックが存在しても、すべてのアクセスがそれによって保護されているわけではない。

READ_ONCE/WRITE_ONCE、atomic、data_race() などのマーキングは、実際のプロトコルを反映しなければならない。レポート対象の位置に data_race() を適用するのは、「これは判断により許可された競合である」と表現するだけであり、順序、アトミック性、または寿命を修正するものではない。例外については、一貫性のない値の読み取りが無害である理由と、その後の使用が安全である理由を説明する必要がある。

ロックフリーリリースの問題については、Linux Kernel Memory Model のリトマステストを使用して許容される結果を分析し、KCSAN およびストレステストと組み合わせることができる。ユーザー空間の C/C++ アトミックモデルの直感を、検証せずにカーネルメモリモデルに置き換えてはならない。KCSAN および LKMM を参照。

6. アトミックコンテキストでのスリープと待機プロトコル

CONFIG_DEBUG_ATOMIC_SLEEP などのチェックは、通常、might_sleep などの位置で「無効なコンテキストからのスリープ関数呼び出し」としてレポートされる。このような診断が必ずしも即座の BUG/panic を意味するわけではない。具体的な処理はパスと構成の影響を受ける。

保持しているロック、プリエンプションカウント、割り込み状態、および呼び出しチェーンを確認し、raw spinlock、hardirq、またはスリープする可能性のある API(メモリ割り当てなど)を呼び出すことができない場所で呼び出されていないことを確認する。ある割り当てがたまたまファストパスにヒットしてブロックしなかったからといって、そのような呼び出しが合法である証明にはならない。

ウェイクアップの喪失は、必ずしもロック依存関係の循環を生成しない。スレッドは条件チェック後、実際に待機を登録する前に、唯一のウェイクアップを見逃す可能性がある。wait_event/completion などの正しいプロトコルを使用し、タイムアウト、キャンセル、およびエグジット後のオブジェクト寿命を確認する必要がある。

7. Hung task、softlockup、および hardlockup

Hung task は、長期間不可分断待機状態にあり、進捗がないタスクに焦点を当てる。D 状態は、ストレージ、ネットワークファイルシステム、completion、またはロックを待っている可能性がある。タイムアウトしきい値は構成可能であり、120秒はシステム間で不変の定数ではない。

Softlockup は、CPU が長期間、ウォッチドッグ関連タスクにスケジューリングの進捗を与えていないことを示す。割り込みの処理は継続している可能性がある。長時間の非プリエンプティブループ、スケジューリングの飢餓などが影響を与える。

Hardlockup は、一般的な NMI/perf 実装において、NMI 経由で通常タイマー割り込みの進捗をチェックできるかどうかによって検出される。これは、「通常割り込みがオフになっているため、NMI も受信できない」というように判断されるものではない。検出器の実装は、アーキテクチャ、PMU、構成、および仮想化サポートに応じて異なる。VM の一時停止などもタイミングに影響を与える。

これら3つは RCU stall と同時に発生する可能性があるが、異なる進捗の不足を表現する。重複するスタックと待機関係を探し、共通の根本原因を判断する。lockup watchdog を参照。

8. 検出能力としきい値をリードオンリーで確認

from pathlib import Path

names = ('hung_task_timeout_secs', 'hung_task_panic', 'watchdog',
         'watchdog_thresh', 'nmi_watchdog', 'softlockup_panic')
for name in names:
    path = Path('/proc/sys/kernel') / name
    try:
        print(name, path.read_text().strip())
    except OSError as exc:
        print(name, type(exc).__name__)
for path in ('/proc/lockdep_stats', '/proc/lock_stat'):
    try:
        with open(path) as source:
            head = ''.join(next(source, '') for _ in range(5))
        print(path, head.strip())
    except OSError as exc:
        print(path, type(exc).__name__)

状態ファイルが存在しない場合は、構成が有効になっていない可能性がある。権限拒否は、機能が存在しないことを意味するものではない。/proc/<pid>/stack には対応する構成とアクセス権限が必要である。1つのタスクの1回のスタックはスナップショットに過ぎず、必要に応じて他のタスクおよび重複サンプルと関連付けること。

9. 修正後の検証方法

問題と同じ構成とコンテキストを使用して、まず既知のトリガーパスをカバーし、次に並行更新、エラー戻り値、タイムアウト、およびエグジットクリーンアップをカバーする。ロック順序の問題については、依存関係がもはや競合しないことを確認し、競合の問題については不変条件およびリリース/寿命プロトコルが成立していることを確認する。パフォーマンスの問題については、待機分布とビジネスレイテンシーを再測定する。

デテクターの無効化、しきい値の引き上げ、または注釈の追加によって説明を代用してはならない。ツールのレポートは証拠であり、最終的な結論は特定の不変条件、ロック順序、およびオブジェクトのライフサイクルに収束する必要がある。