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

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

このコミットは、Goランタイムにおけるクラッシュ時のトレースバック機能を改善するために導入されました。具体的には、クラッシュ発生時に実行中のゴルーチンを可能な限り停止させるための新しい関数 freezetheworld が追加され、パニック処理の開始時にこの関数が呼び出されるように変更されています。これにより、クラッシュダンプやデバッグ情報がより正確になり、問題の診断が容易になることが期待されます。

コミット

commit 01f1e3da484f74a7229c3c1eb719403b4e8c7a1c
Author: Dmitriy Vyukov <dvyukov@google.com>
Date:   Fri Aug 9 12:53:35 2013 +0400

    runtime: traceback running goroutines
    Introduce freezetheworld function that is a best-effort attempt to stop any concurrently running goroutines. Call it during crash.
    Fixes #5873.

    R=golang-dev, rsc
    CC=golang-dev
    https://golang.org/cl/12054044

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

https://github.com/golang/go/commit/01f1e3da484f74a7229c3c1eb719403b4e8c7a1c

元コミット内容

このコミットの目的は、Goランタイムがクラッシュした際に、実行中のゴルーチンのトレースバックをより正確に取得することです。そのために、freezetheworld という関数が導入されました。この関数は、同時に実行されているゴルーチンを停止させるための「最善の努力」を行うものであり、クラッシュ時に呼び出されます。これにより、Issue #5873 で報告された問題が修正されます。

変更の背景

Goプログラムがパニック(クラッシュ)に陥った際、デバッグのために現在のすべてのゴルーチンのスタックトレース(トレースバック)を生成することが重要です。しかし、クラッシュ発生時に他のゴルーチンがまだ実行中であると、それらのゴルーチンの状態が変化し続け、正確なスタックトレースを取得することが困難になる場合があります。特に、トレースバックを生成している最中にゴルーチンがスケジューリングされたり、状態を変更したりすると、不整合な情報が記録される可能性があります。

このコミットは、この問題を解決するために導入されました。freezetheworld 関数を導入し、パニック処理の初期段階でこれを呼び出すことで、他のゴルーチンの実行を一時的に停止させ、安定した状態でトレースバックを生成できるようにします。これにより、デバッグ情報の信頼性が向上し、クラッシュの原因特定が容易になります。

関連するIssue #5873は、"runtime: traceback running goroutines" というタイトルで、まさにこの問題、つまりクラッシュ時のゴルーチンのトレースバックの正確性に関する課題を扱っています。このコミットは、その課題に対する直接的な解決策を提供します。

前提知識の解説

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

  • ゴルーチン (Goroutine): Goにおける軽量な並行実行単位です。OSのスレッドよりもはるかに軽量で、数百万個のゴルーチンを同時に実行することも可能です。Goランタイムのスケジューラによって管理されます。
  • M (Machine) と P (Processor): Goランタイムのスケジューラにおける重要な概念です。
    • M (Machine): OSのスレッドに対応します。ゴルーチンを実行する実際のOSスレッドです。
    • P (Processor): 論理的なプロセッサ(コンテキスト)を表します。Mがゴルーチンを実行するためにはPを必要とします。Pは実行可能なゴルーチンのキューを持ち、MはPからゴルーチンを取得して実行します。GOMAXPROCS 環境変数によってPの数が制御されます。
  • スケジューラ (Scheduler): Goランタイムのスケジューラは、ゴルーチンをMとPに割り当て、実行を管理します。ゴルーチンの生成、実行、停止、ブロック、アンブロックなどを担当します。
  • プリエンプション (Preemption): Goランタイムは、長時間実行されるゴルーチンが他のゴルーチンの実行を妨げないように、プリエンプション(横取り)の仕組みを持っています。これにより、スケジューラは実行中のゴルーチンを一時停止させ、別のゴルーチンにCPUを割り当てることができます。
  • Stop-the-world (STW): Goのガベージコレクション(GC)などで使用されるメカニズムで、プログラム内のすべてのゴルーチンの実行を一時的に停止させることを指します。これは、GCがメモリの状態を安全に検査・変更するために必要です。STW中は、ユーザーコードの実行は完全に停止します。
  • パニック (Panic): Goプログラムが回復不可能なエラーに遭遇した際に発生するメカニズムです。パニックが発生すると、通常の実行フローは中断され、ランタイムはスタックをアンワインドし、最終的にプログラムを終了させます。この過程で、デバッグ情報としてゴルーチンのトレースバックが生成されます。
  • runtime·startpanic: Goランタイム内部でパニック処理が開始される際に呼び出される関数です。

