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

[インデックス 18527] ファイルの概要

このコミットは、Goランタイムの src/pkg/runtime/proc.c ファイルに対する変更です。proc.c はGoランタイムの中核をなすファイルの一つで、ゴルーチン(goroutine)のスケジューリング、M(Machine)とP(Processor)の管理、デッドロック検出、パニック処理など、Goプログラムの実行モデルに関する低レベルな処理を司っています。特に、このファイルにはGoランタイムのデッドロック検出機構である checkdead 関数が含まれています。

コミット

このコミットは、Goランタイムにおける誤解を招くクラッシュ時のメッセージ、特に checkdead 関数が誤ったデッドロック検出メッセージを出力する問題を修正します。具体的には、パニック発生時に checkdead が誤って「デッドロック」と判断してしまう偽陽性(false positive)を排除するための変更です。

GitHub上でのコミットページへのリンク

https://github.com/golang/go/commit/47534ddc68d6c605db2d5ec5726927a244159ad9

元コミット内容

runtime: remove misleading message during crash
The following checkdead message is false positive:

$ go test -race -c runtime
$ ./runtime.test -test.cpu=2 -test.run=TestSmhasherWindowed -test.v
=== RUN TestSmhasherWindowed-2
checkdead: find g 18 in status 1
SIGABRT: abort
PC=0x42bff1

LGTM=rsc
R=golang-codereviews, gobot, rsc
CC=golang-codereviews, iant, khr
https://golang.org/cl/59490046

変更の背景

この変更の背景には、Goランタイムのデッドロック検出機構である checkdead 関数が、プログラムがクラッシュ(特にパニック)している最中に誤ったデッドロックメッセージを出力するという問題がありました。

元のコミットメッセージに示されているように、go test -race を使用して runtime パッケージのテストを実行した際に、TestSmhasherWindowed というテストでクラッシュが発生し、その際に以下のようなメッセージが出力されていました。

checkdead: find g 18 in status 1
SIGABRT: abort
PC=0x42bff1

この checkdead: find g 18 in status 1 というメッセージは、checkdead 関数がゴルーチン g 18status 1 (Grunnable、つまり実行可能状態) であるにもかかわらず、システムがデッドロック状態にあると誤って判断したことを示しています。しかし、実際にはこれはデッドロックではなく、プログラムがパニックによって終了処理を行っている最中の状態でした。

Goランタイムは、パニックが発生すると freezetheworld と呼ばれるメカニズムを発動し、すべてのゴルーチンを停止させて、パニック処理に専念します。この freezetheworld の過程で、一部のゴルーチンが一時的に実行可能な状態(Grunnable)のまま停止しているように見えることがあります。checkdead 関数は、このような一時的な状態をデッドロックと誤認し、誤解を招くメッセージを出力していました。

この誤解を招くメッセージは、ユーザーがデバッグを行う際に混乱を招く可能性がありました。実際のデッドロックではないにもかかわらず、デッドロックが原因であるかのように示唆されるため、問題の根本原因の特定を妨げる恐れがあったのです。したがって、このコミットは、パニック発生時には checkdead がデッドロック検出を行わないようにすることで、この偽陽性を排除することを目的としています。

前提知識の解説

