[インデックス 15716] ファイルの概要
このコミットは、Goランタイムのデッドロック検出器における誤検知(false negative)を修正するものです。具体的には、スカベンジャー(scavenger)ゴルーチンがデッドロック検出の対象から正しく除外されるように、そのシステムゴルーチンとしての属性管理を改善しています。
コミット
commit 6ee739d7e9473d772a371e6f774a424bfcbbb0fc
Author: Dmitriy Vyukov <dvyukov@google.com>
Date: Tue Mar 12 17:21:44 2013 +0400
runtime: fix deadlock detector false negative
The issue was that scvg is assigned *after* the scavenger goroutine is started,
so when the scavenger calls entersyscall() the g==scvg check can fail.
Fixes #5025.
R=golang-dev, iant
CC=golang-dev
https://golang.org/cl/7629045
GitHub上でのコミットページへのリンク
https://github.com/golang/go/commit/6ee739d7e9473d772a371e6f774a424bfcbbb0fc
元コミット内容
このコミットの元のメッセージは以下の通りです。
runtime: fix deadlock detector false negative
The issue was that scvg is assigned *after* the scavenger goroutine is started,
so when the scavenger calls entersyscall() the g==scvg check can fail.
Fixes #5025.
これは、Goランタイムのデッドロック検出器がスカベンジャーゴルーチンを誤ってデッドロックの一部と判断してしまう「false negative」(誤検知)の問題を修正するものです。問題の根源は、スカベンジャーゴルーチンが開始された後にscvg
というグローバル変数に割り当てられていたため、スカベンジャーがentersyscall()
を呼び出す際にg==scvg
というチェックが失敗する可能性があったことにあります。この修正は、Go issue #5025を解決します。
変更の背景
Goランタイムには、プログラムがデッドロック状態に陥ったことを検出するメカニズムが組み込まれています。デッドロックとは、複数のゴルーチンが互いにリソースを待機し、どのゴルーチンも処理を進められない状態を指します。Goのデッドロック検出器は、すべてのゴルーチンがブロックされており、かつ、どのゴルーチンも実行可能でない場合にデッドロックと判断し、プログラムを終了させます。
しかし、一部のシステムゴルーチン(例えば、ガベージコレクションの一部であるスカベンジャーゴルーチン)は、その性質上、長時間ブロックされることがあります。これらのゴルーチンは、ユーザーコードのデッドロックとは異なり、ランタイムの正常な動作の一部としてブロックされるため、デッドロック検出器からは除外される必要があります。
このコミット以前は、スカベンジャーゴルーチンをデッドロック検出から除外するために、そのゴルーチンがscvg
という特定のグローバル変数に格納されているかどうかをチェックしていました。しかし、スカベンジャーゴルーチンが起動された直後、まだscvg
変数にそのゴルーチンへのポインタが割り当てられる前に、スカベンジャーがシステムコール(entersyscall()
)に入ることがありました。このタイミングのずれにより、g==scvg
のチェックが失敗し、スカベンジャーゴルーチンがデッドロック検出器によって誤ってデッドロックの一部と見なされてしまう「false negative」(デッドロックではないのにデッドロックと判断される)が発生していました。
この誤検知は、Goプログラムが実際にはデッドロックしていないにもかかわらず、ランタイムによってデッドロックと判断され、予期せず終了してしまうという問題を引き起こしていました。
前提知識の解説
このコミットを理解するためには、以下のGoランタイムの概念に関する知識が必要です。
- ゴルーチン (Goroutine): Goにおける軽量な実行スレッドです。Goランタイムによってスケジューリングされ、並行処理を実現します。
- スケジューラ (Scheduler): Goランタイムの一部で、ゴルーチンをOSスレッドにマッピングし、実行を管理します。
- M (Machine): OSスレッドを表すランタイムの内部構造体です。
- P (Processor): 論理プロセッサを表すランタイムの内部構造体で、Mとゴルーチンを関連付けます。Pは実行可能なゴルーチンを保持するキューを持ちます。
- G (Goroutine): ゴルーチンを表すランタイムの内部構造体です。
- システムコール (System Call): プログラムがOSの機能(ファイルI/O、ネットワーク通信など)を利用するためにOSに要求を出すことです。Goのゴルーチンがシステムコールを実行する際には、
entersyscall()
やexitsyscall()
といったランタイム関数が呼び出されます。 - デッドロック検出器 (Deadlock Detector): Goランタイムに組み込まれた機能で、すべてのゴルーチンがブロックされ、かつ実行可能なゴルーチンが存在しない場合に、プログラムがデッドロック状態にあると判断し、エラーを報告して終了します。
- スカベンジャーゴルーチン (Scavenger Goroutine): Goのガベージコレクション(GC)の一部として動作するシステムゴルーチンです。ヒープメモリの解放や、未使用のメモリ領域をOSに返却する(scavenge)役割を担います。このゴルーチンは、GCのサイクルとは独立してバックグラウンドで動作し、必要に応じて長時間ブロックされることがあります。
issystem
とisbackground
フラグ:runtime.G
構造体(ゴルーチンを表す)に存在するフラグです。issystem
: そのゴルーチンがシステムゴルーチンであるかを示します。システムゴルーチンは、スタックダンプに出力されないなど、通常のユーザーゴルーチンとは異なる扱いを受けます。isbackground
: そのゴルーチンがデッドロック検出器によって無視されるべきバックグラウンドゴルーチンであるかを示します。
技術的詳細
この修正は、スカベンジャーゴルーチンをデッドロック検出から正しく除外するために、その属性設定のタイミングと方法を変更しています。
以前のGoランタイムでは、スカベンジャーゴルーチン(scvg
)は、runtime.main
関数内でruntime·newproc1(&scavenger, ...)
によって起動された後、明示的にscvg = runtime·newproc1(...)
という代入によってグローバル変数scvg
に割り当てられ、その後にscvg->issystem = true;
と設定されていました。
問題は、runtime·newproc1
がゴルーチンを起動し、そのゴルーチンがすぐに実行を開始する可能性があることです。スカベンジャーゴルーチンは起動後すぐにentersyscall()
を呼び出す可能性がありましたが、その時点ではまだscvg
グローバル変数にそのゴルーチンへのポインタが割り当てられていませんでした。そのため、entersyscall()
やexitsyscall()
、そしてデッドロック検出を行うcheckdead()
関数内でg == scvg
というチェックが行われた際に、スカベンジャーゴルーチン自身が実行中であるにもかかわらず、この条件がfalse
となり、デッドロック検出器がスカベンジャーを通常のユーザーゴルーチンとして扱ってしまい、誤検知を引き起こしていました。
このコミットでは、このタイミングの問題を解決するために、以下の変更を行っています。
scvg
グローバル変数の廃止: スカベンジャーゴルーチンを識別するためのstatic G *scvg;
というグローバル変数が削除されました。これにより、特定のゴルーチンをグローバル変数で追跡する必要がなくなります。isbackground
フラグの導入と早期設定:runtime.G
構造体に新たにbool isbackground;
というフィールドが追加されました。このフラグは、そのゴルーチンがデッドロック検出器によって無視されるべきバックグラウンドゴルーチンであることを示します。runtime·MHeap_Scavenger
関数(スカベンジャーゴルーチンのエントリポイント)の冒頭で、現在実行中のゴルーチン(g
)に対してg->issystem = true;
とg->isbackground = true;
が設定されるようになりました。これにより、スカベンジャーゴルーチンが起動され、その実行が開始された直後に、自身がシステムゴルーチンであり、かつバックグラウンドゴルーチンであることを明示的にマークします。この設定は、ゴルーチンがentersyscall()
を呼び出すよりも前に確実に行われます。
- デッドロック検出ロジックの変更:
entersyscall()
、exitsyscall()
、およびcheckdead()
関数内のデッドロック検出ロジックが、g == scvg
というチェックからg->isbackground
というチェックに変更されました。これにより、特定のグローバル変数に依存するのではなく、ゴルーチン自身のisbackground
フラグの状態に基づいて、デッドロック検出から除外するかどうかが判断されるようになります。
この変更により、スカベンジャーゴルーチンは起動直後に自身を「バックグラウンドゴルーチン」としてマークするため、デッドロック検出器がそのゴルーチンを誤ってデッドロックの一部と判断することがなくなります。これにより、デッドロック検出器の正確性が向上し、誤検知による予期せぬプログラム終了が防止されます。
コアとなるコードの変更箇所
このコミットによる主要なコード変更は以下の3つのファイルにわたります。
-
src/pkg/runtime/mheap.c
:runtime·MHeap_Scavenger
関数の冒頭に、現在のゴルーチンg
のissystem
とisbackground
フラグを設定する行が追加されました。// src/pkg/runtime/mheap.c + g->issystem = true; + g->isbackground = true;
-
src/pkg/runtime/proc.c
:scvg
グローバル変数の宣言が削除されました。// src/pkg/runtime/proc.c -// Keep trace of scavenger's goroutine for deadlock detection. -// static G *scvg;
runtime·main
関数内で、scvg
への代入とscvg->issystem
の設定が削除されました。// src/pkg/runtime/proc.c - scvg = runtime·newproc1(&scavenger, nil, 0, 0, runtime·main); - scvg->issystem = true; + runtime·newproc1(&scavenger, nil, 0, 0, runtime·main);
entersyscall()
、exitsyscall()
、checkdead()
関数内で、g == scvg
のチェックがg->isbackground
のチェックに置き換えられました。// src/pkg/runtime/proc.c (entersyscall) - if(g == scvg) // do not consider blocked scavenger for deadlock detection + if(g->isbackground) // do not consider blocked scavenger for deadlock detection // src/pkg/runtime/proc.c (exitsyscall) - if(g == scvg) // do not consider blocked scavenger for deadlock detection + if(g->isbackground) // do not consider blocked scavenger for deadlock detection // src/pkg/runtime/proc.c (checkdead) - if(gp == scvg) + if(gp->isbackground)
-
src/pkg/runtime/runtime.h
:G
構造体(ゴルーチンを表す)にisbackground
フィールドが追加されました。// src/pkg/runtime/runtime.h struct G { // ... bool issystem; // do not output in stack dump + bool isbackground; // ignore in deadlock detector int8 traceignore; // ignore race detection events // ... };
コアとなるコードの解説
このコミットの核心は、スカベンジャーゴルーチンをデッドロック検出器から除外するためのメカニズムを、グローバル変数scvg
による識別から、ゴルーチン自身の属性フラグisbackground
による識別へと変更した点にあります。
-
isbackground
フラグの導入:runtime.h
でG
構造体にisbackground
という新しいブーリアンフィールドが追加されました。このフィールドは、そのゴルーチンがランタイムのバックグラウンドタスクであり、デッドロック検出の対象から除外されるべきであることを明示的に示します。これにより、デッドロック検出器は、特定のゴルーチンがスカベンジャーであるかどうかをグローバル変数で判断するのではなく、各ゴルーチンが持つこのフラグを直接参照できるようになります。 -
スカベンジャーゴルーチンでの
isbackground
設定:mheap.c
内のruntime·MHeap_Scavenger
関数は、スカベンジャーゴルーチンのエントリポイントです。この関数の冒頭で、現在実行中のゴルーチンg
に対してg->issystem = true;
とg->isbackground = true;
が設定されます。この変更の重要な点は、runtime·newproc1
によってスカベンジャーゴルーチンが起動され、その実行が開始された「直後」に、このフラグが設定されることです。これにより、スカベンジャーゴルーチンがentersyscall()
などのデッドロック検出に関連する関数を呼び出すよりも前に、自身がバックグラウンドゴルーチンであることを確実にマークできます。 -
デッドロック検出ロジックの更新:
proc.c
内のentersyscall()
、exitsyscall()
、checkdead()
といった関数は、ゴルーチンの状態遷移やデッドロック検出の際に呼び出されます。これらの関数では、以前はg == scvg
という比較によってスカベンジャーゴルーチンを識別し、デッドロック検出から除外していました。このコミットでは、この比較がg->isbackground
というより汎用的なチェックに置き換えられました。entersyscall()
とexitsyscall()
では、ゴルーチンがシステムコールに入ったり出たりする際に、デッドロック検出器の内部カウンタ(inclocked
)を調整します。バックグラウンドゴルーチンはデッドロック検出の対象外であるため、これらのゴルーチンがシステムコールでブロックされても、デッドロック検出器のカウンタに影響を与えないようにします。checkdead()
は、すべてのゴルーチンがブロックされているかどうかを判断し、デッドロックを検出する主要な関数です。この関数も、gp->isbackground
がtrue
であるゴルーチンをデッドロック検出の対象から除外するようになりました。
これらの変更により、スカベンジャーゴルーチンは、そのライフサイクルの非常に早い段階で自身を「デッドロック検出から除外されるべきバックグラウンドゴルーチン」としてマークします。これにより、デッドロック検出器がスカベンジャーゴルーチンを誤ってデッドロックの一部と判断する可能性が完全に排除され、デッドロック検出器の信頼性が向上しました。
関連リンク
- Go issue #5025: https://code.google.com/p/go/issues/detail?id=5025 (古いGo issueトラッカーのリンクですが、このコミットが修正した問題の元の報告です)
- Go CL 7629045: https://golang.org/cl/7629045 (このコミットに対応するGoのコードレビューリンク)
参考にした情報源リンク
- Goのソースコード(特に
src/runtime
ディレクトリ) - Goのデッドロック検出に関するドキュメントやブログ記事(一般的なGoランタイムの動作に関する情報)
- Goのガベージコレクションに関するドキュメント(スカベンジャーゴルーチンの役割に関する情報)
- Go issue #5025の議論(問題の詳細と背景を理解するため)
- Go CL 7629045のコードレビューコメント(変更の意図と詳細を理解するため)
- Goの
G
、M
、P
スケジューリングモデルに関する解説記事