技術的詳細

このコミットの核となるのは、runtime·freezetheworld 関数の導入とその利用です。

runtime·freezetheworld は、stoptheworld と似ていますが、いくつかの重要な違いがあります。

  • ベストエフォート (Best-effort): stoptheworld がすべてのゴルーチンの停止を保証するのに対し、freezetheworld は「最善の努力」をします。これは、クラッシュという異常な状況下で、完全にすべてのゴルーチンを停止させるのが困難な場合があるためです。
  • 複数回呼び出し可能: freezetheworld は複数回呼び出すことができ、逆操作(停止したゴルーチンを再開する操作)は存在しません。これは、クラッシュ時にのみ使用されるためです。
  • ミューテックスのロック禁止: freezetheworld は、ミューテックスをロックしてはならないという制約があります。これは、パニック処理中にデッドロックを避けるためです。パニックは任意の時点で発生する可能性があり、その際にすでにロックされているミューテックスを再度ロックしようとすると、デッドロックが発生し、デバッグ情報の取得すらできなくなる可能性があります。

freezetheworld の実装は以下のステップで行われます。

  1. GOMAXPROCS == 1 の場合の早期リターン: GOMAXPROCS が1の場合(つまり、並行実行がない場合)、ゴルーチンを停止させる必要がないため、すぐにリターンします。
  2. 複数回の試行: 実行中のスレッドとの競合により、停止要求が失われる可能性があるため、freezetheworld はループ内で複数回(最大5回)停止処理を試行します。
  3. スケジューラの停止:
    • runtime·sched.stopwait = 0x7fffffff;: これは、スケジューラが新しいゴルーチンを開始しないように指示するものです。stopwait は、stoptheworld などの操作中にスケジューラが待機するゴルーチンの数を制御するために使用されます。非常に大きな値を設定することで、実質的に新しいゴルーチンの実行を停止させます。
    • runtime·atomicstore((uint32*)&runtime·gcwaiting, 1);: gcwaiting フラグをセットすることで、GCが待機中であることを示し、スケジューラの動作に影響を与えます。
  4. ゴルーチンのプリエンプション: preemptall() を呼び出して、現在実行中のすべてのゴルーチンにプリエンプションを要求します。preemptall は、プリエンプション要求が少なくとも1つのゴルーチンに発行された場合に true を返すように変更されました。
  5. 短いスリープ: 各試行の間に runtime·usleep(1000) を呼び出して、短い時間(1ミリ秒)スリープします。これにより、プリエンプション要求が伝播し、ゴルーチンが停止する機会を与えます。
  6. exitsyscallfast の変更: exitsyscallfast 関数(システムコールから高速に復帰する際に呼び出される)が変更され、runtime·sched.stopwait が設定されている場合(つまり freezetheworld がアクティブな場合)には、P(プロセッサ)を再取得せずに false を返すようになりました。これにより、freezetheworld がアクティブな間は、システムコールから戻ったゴルーチンがすぐに実行を再開するのを防ぎます。
  7. preemptallpreemptone の変更: これらの関数は、プリエンプション要求が発行されたかどうかを示す bool 値を返すように変更されました。これにより、freezetheworld はプリエンプションが実際に試行されたかどうかを判断できます。

この一連の処理により、クラッシュ発生時に実行中のゴルーチンが可能な限り停止され、その時点での正確なスタックトレースが取得できるようになります。

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

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

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

    • runtime·startpanic 関数内に runtime·freezetheworld(); の呼び出しが追加されました。これにより、パニック処理の開始時にゴルーチンの停止が試みられます。
  2. src/pkg/runtime/proc.c:

    • preemptall および preemptone 関数のシグネチャが変更され、戻り値が void から bool になりました。これは、プリエンプション要求が発行されたかどうかを示すためです。
    • 新しい関数 runtime·freezetheworld が追加されました。この関数は、クラッシュ時にゴルーチンを停止させるためのロジックを含みます。
    • exitsyscallfast 関数が変更され、runtime·sched.stopwait が設定されている場合にPを再取得しないロジックが追加されました。
    • preemptall および preemptone の実装が変更され、プリエンプション要求が発行された場合に true を返すようになりました。
  3. src/pkg/runtime/runtime.h:

    • runtime·freezetheworld 関数のプロトタイプ宣言が追加されました。