このコミットを理解するためには、以下のGoランタイムの概念を理解しておく必要があります。

  1. ゴルーチン (Goroutine): Goにおける軽量な実行スレッドです。Goランタイムによってスケジューリングされ、並行処理を実現します。ゴルーチンには様々な状態(例: Grunning, Grunnable, Gsyscall, Gwaitingなど)があります。
    • Grunnable (status 1): 実行可能だが、現在実行されていないゴルーチン。スケジューラによって実行されるのを待っている状態。
    • Grunning (status 2): 現在実行中のゴルーチン。
    • Gsyscall (status 4): システムコールを実行中のゴルーチン。
  2. M (Machine) と P (Processor): Goランタイムスケジューラの重要な要素です。
    • M (Machine): オペレーティングシステムのスレッドに相当します。GoランタイムはMを使ってゴルーチンを実行します。
    • P (Processor): 論理プロセッサに相当します。MはPにアタッチされ、Pは実行可能なゴルーチンをキューから取得してMに渡します。Pの数は通常、CPUのコア数に設定されます。
  3. checkdead 関数: Goランタイムのデッドロック検出機構です。この関数は、すべてのゴルーチンがブロックされており、かつ実行可能なゴルーチンが一つも存在しない場合に、システムがデッドロック状態にあると判断します。これは、Goプログラムがハングアップした場合に、その原因がデッドロックであることを診断するために非常に重要です。
  4. runtime.panicking: Goランタイム内部の変数で、現在パニック処理が進行中であるかどうかを示すフラグです。0より大きい値の場合、パニックが発生していることを意味します。
  5. freezetheworld: GoランタイムがパニックやGC(ガベージコレクション)などの特定の重要なイベントが発生した際に、すべてのゴルーチンを一時的に停止させるメカニズムです。これにより、ランタイムは一貫した状態で必要な処理を実行できます。パニック時には、freezetheworld が呼び出され、すべてのゴルーチンが停止された後、パニック処理が開始されます。
  6. Go Race Detector: Goのデータ競合検出ツールです。go test -race コマンドで有効にできます。並行処理におけるデータ競合を検出し、デバッグを支援します。このコミットの例では、レース検出器を有効にしたテスト中に問題が再現しています。

技術的詳細

このコミットが修正する問題は、Goランタイムの checkdead 関数が、パニック処理中に誤ってデッドロックを報告するというものです。

checkdead 関数の主な目的は、Goプログラムがデッドロックに陥ったかどうかを判断することです。これは、すべてのM(OSスレッド)がアイドル状態であるか、またはシステムコール中でブロックされており、かつ実行可能なゴルーチン(Grunnable または Grunning 状態)が一つも存在しない場合に、デッドロックと判断します。

しかし、プログラムがパニックを起こすと、Goランタイムは freezetheworld というメカニズムを発動します。この freezetheworld は、すべてのゴルーチンを停止させ、ランタイムがパニック処理を安全に行えるようにします。この停止の過程で、一部のゴルーチンは Grunnable (実行可能) の状態のまま一時的に停止することがあります。

問題は、checkdead 関数がこの freezetheworld 中の一時的な状態を適切に区別できなかった点にあります。checkdead は、runtime.sched.mcount (Mの総数)、runtime.sched.nmidle (アイドルなMの数)、runtime.sched.nmidlelocked (ロックされたアイドルなMの数) をチェックし、実行中のMがいないと判断した場合に、すべてのゴルーチンの状態を走査します。この走査中に、もし GrunnableGrunningGsyscall 状態のゴルーチンが見つかると、それはデッドロックではないと判断し、runtime.throw("checkdead: runnable g") を呼び出してエラーを報告します。

パニック発生時、freezetheworld によってゴルーチンが停止されると、checkdead が実行されるタイミングによっては、まだ Grunnable 状態のゴルーチンが存在するにもかかわらず、他のMがすべて停止しているために、checkdead がデッドロックと誤認してしまう状況が発生していました。これは、システムが実際にデッドロックしているわけではなく、パニック処理の途中で一時的にそのような状態に見えるだけであるため、「偽陽性」となります。

このコミットは、checkdead 関数が実行される際に、runtime.panicking フラグをチェックすることでこの問題を解決します。runtime.panicking が0より大きい場合(つまり、パニック処理が進行中の場合)、checkdead はデッドロック検出をスキップします。これにより、パニック処理中の一時的なゴルーチンの状態がデッドロックと誤認されることを防ぎ、誤解を招くメッセージの出力を抑制します。

また、runtime·printf の出力に runtime: というプレフィックスが追加されています。これは、ランタイムが出力するメッセージであることを明確にし、他のアプリケーションの出力と区別しやすくするための一般的な改善です。

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

src/pkg/runtime/proc.c ファイルにおいて、以下の変更が行われました。

--- a/src/pkg/runtime/proc.c
+++ b/src/pkg/runtime/proc.c
@@ -2441,8 +2441,14 @@ checkdead(void)
 	run = runtime·sched.mcount - runtime·sched.nmidle - runtime·sched.nmidlelocked - 1;
 	if(run > 0)
 		return;
