Keyboard shortcuts

Press or to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

[インデックス 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のサイクルとは独立してバックグラウンドで動作し、必要に応じて長時間ブロックされることがあります。
  • issystemisbackground フラグ: 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となり、デッドロック検出器がスカベンジャーを通常のユーザーゴルーチンとして扱ってしまい、誤検知を引き起こしていました。

このコミットでは、このタイミングの問題を解決するために、以下の変更を行っています。

  1. scvgグローバル変数の廃止: スカベンジャーゴルーチンを識別するためのstatic G *scvg;というグローバル変数が削除されました。これにより、特定のゴルーチンをグローバル変数で追跡する必要がなくなります。
  2. isbackgroundフラグの導入と早期設定:
    • runtime.G構造体に新たにbool isbackground;というフィールドが追加されました。このフラグは、そのゴルーチンがデッドロック検出器によって無視されるべきバックグラウンドゴルーチンであることを示します。
    • runtime·MHeap_Scavenger関数(スカベンジャーゴルーチンのエントリポイント)の冒頭で、現在実行中のゴルーチン(g)に対してg->issystem = true;g->isbackground = true;が設定されるようになりました。これにより、スカベンジャーゴルーチンが起動され、その実行が開始された直後に、自身がシステムゴルーチンであり、かつバックグラウンドゴルーチンであることを明示的にマークします。この設定は、ゴルーチンがentersyscall()を呼び出すよりも前に確実に行われます。
  3. デッドロック検出ロジックの変更:
    • entersyscall()exitsyscall()、およびcheckdead()関数内のデッドロック検出ロジックが、g == scvgというチェックからg->isbackgroundというチェックに変更されました。これにより、特定のグローバル変数に依存するのではなく、ゴルーチン自身のisbackgroundフラグの状態に基づいて、デッドロック検出から除外するかどうかが判断されるようになります。

この変更により、スカベンジャーゴルーチンは起動直後に自身を「バックグラウンドゴルーチン」としてマークするため、デッドロック検出器がそのゴルーチンを誤ってデッドロックの一部と判断することがなくなります。これにより、デッドロック検出器の正確性が向上し、誤検知による予期せぬプログラム終了が防止されます。

コアとなるコードの変更箇所

このコミットによる主要なコード変更は以下の3つのファイルにわたります。

  1. src/pkg/runtime/mheap.c:

    • runtime·MHeap_Scavenger関数の冒頭に、現在のゴルーチンgissystemisbackgroundフラグを設定する行が追加されました。
      // src/pkg/runtime/mheap.c
      +	g->issystem = true;
      +	g->isbackground = true;
      
  2. 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)
      
  3. 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による識別へと変更した点にあります。

  1. isbackgroundフラグの導入: runtime.hG構造体にisbackgroundという新しいブーリアンフィールドが追加されました。このフィールドは、そのゴルーチンがランタイムのバックグラウンドタスクであり、デッドロック検出の対象から除外されるべきであることを明示的に示します。これにより、デッドロック検出器は、特定のゴルーチンがスカベンジャーであるかどうかをグローバル変数で判断するのではなく、各ゴルーチンが持つこのフラグを直接参照できるようになります。

  2. スカベンジャーゴルーチンでのisbackground設定: mheap.c内のruntime·MHeap_Scavenger関数は、スカベンジャーゴルーチンのエントリポイントです。この関数の冒頭で、現在実行中のゴルーチンgに対してg->issystem = true;g->isbackground = true;が設定されます。この変更の重要な点は、runtime·newproc1によってスカベンジャーゴルーチンが起動され、その実行が開始された「直後」に、このフラグが設定されることです。これにより、スカベンジャーゴルーチンがentersyscall()などのデッドロック検出に関連する関数を呼び出すよりも前に、自身がバックグラウンドゴルーチンであることを確実にマークできます。

  3. デッドロック検出ロジックの更新: proc.c内のentersyscall()exitsyscall()checkdead()といった関数は、ゴルーチンの状態遷移やデッドロック検出の際に呼び出されます。これらの関数では、以前はg == scvgという比較によってスカベンジャーゴルーチンを識別し、デッドロック検出から除外していました。このコミットでは、この比較がg->isbackgroundというより汎用的なチェックに置き換えられました。

    • entersyscall()exitsyscall()では、ゴルーチンがシステムコールに入ったり出たりする際に、デッドロック検出器の内部カウンタ(inclocked)を調整します。バックグラウンドゴルーチンはデッドロック検出の対象外であるため、これらのゴルーチンがシステムコールでブロックされても、デッドロック検出器のカウンタに影響を与えないようにします。
    • checkdead()は、すべてのゴルーチンがブロックされているかどうかを判断し、デッドロックを検出する主要な関数です。この関数も、gp->isbackgroundtrueであるゴルーチンをデッドロック検出の対象から除外するようになりました。

これらの変更により、スカベンジャーゴルーチンは、そのライフサイクルの非常に早い段階で自身を「デッドロック検出から除外されるべきバックグラウンドゴルーチン」としてマークします。これにより、デッドロック検出器がスカベンジャーゴルーチンを誤ってデッドロックの一部と判断する可能性が完全に排除され、デッドロック検出器の信頼性が向上しました。

関連リンク

参考にした情報源リンク

  • Goのソースコード(特にsrc/runtimeディレクトリ)
  • Goのデッドロック検出に関するドキュメントやブログ記事(一般的なGoランタイムの動作に関する情報)
  • Goのガベージコレクションに関するドキュメント(スカベンジャーゴルーチンの役割に関する情報)
  • Go issue #5025の議論(問題の詳細と背景を理解するため)
  • Go CL 7629045のコードレビューコメント(変更の意図と詳細を理解するため)
  • GoのGMPスケジューリングモデルに関する解説記事