コアとなるコードの解説

src/pkg/runtime/panic.c の変更

@@ -419,6 +419,7 @@ runtime·startpanic(void)
 		g->writebuf = nil;
 	runtime·xadd(&runtime·panicking, 1);
 	runtime·lock(&paniclk);
+	runtime·freezetheworld();
 }

runtime·startpanic は、Goプログラムがパニック状態に入ったときに呼び出されるランタイム関数です。この変更により、パニック処理の非常に早い段階で runtime·freezetheworld() が呼び出されるようになりました。これにより、パニックが発生した直後に、他のゴルーチンがさらに状態を変更する前に、それらを停止させる試みが行われます。これは、後続のトレースバック生成処理が、より安定した(フリーズした)システム状態で行われることを保証するために重要です。

src/pkg/runtime/proc.c の変更

@@ -107,8 +107,8 @@ static G* globrunqget(P*, int32);
 static P* pidleget(void);
 static void pidleput(P*);
 static void injectglist(G*);
-static void preemptall(void);
-static void preemptone(P*);
+static bool preemptall(void);
+static bool preemptone(P*);
 static bool exitsyscallfast(void);

preemptallpreemptone の関数シグネチャが変更されました。以前は void を返していましたが、bool を返すようになりました。この bool は、プリエンプション要求が実際に発行されたかどうかを示します。これにより、呼び出し元(特に freezetheworld)は、プリエンプションが試行されたかどうかを判断し、必要に応じて再試行するロジックを実装できるようになります。

@@ -374,6 +374,34 @@ runtime·helpgc(int32 nproc)
 	runtime·unlock(&runtime·sched);
 }
 