+	// If we are dying because of a signal caught on an already idle thread,
+	// freezetheworld will cause all running threads to block.
+	// And runtime will essentially enter into deadlock state,
+	// except that there is a thread that will call runtime·exit soon.
+	if(runtime·panicking > 0)
+		return;
 	if(run < 0) {
-\truntime·printf("checkdead: nmidle=%d nmidlelocked=%d mcount=%d\n",
+\truntime·printf("runtime: checkdead: nmidle=%d nmidlelocked=%d mcount=%d\n",
 		\truntime·sched.nmidle, runtime·sched.nmidlelocked, runtime·sched.mcount);
 		\truntime·throw("checkdead: inconsistent counts");
 	}
@@ -2457,7 +2463,7 @@ checkdead(void)
 			grunning++;
 		else if(s == Grunnable || s == Grunning || s == Gsyscall) {
 			runtime·unlock(&allglock);
-\truntime·printf("checkdead: find g %D in status %d\n", gp->goid, s);\
+\truntime·printf("runtime: checkdead: find g %D in status %d\n", gp->goid, s);
 			runtime·throw("checkdead: runnable g");
 		}
 	}

具体的には、以下の2つの変更点があります。

  1. checkdead 関数の冒頭に if(runtime·panicking > 0) return; という行が追加されました。
  2. 既存の runtime·printf の呼び出し2箇所で、フォーマット文字列の先頭に "runtime: " が追加されました。

コアとなるコードの解説

1. if(runtime·panicking > 0) return; の追加

この行がこのコミットの最も重要な変更点です。 checkdead 関数が呼び出された際に、まず runtime·panicking というグローバル変数の値がチェックされます。

  • runtime·panicking0 より大きい場合:これは現在Goランタイムがパニック処理の途中であることを意味します。パニック処理中は、freezetheworld メカニズムによってすべてのゴルーチンが停止され、ランタイムは終了処理に向けて動いています。このような状況では、checkdead がデッドロックを検出しても、それは実際のデッドロックではなく、パニック処理による一時的な状態である可能性が高いです。したがって、この条件が真の場合、checkdead 関数はすぐに return し、それ以降のデッドロック検出ロジックを実行しません。これにより、パニック中の偽陽性デッドロックメッセージの出力を防ぎます。

この変更により、Goランタイムは、真のデッドロックと、パニック処理中の一時的な状態を区別できるようになり、ユーザーに誤解を招く情報を提供することを避けることができます。

2. runtime·printf の出力変更

以下の2つの runtime·printf の呼び出しにおいて、出力される文字列の先頭に "runtime: " というプレフィックスが追加されました。

  • runtime·printf("runtime: checkdead: nmidle=%d nmidlelocked=%d mcount=%d\n", ...)
  • runtime·printf("runtime: checkdead: find g %D in status %d\n", ...)

この変更は機能的なものではなく、主にログの可読性と識別性を向上させるためのものです。Goランタイムが出力するメッセージであることを明示することで、システムログやデバッグ出力において、ランタイムからのメッセージとアプリケーションコードからのメッセージを容易に区別できるようになります。これは、大規模なシステムや複雑なデバッグシナリオにおいて、非常に役立つ改善です。

関連リンク

  • Go Change-ID: https://golang.org/cl/59490046 (元のコミットメッセージに記載されているChange-ID)
    • このChange-IDは、Gerrit (Goプロジェクトが使用するコードレビューシステム) 上のこのコミットのレビューページを指します。ここには、コミットに関する議論、レビューコメント、および最終的な承認プロセスに関する詳細な情報が含まれています。

参考にした情報源リンク

  • Goのソースコード (src/pkg/runtime/proc.c)
  • Goのドキュメント (Goランタイム、ゴルーチン、スケジューラに関する一般的な情報)
  • GoのIssue Tracker (関連するバグ報告や議論がある場合)
  • Goのコードレビューシステム (Gerrit)
  • Goの公式ブログや技術記事 (Goランタイムの内部動作に関する解説)
  • Goのデッドロック検出に関する一般的な情報 (例: Goのデッドロック検出の仕組み)
  • Goのパニック処理に関する一般的な情報 (例: Goのパニックとリカバリ)
  • GoのRace Detectorに関する情報 (例: Go Race Detectorの仕組み)

[インデックス 18527] ファイルの概要