+// Similar to stoptheworld but best-effort and can be called several times.
+// There is no reverse operation, used during crashing.
+// This function must not lock any mutexes.
+void
+runtime·freezetheworld(void)
+{
+	int32 i;
+
+	if(runtime·gomaxprocs == 1)
+		return;
+	// stopwait and preemption requests can be lost
+	// due to races with concurrently executing threads,
+	// so try several times
+	for(i = 0; i < 5; i++) {
+		// this should tell the scheduler to not start any new goroutines
+		runtime·sched.stopwait = 0x7fffffff;
+		runtime·atomicstore((uint32*)&runtime·gcwaiting, 1);
+		// this should stop running goroutines
+		if(!preemptall())
+			break;  // no running goroutines
+		runtime·usleep(1000);
+	}
+	// to be sure
+	runtime·usleep(1000);
+	preemptall();
+	runtime·usleep(1000);
+}
+
 void
 runtime·stoptheworld(void)
 {

runtime·freezetheworld 関数の実装です。

  • runtime·gomaxprocs == 1 の場合は、並行実行がないためすぐにリターンします。
  • for ループで5回まで試行します。これは、並行実行中のスレッドとの競合により、停止要求が失われる可能性があるためです。
  • runtime·sched.stopwait = 0x7fffffff;runtime·atomicstore((uint32*)&runtime·gcwaiting, 1); は、スケジューラに新しいゴルーチンを開始させないように指示します。stopwait に大きな値を設定することで、実質的にスケジューラを停止状態に近づけます。
  • if(!preemptall()) break; は、すべてのPに対してプリエンプションを要求します。もし preemptallfalse を返した場合(つまり、プリエンプション要求が発行されたゴルーチンが一つもなかった場合)、それ以上試行しても意味がないためループを抜けます。
  • runtime·usleep(1000); は、各試行の間に1ミリ秒スリープし、プリエンプション要求が伝播する時間を与えます。
  • ループの後に再度 preemptall()runtime·usleep(1000) を呼び出すことで、確実に停止を試みます。
@@ -1518,6 +1546,12 @@ exitsyscallfast(void)
 {
 	P *p;
 
+	// Freezetheworld sets stopwait but does not retake P's.
+	if(runtime·sched.stopwait) {
+		m->p = nil;
+		return false;
+	}
+
 	// Try to re-acquire the last P.
 	if(m->p && m->p->status == Psyscall && runtime·cas(&m->p->status, Psyscall, Prunning)) {
 		// There's a cpu for us, so we can run.

exitsyscallfast は、システムコールから高速に復帰する際に呼び出される関数です。この変更により、runtime·sched.stopwait が設定されている場合(freezetheworld がアクティブな場合)には、M(OSスレッド)がP(プロセッサ)を再取得せずに false を返すようになりました。これは、freezetheworld がアクティブな間は、システムコールから戻ったゴルーチンがすぐに実行を再開するのを防ぎ、フリーズ状態を維持するために重要です。

@@ -2243,18 +2277,22 @@ retake(int64 now)
 // This function is purely best-effort.  It can fail to inform a goroutine if a
 // processor just started running it.
 // No locks need to be held.
-static void
+// Returns true if preemption request was issued to at least one goroutine.
+static bool
 preemptall(void)
 {
 	P *p;
 	int32 i;
+	bool res;
 
+\tres = false;
 	for(i = 0; i < runtime·gomaxprocs; i++) {
 		p = runtime·allp[i];
 		if(p == nil || p->status != Prunning)
 			continue;
-\t\tpreemptone(p);\n+\t\tres |= preemptone(p);\n \t}\n+\treturn res;\n }

preemptall は、すべてのPに対してプリエンプションを要求する関数です。変更点として、bool res = false; が追加され、各 preemptone(p) の結果を res |= preemptone(p); で論理OR結合しています。これにより、少なくとも1つのゴルーチンにプリエンプション要求が発行された場合に true を返すようになりました。

@@ -2263,7 +2301,8 @@ preemptall(void)
 // correct goroutine, that goroutine might ignore the request if it is
 // simultaneously executing runtime·newstack.
 // No lock needs to be held.
-static void
+// Returns true if preemption request was issued.
+static bool
 preemptone(P *p)
 {
 	M *mp;
@@ -2271,12 +2310,13 @@ preemptone(P *p)
 
 	mp = p->m;
 	if(mp == nil || mp == m)
-\t\treturn;\n+\t\treturn false;\n \tgp = mp->curg;\n \tif(gp == nil || gp == mp->g0)\n-\t\treturn;\n+\t\treturn false;\n \tgp->preempt = true;\n \tgp->stackguard0 = StackPreempt;\n+\treturn true;\n }

preemptone は、特定のP上で実行中のゴルーチンにプリエンプションを要求する関数です。変更点として、プリエンプション要求が発行されなかった場合(mp == nil || mp == m または gp == nil || gp == mp->g0 の場合)に false を返すようになりました。それ以外の場合(プリエンプション要求が設定された場合)は true を返します。

src/pkg/runtime/runtime.h の変更

@@ -837,6 +837,7 @@ int32	runtime·callers(int32, uintptr*, int32);\n int64	runtime·nanotime(void);\n void	runtime·dopanic(int32);\n void	runtime·startpanic(void);\n+void	runtime·freezetheworld(void);\n void	runtime·unwindstack(G*, byte*);\n void	runtime·sigprof(uint8 *pc, uint8 *sp, uint8 *lr, G *gp);\n void	runtime·resetcpuprofiler(int32);\n```
`runtime·freezetheworld` 関数のプロトタイプ宣言が追加されました。これにより、他のランタイムファイルからこの関数を呼び出すことが可能になります。

## 関連リンク

*   Go Issue #5873: [runtime: traceback running goroutines](https://github.com/golang/go/issues/5873)
*   Go CL 12054044: [runtime: traceback running goroutines](https://go.dev/cl/12054044)

## 参考にした情報源リンク

*   Goの公式ドキュメント(Goランタイム、ゴルーチン、スケジューラに関する一般的な情報)
*   Goのソースコード(特に `src/runtime` ディレクトリ)
*   GoのIssueトラッカー(Issue #5873の詳細)
*   Goのコードレビューシステム(CL 12054044の詳細)
*   Goのガベージコレクションに関する資料(Stop-the-worldの概念理解のため)
*   Goのプリエンプションに関する資料(プリエンプションの仕組み理解のため)# [インデックス 17120] ファイルの概要

このコミットは、Goランタイムにおけるクラッシュ時のトレースバック機能を改善するために導入されました。具体的には、クラッシュ発生時に実行中のゴルーチンを可能な限り停止させるための新しい関数 `freezetheworld` が追加され、パニック処理の開始時にこの関数が呼び出されるように変更されています。これにより、クラッシュダンプやデバッグ情報がより正確になり、問題の診断が容易になることが期待されます。

## コミット

commit 01f1e3da484f74a7229c3c1eb719403b4e8c7a1c Author: Dmitriy Vyukov dvyukov@google.com Date: Fri Aug 9 12:53:35 2013 +0400

runtime: traceback running goroutines
Introduce freezetheworld function that is a best-effort attempt to stop any concurrently running goroutines. Call it during crash.
Fixes #5873.

R=golang-dev, rsc
CC=golang-dev
https://golang.org/cl/12054044

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

[https://github.com/golang/go/commit/01f1e3da484f74a7229c3c1eb719403b4e8c7a1c](https://github.com/golang/go/commit/01f1e3da484f74a7229c3c1eb719403b4e8c7a1c)

## 元コミット内容

このコミットの目的は、Goランタイムがクラッシュした際に、実行中のゴルーチンのトレースバックをより正確に取得することです。そのために、`freezetheworld` という関数が導入されました。この関数は、同時に実行されているゴルーチンを停止させるための「最善の努力」を行うものであり、クラッシュ時に呼び出されます。これにより、Issue #5873 で報告された問題が修正されます。

## 変更の背景

Goプログラムがパニック(クラッシュ)に陥った際、デバッグのために現在のすべてのゴルーチンのスタックトレース(トレースバック)を生成することが重要です。しかし、クラッシュ発生時に他のゴルーチンがまだ実行中であると、それらのゴルーチンの状態が変化し続け、正確なスタックトレースを取得することが困難になる場合があります。特に、トレースバックを生成している最中にゴルーチンがスケジューリングされたり、状態を変更したりすると、不整合な情報が記録される可能性があります。

このコミットは、この問題を解決するために導入されました。`freezetheworld` 関数を導入し、パニック処理の初期段階でこれを呼び出すことで、他のゴルーチンの実行を一時的に停止させ、安定した状態でトレースバックを生成できるようにします。これにより、デバッグ情報の信頼性が向上し、クラッシュの原因特定が容易になります。

関連するIssue #5873は、"runtime: traceback running goroutines" というタイトルで、まさにこの問題、つまりクラッシュ時のゴルーチンのトレースバックの正確性に関する課題を扱っています。このコミットは、その課題に対する直接的な解決策を提供します。

## 前提知識の解説

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

*   **ゴルーチン (Goroutine)**: Goにおける軽量な並行実行単位です。OSのスレッドよりもはるかに軽量で、数百万個のゴルーチンを同時に実行することも可能です。Goランタイムのスケジューラによって管理されます。
*   **M (Machine) と P (Processor)**: Goランタイムのスケジューラにおける重要な概念です。
    *   **M (Machine)**: OSのスレッドに対応します。ゴルーチンを実行する実際のOSスレッドです。
    *   **P (Processor)**: 論理的なプロセッサ(コンテキスト)を表します。Mがゴルーチンを実行するためにはPを必要とします。Pは実行可能なゴルーチンのキューを持ち、MはPからゴルーチンを取得して実行します。`GOMAXPROCS` 環境変数によってPの数が制御されます。
*   **スケジューラ (Scheduler)**: Goランタイムのスケジューラは、ゴルーチンをMとPに割り当て、実行を管理します。ゴルーチンの生成、実行、停止、ブロック、アンブロックなどを担当します。
*   **プリエンプション (Preemption)**: Goランタイムは、長時間実行されるゴルーチンが他のゴルーチンの実行を妨げないように、プリエンプション(横取り)の仕組みを持っています。これにより、スケジューラは実行中のゴルーチンを一時停止させ、別のゴルーチンにCPUを割り当てることができます。
*   **Stop-the-world (STW)**: Goのガベージコレクション(GC)などで使用されるメカニズムで、プログラム内のすべてのゴルーチンの実行を一時的に停止させることを指します。これは、GCがメモリの状態を安全に検査・変更するために必要です。STW中は、ユーザーコードの実行は完全に停止します。
*   **パニック (Panic)**: Goプログラムが回復不可能なエラーに遭遇した際に発生するメカニズムです。パニックが発生すると、通常の実行フローは中断され、ランタイムはスタックをアンワインドし、最終的にプログラムを終了させます。この過程で、デバッグ情報としてゴルーチンのトレースバックが生成されます。
*   **`runtime·startpanic`**: Goランタイム内部でパニック処理が開始される際に呼び出される関数です。

## 技術的詳細

このコミットの核となるのは、`runtime·freezetheworld` 関数の導入とその利用です。

`runtime·freezetheworld` は、`stoptheworld` と似ていますが、いくつかの重要な違いがあります。
*   **ベストエフォート (Best-effort)**: `stoptheworld` がすべてのゴルーチンの停止を保証するのに対し、`freezetheworld` は「最善の努力」をします。これは、クラッシュという異常な状況下で、完全にすべてのゴルーチンを停止させるのが困難な場合があるためです。
*   **複数回呼び出し可能**: `freezetheworld` は複数回呼び出すことができ、逆操作(停止したゴルーチンを再開する操作)は存在しません。これは、クラッシュ時にのみ使用されるためです。
*   **ミューテックスのロック禁止**: `freezetheworld` は、ミューテックスをロックしてはならないという制約があります。これは、パニック処理中にデッドロックを避けるためです。パニックは任意の時点で発生する可能性があり、その際にすでにロックされているミューテックスを再度ロックしようとすると、デッドロックが発生し、デバッグ情報の取得すらできなくなる可能性があります。

`freezetheworld` の実装は以下のステップで行われます。

1.  **`GOMAXPROCS == 1` の場合の早期リターン**: `GOMAXPROCS` が1の場合(つまり、並行実行がない場合)、ゴルーチンを停止させる必要がないため、すぐにリターンします。
2.  **複数回の試行**: 実行中のスレッドとの競合により、停止要求が失われる可能性があるため、`freezetheworld` はループ内で複数回(最大5回)停止処理を試行します。
3.  **スケジューラの停止**:
    *   `runtime·sched.stopwait = 0x7fffffff;`: これは、スケジューラが新しいゴルーチンを開始しないように指示するものです。`stopwait` は、`stoptheworld` などの操作中にスケジューラが待機するゴルーチンの数を制御するために使用されます。非常に大きな値を設定することで、実質的に新しいゴルーチンの実行を停止させます。
    *   `runtime·atomicstore((uint32*)&runtime·gcwaiting, 1);`: `gcwaiting` フラグをセットすることで、GCが待機中であることを示し、スケジューラの動作に影響を与えます。
4.  **ゴルーチンのプリエンプション**: `preemptall()` を呼び出して、現在実行中のすべてのゴルーチンにプリエンプションを要求します。`preemptall` は、プリエンプション要求が少なくとも1つのゴルーチンに発行された場合に `true` を返すように変更されました。
5.  **短いスリープ**: 各試行の間に `runtime·usleep(1000)` を呼び出して、短い時間(1ミリ秒)スリープします。これにより、プリエンプション要求が伝播し、ゴルーチンが停止する機会を与えます。
6.  **`exitsyscallfast` の変更**: `exitsyscallfast` 関数(システムコールから高速に復帰する際に呼び出される)が変更され、`runtime·sched.stopwait` が設定されている場合(つまり `freezetheworld` がアクティブな場合)には、P(プロセッサ)を再取得せずに `false` を返すようになりました。これにより、`freezetheworld` がアクティブな間は、システムコールから戻ったゴルーチンがすぐに実行を再開するのを防ぎます。
7.  **`preemptall` と `preemptone` の変更**: これらの関数は、プリエンプション要求が発行されたかどうかを示す `bool` 値を返すように変更されました。これにより、`freezetheworld` はプリエンプションが実際に試行されたかどうかを判断できます。

この一連の処理により、クラッシュ発生時に実行中のゴルーチンが可能な限り停止され、その時点での正確なスタックトレースが取得できるようになります。

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

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

1.  **`src/pkg/runtime/panic.c`**:
    *   `runtime·startpanic` 関数内に `runtime·freezetheworld();` の呼び出しが追加されました。これにより、パニック処理の開始時にゴルーチンの停止が試みられます。

2.  **`src/pkg/runtime/proc.c`**:
    *   `preemptall` および `preemptone` 関数のシグネチャが変更され、戻り値が `void` から `bool` になりました。これは、プリエンプション要求が発行されたかどうかを示すためです。
    *   新しい関数 `runtime·freezetheworld` が追加されました。この関数は、クラッシュ時にゴルーチンを停止させるためのロジックを含みます。
    *   `exitsyscallfast` 関数が変更され、`runtime·sched.stopwait` が設定されている場合にPを再取得しないロジックが追加されました。
    *   `preemptall` および `preemptone` の実装が変更され、プリエンプション要求が発行された場合に `true` を返すようになりました。

3.  **`src/pkg/runtime/runtime.h`**:
    *   `runtime·freezetheworld` 関数のプロトタイプ宣言が追加されました。

## コアとなるコードの解説

### `src/pkg/runtime/panic.c` の変更

```c
@@ -419,6 +419,7 @@ runtime·startpanic(void)
 		g->writebuf = nil;
 	runtime·xadd(&runtime·panicking, 1);
 	runtime·lock(&paniclk);
+	runtime·freezetheworld();
 }

runtime·startpanic は、Goプログラムがパニック状態に入ったときに呼び出されるランタイム関数です。この変更により、パニック処理の非常に早い段階で runtime·freezetheworld() が呼び出されるようになりました。これにより、パニックが発生した直後に、他のゴルーチンがさらに状態を変更する前に、それらを停止させる試みが行われます。これは、後続のトレースバック生成処理が、より安定した(フリーズした)システム状態で行われることを保証するために重要です。

src/pkg/runtime/proc.c の変更

@@ -107,8 +107,8 @@ static G* globrunqget(P*, int32);
 static P* pidleget(void);
 static void pidleput(P*);
 static void injectglist(G*);
-static void preemptall(void);
-static void preemptone(P*);
+static bool preemptall(void);
+static bool preemptone(P*);
 static bool exitsyscallfast(void);

preemptallpreemptone の関数シグネチャが変更されました。以前は void を返していましたが、bool を返すようになりました。この bool は、プリエンプション要求が実際に発行されたかどうかを示します。これにより、呼び出し元(特に freezetheworld)は、プリエンプションが試行されたかどうかを判断し、必要に応じて再試行するロジックを実装できるようになります。

@@ -374,6 +374,34 @@ runtime·helpgc(int32 nproc)
 	runtime·unlock(&runtime·sched);
 }
 
+// Similar to stoptheworld but best-effort and can be called several times.
+// There is no reverse operation, used during crashing.
+// This function must not lock any mutexes.
+void
+runtime·freezetheworld(void)
+{
+	int32 i;
+
+	if(runtime·gomaxprocs == 1)
+		return;
+	// stopwait and preemption requests can be lost
+	// due to races with concurrently executing threads,
+	// so try several times
+	for(i = 0; i < 5; i++) {
+		// this should tell the scheduler to not start any new goroutines
+		runtime·sched.stopwait = 0x7fffffff;
+		runtime·atomicstore((uint32*)&runtime·gcwaiting, 1);
+		// this should stop running goroutines
+		if(!preemptall())
+			break;  // no running goroutines
+		runtime·usleep(1000);
+	}
+	// to be sure
+	runtime·usleep(1000);
+	preemptall();
+	runtime·usleep(1000);
+}
+
 void
 runtime·stoptheworld(void)
 {

runtime·freezetheworld 関数の実装です。

  • runtime·gomaxprocs == 1 の場合は、並行実行がないためすぐにリターンします。
  • for ループで5回まで試行します。これは、並行実行中のスレッドとの競合により、停止要求が失われる可能性があるためです。
  • runtime·sched.stopwait = 0x7fffffff;runtime·atomicstore((uint32*)&runtime·gcwaiting, 1); は、スケジューラに新しいゴルーチンを開始させないように指示します。stopwait に大きな値を設定することで、実質的にスケジューラを停止状態に近づけます。
  • if(!preemptall()) break; は、すべてのPに対してプリエンプションを要求します。もし preemptallfalse を返した場合(つまり、プリエンプション要求が発行されたゴルーチンが一つもなかった場合)、それ以上試行しても意味がないためループを抜けます。
  • runtime·usleep(1000); は、各試行の間に1ミリ秒スリープし、プリエンプション要求が伝播する時間を与えます。
  • ループの後に再度 preemptall()runtime·usleep(1000) を呼び出すことで、確実に停止を試みます。
@@ -1518,6 +1546,12 @@ exitsyscallfast(void)
 {
 	P *p;
 
+	// Freezetheworld sets stopwait but does not retake P's.
+	if(runtime·sched.stopwait) {
+		m->p = nil;
+		return false;
+	}
+
 	// Try to re-acquire the last P.
 	if(m->p && m->p->status == Psyscall && runtime·cas(&m->p->status, Psyscall, Prunning)) {
 		// There's a cpu for us, so we can run.

exitsyscallfast は、システムコールから高速に復帰する際に呼び出される関数です。この変更により、runtime·sched.stopwait が設定されている場合(freezetheworld がアクティブな場合)には、M(OSスレッド)がP(プロセッサ)を再取得せずに false を返すようになりました。これは、freezetheworld がアクティブな間は、システムコールから戻ったゴルーチンがすぐに実行を再開するのを防ぎ、フリーズ状態を維持するために重要です。

@@ -2243,18 +2277,22 @@ retake(int64 now)
 // This function is purely best-effort.  It can fail to inform a goroutine if a
 // processor just started running it.
 // No locks need to be held.
-static void
+// Returns true if preemption request was issued to at least one goroutine.
+static bool
 preemptall(void)
 {
 	P *p;
 	int32 i;
+	bool res;
 
+\tres = false;
 	for(i = 0; i < runtime·gomaxprocs; i++) {
 		p = runtime·allp[i];
 		if(p == nil || p->status != Prunning)
 			continue;
-\t\tpreemptone(p);\n+\t\tres |= preemptone(p);\n \t}\n+\treturn res;\n }

preemptall は、すべてのPに対してプリエンプションを要求する関数です。変更点として、bool res = false; が追加され、各 preemptone(p) の結果を res |= preemptone(p); で論理OR結合しています。これにより、少なくとも1つのゴルーチンにプリエンプション要求が発行された場合に true を返すようになりました。

@@ -2263,7 +2301,8 @@ preemptall(void)
 // correct goroutine, that goroutine might ignore the request if it is
 // simultaneously executing runtime·newstack.
 // No lock needs to be held.
-static void
+// Returns true if preemption request was issued.
+static bool
 preemptone(P *p)
 {
 	M *mp;
@@ -2271,12 +2310,13 @@ preemptone(P *p)
 
 	mp = p->m;
 	if(mp == nil || mp == m)
-\t\treturn;\n+\t\treturn false;\n \tgp = mp->curg;\n \tif(gp == nil || gp == mp->g0)\n-\t\treturn;\n+\t\treturn false;\n \tgp->preempt = true;\n \tgp->stackguard0 = StackPreempt;\n+\treturn true;\n }

preemptone は、特定のP上で実行中のゴルーチンにプリエンプションを要求する関数です。変更点として、プリエンプション要求が発行されなかった場合(mp == nil || mp == m または gp == nil || gp == mp->g0 の場合)に false を返すようになりました。それ以外の場合(プリエンプション要求が設定された場合)は true を返します。

src/pkg/runtime/runtime.h の変更

@@ -837,6 +837,7 @@ int32	runtime·callers(int32, uintptr*, int32);\n int64	runtime·nanotime(void);\n void	runtime·dopanic(int32);\n void	runtime·startpanic(void);\n+void	runtime·freezetheworld(void);\n void	runtime·unwindstack(G*, byte*);\n void	runtime·sigprof(uint8 *pc, uint8 *sp, uint8 *lr, G *gp);\n void	runtime·resetcpuprofiler(int32);\n```
`runtime·freezetheworld` 関数のプロトタイプ宣言が追加されました。これにより、他のランタイムファイルからこの関数を呼び出すことが可能になります。

## 関連リンク

*   Go Issue #5873: [runtime: traceback running goroutines](https://github.com/golang/go/issues/5873)
*   Go CL 12054044: [runtime: traceback running goroutines](https://go.dev/cl/12054044)

## 参考にした情報源リンク

*   Goの公式ドキュメント(Goランタイム、ゴルーチン、スケジューラに関する一般的な情報)
*   Goのソースコード(特に `src/runtime` ディレクトリ)
*   GoのIssueトラッカー(Issue #5873の詳細)
*   Goのコードレビューシステム(CL 12054044の詳細)
*   Goのガベージコレクションに関する資料(Stop-the-worldの概念理解のため)
*   Goのプリエンプションに関する資料(プリエンプションの仕組み理解のため)