このコミットは、Goランタイムの src/pkg/runtime/proc.c ファイルに対する変更です。proc.c はGoランタイムの中核をなすファイルの一つで、ゴルーチン(goroutine)のスケジューリング、M(Machine)とP(Processor)の管理、デッドロック検出、パニック処理など、Goプログラムの実行モデルに関する低レベルな処理を司っています。特に、このファイルにはGoランタイムのデッドロック検出機構である checkdead 関数が含まれています。

コミット

このコミットは、Goランタイムにおける誤解を招くクラッシュ時のメッセージ、特に checkdead 関数が誤ったデッドロック検出メッセージを出力する問題を修正します。具体的には、パニック発生時に checkdead が誤って「デッドロック」と判断してしまう偽陽性(false positive)を排除するための変更です。

GitHub上でのコミットページへのリンク

https://github.com/golang/go/commit/47534ddc68d6c605db2d5ec5726927a244159ad9

元コミット内容

runtime: remove misleading message during crash
The following checkdead message is false positive:

$ go test -race -c runtime
$ ./runtime.test -test.cpu=2 -test.run=TestSmhasherWindowed -test.v
=== RUN TestSmhasherWindowed-2
checkdead: find g 18 in status 1
SIGABRT: abort
PC=0x42bff1

LGTM=rsc
R=golang-codereviews, gobot, rsc
CC=golang-codereviews, iant, khr
https://golang.org/cl/59490046

変更の背景

この変更の背景には、Goランタイムのデッドロック検出機構である checkdead 関数が、プログラムがクラッシュ(特にパニック)している最中に誤ったデッドロックメッセージを出力するという問題がありました。

元のコミットメッセージに示されているように、go test -race を使用して runtime パッケージのテストを実行した際に、TestSmhasherWindowed というテストでクラッシュが発生し、その際に以下のようなメッセージが出力されていました。

checkdead: find g 18 in status 1
SIGABRT: abort
PC=0x42bff1

この checkdead: find g 18 in status 1 というメッセージは、checkdead 関数がゴルーチン g 18status 1 (Grunnable、つまり実行可能状態) であるにもかかわらず、システムがデッドロック状態にあると誤って判断したことを示しています。しかし、実際にはこれはデッドロックではなく、プログラムがパニックによって終了処理を行っている最中の状態でした。

Goランタイムは、パニックが発生すると freezetheworld と呼ばれるメカニズムを発動し、すべてのゴルーチンを停止させて、パニック処理に専念します。この freezetheworld の過程で、一部のゴルーチンが一時的に実行可能な状態(Grunnable)のまま停止しているように見えることがあります。checkdead 関数は、このような一時的な状態をデッドロックと誤認し、誤解を招くメッセージを出力していました。

この誤解を招くメッセージは、ユーザーがデバッグを行う際に混乱を招く可能性がありました。実際のデッドロックではないにもかかわらず、デッドロックが原因であるかのように示唆されるため、問題の根本原因の特定を妨げる恐れがあったのです。したがって、このコミットは、パニック発生時には checkdead がデッドロック検出を行わないようにすることで、この偽陽性を排除することを目的としています。

前提知識の解説

このコミットを理解するためには、以下のGoランタイムの概念を理解しておく必要があります。

  1. ゴルーチン (Goroutine): Goにおける軽量な実行スレッドです。Goランタイムによってスケジューリングされ、並行処理を実現します。ゴルーチンには様々な状態(例: Grunning, Grunnable, Gsyscall, Gwaitingなど)があります。
    • _Grunnable (status 1): 実行可能だが、現在実行されていないゴルーチン。スケジューラによって実行されるのを待っている状態。
    • _Grunning (status 2): 現在実行中のゴルーチン。
    • _Gsyscall (status 4): システムコールを実行中のゴルーチン。
  2. M (Machine) と P (Processor): Goランタイムスケジューラの重要な要素です。
    • M (Machine): オペレーティングシステムのスレッドに相当します。GoランタイムはMを使ってゴルーチンを実行します。
    • P (Processor): 論理プロセッサに相当します。MはPにアタッチされ、Pは実行可能なゴルーチンをキューから取得してMに渡します。Pの数は通常、CPUのコア数に設定されます。
  3. checkdead 関数: Goランタイムのデッドロック検出機構です。この関数は、すべてのゴルーチンがブロックされており、かつ実行可能なゴルーチンが一つも存在しない場合に、システムがデッドロック状態にあると判断します。これは、Goプログラムがハングアップした場合に、その原因がデッドロックであることを診断するために非常に重要です。
  4. runtime.panicking: Goランタイム内部の変数で、現在パニック処理が進行中であるかどうかを示すフラグです。0より大きい値の場合、パニックが発生していることを意味します。これはユーザープログラムから直接アクセスできる変数ではありませんが、ランタイム内部でパニックの状態を管理するために使用されます。
  5. freezetheworld: GoランタイムがパニックやGC(ガベージコレクション)などの特定の重要なイベントが発生した際に、すべてのゴルーチンを一時的に停止させるメカニズムです。これにより、ランタイムは一貫した状態で必要な処理を実行できます。パニック時には、freezetheworld が呼び出され、すべてのゴルーチンが停止された後、パニック処理が開始されます。
  6. Go Race Detector: Goのデータ競合検出ツールです。go test -race コマンドで有効にできます。並行処理におけるデータ競合を検出し、デバッグを支援します。このコミットの例では、レース検出器を有効にしたテスト中に問題が再現しています。

技術的詳細

このコミットが修正する問題は、Goランタイムの checkdead 関数が、パニック処理中に誤ってデッドロックを報告するというものです。

checkdead 関数の主な目的は、Goプログラムがデッドロックに陥ったかどうかを判断することです。これは、すべてのM(OSスレッド)がアイドル状態であるか、またはシステムコール中でブロックされており、かつ実行可能なゴルーチン(_Grunnable または _Grunning 状態)が一つも存在しない場合に、デッドロックと判断します。

しかし、プログラムがパニックを起こすと、Goランタイムは freezetheworld というメカニズムを発動します。この freezetheworld は、すべてのゴルーチンを停止させ、ランタイムがパニック処理を安全に行えるようにします。この停止の過程で、一部のゴルーチンは _Grunnable (実行可能) の状態のまま一時的に停止することがあります。

問題は、checkdead 関数がこの freezetheworld 中の一時的な状態を適切に区別できなかった点にあります。checkdead は、runtime.sched.mcount (Mの総数)、runtime.sched.nmidle (アイドルなMの数)、runtime.sched.nmidlelocked (ロックされたアイドルなMの数) をチェックし、実行中のMがいないと判断した場合に、すべてのゴルーチンの状態を走査します。この走査中に、もし _Grunnable_Grunning_Gsyscall 状態のゴルーチンが見つかると、それはデッドロックではないと判断し、runtime·throw("checkdead: runnable g") を呼び出してエラーを報告します。

パニック発生時、freezetheworld によってゴルーチンが停止されると、checkdead が実行されるタイミングによっては、まだ _Grunnable 状態のゴルーチンが存在するにもかかわらず、他のMがすべて停止しているために、checkdead がデッドロックと誤認してしまう状況が発生していました。これは、システムが実際にデッドロックしているわけではなく、パニック処理の途中で一時的にそのような状態に見えるだけであるため、「偽陽性」となります。

このコミットは、checkdead 関数が実行される際に、runtime·panicking フラグをチェックすることでこの問題を解決します。runtime·panicking が0より大きい場合(つまり、パニック処理が進行中の場合)、checkdead はデッドロック検出をスキップします。これにより、パニック処理中の一時的なゴルーチンの状態がデッドロックと誤認されることを防ぎ、誤解を招くメッセージの出力を抑制します。

また、runtime·printf の出力に runtime: というプレフィックスが追加されています。これは、ランタイムが出力するメッセージであることを明確にし、他のアプリケーションの出力と区別しやすくするための一般的な改善です。

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

src/pkg/runtime/proc.c ファイルにおいて、以下の変更が行われました。

--- a/src/pkg/runtime/proc.c
+++ b/src/pkg/runtime/proc.c
@@ -2441,8 +2441,14 @@ checkdead(void)
 	run = runtime·sched.mcount - runtime·sched.nmidle - runtime·sched.nmidlelocked - 1;
 	if(run > 0)
 		return;
+	// If we are dying because of a signal caught on an already idle thread,
+	// freezetheworld will cause all running threads to block.
+	// And runtime will essentially enter into deadlock state,
+	// except that there is a thread that will call runtime·exit soon.
+	if(runtime·panicking > 0)
+		return;
 	if(run < 0) {
-\truntime·printf("checkdead: nmidle=%d nmidlelocked=%d mcount=%d\n",
+\truntime·printf("runtime: checkdead: nmidle=%d nmidlelocked=%d mcount=%d\n",
 		\truntime·sched.nmidle, runtime·sched.nmidlelocked, runtime·sched.mcount);
 		\truntime·throw("checkdead: inconsistent counts");
 	}
@@ -2457,7 +2463,7 @@ checkdead(void)
 			grunning++;
 		else if(s == Grunnable || s == Grunning || s == Gsyscall) {
 			runtime·unlock(&allglock);
-\truntime·printf("checkdead: find g %D in status %d\n", gp->goid, s);\
+\truntime·printf("runtime: checkdead: find g %D in status %d\n", gp->goid, s);
 			runtime·throw("checkdead: runnable g");
 		}
 	}

具体的には、以下の2つの変更点があります。

  1. checkdead 関数の冒頭に if(runtime·panicking > 0) return; という行が追加されました。
  2. 既存の runtime·printf の呼び出し2箇所で、フォーマット文字列の先頭に "runtime: " が追加されました。

コアとなるコードの解説

1. if(runtime·panicking > 0) return; の追加

この行がこのコミットの最も重要な変更点です。 checkdead 関数が呼び出された際に、まず runtime·panicking というグローバル変数の値がチェックされます。

  • runtime·panicking0 より大きい場合:これは現在Goランタイムがパニック処理の途中であることを意味します。パニック処理中は、freezetheworld メカニズムによってすべてのゴルーチンが停止され、ランタイムは終了処理に向けて動いています。このような状況では、checkdead がデッドロックを検出しても、それは実際のデッドロックではなく、パニック処理による一時的な状態である可能性が高いです。したがって、この条件が真の場合、checkdead 関数はすぐに return し、それ以降のデッドロック検出ロジックを実行しません。これにより、パニック中の偽陽性デッドロックメッセージの出力を防ぎます。

この変更により、Goランタイムは、真のデッドロックと、パニック処理中の一時的な状態を区別できるようになり、ユーザーに誤解を招く情報を提供することを避けることができます。

2. runtime·printf の出力変更

以下の2つの runtime·printf の呼び出しにおいて、出力される文字列の先頭に "runtime: " というプレフィックスが追加されました。

  • runtime·printf("runtime: checkdead: nmidle=%d nmidlelocked=%d mcount=%d\n", ...)
  • runtime·printf("runtime: checkdead: find g %D in status %d\n", ...)

この変更は機能的なものではなく、主にログの可読性と識別性を向上させるためのものです。Goランタイムが出力するメッセージであることを明示することで、システムログやデバッグ出力において、ランタイムからのメッセージとアプリケーションコードからのメッセージを容易に区別できるようになります。これは、大規模なシステムや複雑なデバッグシナリオにおいて、非常に役立つ改善です。

関連リンク

  • Go Change-ID: https://golang.org/cl/59490046 (元のコミットメッセージに記載されているChange-ID)
    • このChange-IDは、Gerrit (Goプロジェクトが使用するコードレビューシステム) 上のこのコミットのレビューページを指します。ここには、コミットに関する議論、レビューコメント、および最終的な承認プロセスに関する詳細な情報が含まれています。

参考にした情報源リンク

  • Goのソースコード (src/pkg/runtime/proc.c)
  • Goのドキュメント (Goランタイム、ゴルーチン、スケジューラに関する一般的な情報)
  • GoのIssue Tracker (関連するバグ報告や議論がある場合)
  • Goのコードレビューシステム (Gerrit)
  • Goの公式ブログや技術記事 (Goランタイムの内部動作に関する解説)
  • Goのデッドロック検出に関する一般的な情報 (例: Goのデッドロック検出の仕組み)
  • Goのパニック処理に関する一般的な情報 (例: Goのパニックとリカバリ)
  • GoのRace Detectorに関する情報 (例: Go Race Detectorの仕組み)
  • Goの内部状態に関する情報 (例: runtime.panicking, freezetheworld, ゴルーチンの状態)