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

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

このコミットは、Goランタイムにおけるパニック発生時のスタックトレースのダンプ機能を改善するものです。特に、ランタイムのバグをデバッグする際に役立つよう、パニックを起こしたゴルーチンの完全なスタックをダンプする機能を追加し、GOTRACEBACK環境変数の設定に応じて、システムゴルーチンのトレースバックや「スタックセグメント境界」の表示を制御するように変更されています。

コミット

commit 81221f512d6ad8c15491b3ab29ea3fa3db800466
Author: Dmitriy Vyukov <dvyukov@google.com>
Date:   Tue Jan 29 14:57:11 2013 +0400

    runtime: dump the full stack of a throwing goroutine
    Useful for debugging of runtime bugs.
    + Do not print "stack segment boundary" unless GOTRACEBACK>1.
    + Do not traceback system goroutines unless GOTRACEBACK>1.
    
    R=rsc, minux.ma
    CC=golang-dev
    https://golang.org/cl/7098050
---
 src/pkg/runtime/panic.c         | 2 +++
 src/pkg/runtime/proc.c          | 6 ++++++
 src/pkg/runtime/runtime.h       | 4 +++-
 src/pkg/runtime/symtab.c        | 6 ++++--
 src/pkg/runtime/time.goc        | 4 +++-
 src/pkg/runtime/traceback_arm.c | 9 +++++----
 src/pkg/runtime/traceback_x86.c | 7 ++++---
 7 files changed, 27 insertions(+), 11 deletions(-)

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

https://github.com/golang/go/commit/81221f512d6ad8c15491b3ab29ea3fa3db800466

元コミット内容

runtime: dump the full stack of a throwing goroutine
Useful for debugging of runtime bugs.
+ Do not print "stack segment boundary" unless GOTRACEBACK>1.
+ Do not traceback system goroutines unless GOTRACEBACK>1.

変更の背景

Goプログラムがパニック(panic)を起こした際、Goランタイムは通常、問題が発生したゴルーチンのスタックトレースを出力します。しかし、ランタイム自体のバグをデバッグする際には、より詳細な情報、特にパニックを引き起こしたゴルーチンの完全なスタックトレースや、通常は隠蔽されるシステムゴルーチンの情報が必要となる場合があります。

このコミット以前は、これらの詳細な情報が常に利用できるわけではなく、デバッグ作業を困難にしていました。特に、スタックセグメントの境界を示すメッセージや、Goランタイム内部で動作するシステムゴルーチンのトレースバックは、通常のユーザーには不要な情報であるため、デフォルトでは表示されないように設計されていました。しかし、ランタイム開発者にとっては、これらの情報が不可欠です。

この変更の目的は、GOTRACEBACK環境変数という既存のメカニズムを活用し、デバッグレベルに応じてこれらの詳細な情報を選択的に表示できるようにすることで、ランタイムのデバッグ効率を向上させることにあります。これにより、開発者は必要に応じて詳細なスタック情報を取得し、ランタイムの複雑な問題をより効果的に診断できるようになります。

前提知識の解説

Goのゴルーチンとパニック

  • ゴルーチン (Goroutine): Go言語における軽量な並行実行単位です。OSのスレッドよりもはるかに軽量で、数千、数万のゴルーチンを同時に実行することが可能です。Goランタイムがゴルーチンのスケジューリングを管理します。
  • パニック (Panic): Goプログラムが回復不能なエラーに遭遇した際に発生するランタイムエラーです。パニックが発生すると、現在のゴルーチンの実行が停止し、遅延関数(defer)が実行され、最終的にプログラムが異常終了します。この際、Goランタイムは通常、パニックを起こしたゴルーチンのスタックトレースを出力します。

GOTRACEBACK環境変数

GOTRACEBACKは、Goプログラムがパニックを起こした際に生成されるスタックトレースの詳細度を制御するための環境変数です。この変数の値によって、出力される情報の範囲が変わります。

  • GOTRACEBACK=none (または 0): スタックトレースを一切出力せず、パニックメッセージのみを表示します。
  • GOTRACEBACK=single (デフォルト): パニックを起こしたゴルーチンのスタックトレースのみを表示します。Goランタイム内部の関数は省略されます。
  • GOTRACEBACK=all (または 1): すべてのユーザー作成ゴルーチンのスタックトレースを表示します。ランタイム関連のスタックフレームは引き続き省略されます。並行処理の問題を理解するのに非常に役立ちます。
  • GOTRACEBACK=system (または 2): all と同様にすべてのゴルーチンのスタックトレースを表示しますが、これに加えてランタイム関数のスタックフレームや、Goランタイム自体が内部的に作成したシステムゴルーチンのスタックトレースも表示します。プログラムの最も包括的な状態を把握できます。
  • GOTRACEBACK=crash: system と同様に詳細なスタックトレースを表示しますが、終了時にOS固有の方法でプロセスをクラッシュさせます(例: Unix系システムでは SIGABRT を発生)。これにより、コアダンプが生成され、GDBなどのデバッガで詳細な事後分析が可能になります。CGO関連の問題に特に有用です。

その他のスタックトレース取得方法

  • SIGQUIT シグナル: Unix系システムでは、実行中のGoプログラムに SIGQUIT シグナル(通常は Ctrl+\\)を送ることで、すべてのゴルーチンのスタックトレースをダンプし、プログラムを終了させることができます。
  • runtime/debug.PrintStack(): runtime/debug パッケージの PrintStack() 関数を呼び出すことで、現在のゴルーチンのスタックトレースをプログラム的に標準エラーに出力できます。
  • runtime/pprof: runtime/pprof パッケージを使用すると、ゴルーチンのプロファイルをプログラム的に収集できます。net/http/pprof をインポートすると、HTTPエンドポイント(例: http://localhost:6060/debug/pprof/goroutine?debug=2)経由でゴルーチンのダンプにアクセスすることも可能です。

技術的詳細

このコミットは、Goランタイムのスタックトレース生成ロジックに複数の変更を加えています。主な変更点は以下の通りです。

  1. m->throwing フラグの導入と利用:

    • M 構造体(OSスレッドを表すランタイム内部の構造体)に throwing という新しいフィールドが追加されました。これは、現在パニック処理中のゴルーチンが存在するかどうか、およびその詳細度を制御するためのフラグとして機能します。
    • runtime·throw 関数(パニックを引き起こす関数)が呼び出された際に、m->throwing0 であれば 1 に設定されます。これにより、ランタイムがパニック状態にあることを示します。
    • デッドロック検出時など、特定の状況下でスタックダンプを抑制したい場合には、m->throwing = -1 が設定されます。これは、完全なスタックダンプを行わないことを意味します。
  2. システムゴルーチンの識別と制御:

    • G 構造体(ゴルーチンを表すランタイム内部の構造体)に issystem という新しいブール型フィールドが追加されました。これにより、Goランタイム自身が内部的に作成・管理するゴルーチン(例: スケジューラ、ガベージコレクタ、タイマープロセッサなど)を識別できるようになります。
    • runtime·MHeap_Scavengertimerproc といったシステムゴルーチンが作成される際に、この issystem フラグが true に設定されます。
    • runtime·tracebackothers 関数(他のゴルーチンのトレースバックを行う関数)において、GOTRACEBACK の値が 2 (system) 未満の場合、issystemtrue のゴルーチンはトレースバックの対象から除外されるようになりました。これにより、通常のデバッグ時にはシステムゴルーチンの詳細が隠蔽され、GOTRACEBACK=system 以上の場合にのみ表示されるようになります。
  3. runtime·showframe 関数の変更:

    • runtime·showframe 関数は、スタックトレースにおいて特定のフレームを表示するかどうかを決定します。この関数に bool current という新しい引数が追加されました。これは、現在トレースバック中のフレームが、パニックを起こしたゴルーチンのフレームであるかどうかを示します。
    • currenttrue かつ m->throwing > 0 の場合、常にフレームを表示するようになりました。これにより、パニックを起こしたゴルーチンのスタックは常に完全な形でダンプされるようになります。
    • また、GOTRACEBACK の値が 1 より大きい場合(つまり system または crash レベルの場合)や、関数名にドットが含まれ、かつ runtime. プレフィックスを持たない関数(ユーザーコードの関数)の場合にフレームを表示するという既存のロジックも維持されています。
  4. スタックセグメント境界の表示制御:

    • runtime·gentraceback 関数(スタックトレースを生成する主要な関数)において、「----- stack segment boundary -----」というメッセージの表示が制御されるようになりました。
    • このメッセージは、GOTRACEBACK の値が 1 より大きい場合(system または crash レベル)にのみ表示されるようになりました。これにより、通常のスタックトレースではこの低レベルな情報が隠蔽され、詳細なデバッグが必要な場合にのみ表示されます。

これらの変更により、Goランタイムのデバッグ出力がより柔軟になり、開発者は GOTRACEBACK 環境変数を適切に設定することで、必要な詳細度でスタックトレース情報を取得できるようになりました。

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

このコミットにおける主要なコード変更は以下のファイルに集中しています。

  • src/pkg/runtime/panic.c: runtime·throw 関数に m->throwing フラグを設定するロジックが追加されました。
  • src/pkg/runtime/proc.c:
    • runtime·main 関数内で scvg (scavenger goroutine) に issystem = true を設定。
    • runtime·tracebackothers 関数で GOTRACEBACK の値と gp->issystem をチェックし、システムゴルーチンのトレースバックを制御。
    • デッドロック検出時に m->throwing = -1 を設定し、スタックダンプを抑制。
  • src/pkg/runtime/runtime.h: G 構造体に issystem フィールド、M 構造体に throwing フィールドを追加。runtime·showframe の関数シグネチャを変更。
  • src/pkg/runtime/symtab.c: runtime·showframe 関数の実装を変更し、current 引数と m->throwing の状態を考慮に入れる。
  • src/pkg/runtime/time.goc: addtimer 関数内で timers.timerproc (timer goroutine) に issystem = true を設定。
  • src/pkg/runtime/traceback_arm.c および src/pkg/runtime/traceback_x86.c:
    • runtime·gentraceback 関数内で「stack segment boundary」の表示を runtime·showframe(nil, gp == m->curg) の結果に依存させるように変更。
    • runtime·showframe の呼び出しに gp == m->curg 引数を追加。
    • ゴルーチン作成元の表示ロジックにも runtime·showframe の結果を追加。

コアとなるコードの解説

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

 void
 runtime·throw(int8 *s)
 {
+	if(m->throwing == 0)
+		m->throwing = 1;
 	runtime·startpanic();
 	runtime·printf("fatal error: %s\n", s);
 	runtime·dopanic(0);

runtime·throw はGoプログラムがパニックを起こした際に呼び出されるランタイム関数です。この変更により、パニックが開始されると、現在のM(OSスレッド)に関連付けられた m->throwing フラグが 1 に設定されます。これは、このMが現在パニック処理中であることをランタイム全体に通知し、スタックトレースの生成ロジックに影響を与えます。

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

@@ -242,6 +242,7 @@ runtime·main(void)
 	setmcpumax(runtime·gomaxprocs);
 	runtime·sched.init = true;
 	scvg = runtime·newproc1((byte*)runtime·MHeap_Scavenger, nil, 0, 0, runtime·main);
+	scvg->issystem = true;
 	main·init();
 	runtime·sched.init = false;
 	if(!runtime·sched.lockmain)
@@ -325,10 +326,14 @@ void
 runtime·tracebackothers(G *me)
 {
 	G *gp;
+	int32 traceback;
 
+	traceback = runtime·gotraceback();
 	for(gp = runtime·allg; gp != nil; gp = gp->alllink) {
 		if(gp == me || gp->status == Gdead)
 			continue;
+		if(gp->issystem && traceback < 2)
+			continue;
 		runtime·printf("\n");
 		runtime·goroutineheader(gp);
 		runtime·traceback(gp->sched.pc, (byte*)gp->sched.sp, 0, gp);
@@ -624,6 +629,7 @@ top:
 	if((scvg == nil && runtime·sched.grunning == 0) ||
 	   (scvg != nil && runtime·sched.grunning == 1 && runtime·sched.gwait == 0 &&
 	    (scvg->status == Grunning || scvg->status == Gsyscall))) {
+		m->throwing = -1;  // do not dump full stacks
 		runtime·throw("all goroutines are asleep - deadlock!");
 	}
  • scvg->issystem = true;: メモリのスカベンジャー(ガベージコレクション関連)ゴルーチンがシステムゴルーチンとして明示的にマークされます。
  • if(gp->issystem && traceback < 2) continue;: runtime·tracebackothers は、現在のゴルーチン以外のすべてのゴルーチンのスタックトレースをダンプする際に呼び出されます。ここで、GOTRACEBACK の値が 2 (system) 未満の場合、issystem フラグが true のゴルーチン(つまりシステムゴルーチン)はスキップされ、そのスタックトレースは表示されません。これにより、通常のデバッグ出力が簡潔に保たれます。
  • m->throwing = -1;: デッドロックが検出された場合、m->throwing-1 に設定されます。これは、この状況では完全なスタックダンプを行わないことを示します。

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

@@ -219,6 +219,7 @@ struct	G
 	G*	schedlink;
 	bool	readyonstop;
 	bool	ispanic;
+	bool	issystem;
 	int8	traceignore; // ignore race detection events
 	M*	m;		// for debuggers, but offset not hard-coded
 	M*	lockedm;
@@ -252,6 +253,7 @@ struct	M
 	G*	curg;		// current running goroutine
 	int32	id;
 	int32	mallocing;
+	int32	throwing;
 	int32	gcing;
 	int32	locks;
 	int32	nomemprof;
@@ -865,7 +867,7 @@ Hmap*	runtime·makemap_c(MapType*, int64);
 Hchan*	runtime·makechan_c(ChanType*, int64);
 void	runtime·chansend(ChanType*, Hchan*, byte*, bool*, void*);
 void	runtime·chanrecv(ChanType*, Hchan*, byte*, bool*, bool*);
-bool	runtime·showframe(Func*);
+bool	runtime·showframe(Func*, bool);

G 構造体に issystem が追加され、ゴルーチンがシステムゴルーチンであるかを識別できるようになりました。M 構造体には throwing が追加され、パニック状態を管理します。また、runtime·showframe 関数のシグネチャが変更され、current という新しいブール引数を受け取るようになりました。

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

 bool
-runtime·showframe(Func *f)
+runtime·showframe(Func *f, bool current)
 {
 	static int32 traceback = -1;
 
+	if(current && m->throwing > 0)
+		return 1;
 	if(traceback < 0)
 		traceback = runtime·gotraceback();
-	return traceback > 1 || contains(f->name, ".") && !hasprefix(f->name, "runtime.");
+	return traceback > 1 || f != nil && contains(f->name, ".") && !hasprefix(f->name, "runtime.");
 }

runtime·showframe は、スタックトレースの各フレームを表示するかどうかを決定する重要な関数です。

  • if(current && m->throwing > 0) return 1;: この行が追加されたことで、もし現在トレースバック中のフレームがパニックを起こしたゴルーチンのものであり(currenttrue)、かつランタイムがパニック状態にある(m->throwing > 0)ならば、そのフレームは常に表示されるようになります。これにより、パニックを起こしたゴルーチンのスタックトレースは常に完全な形でダンプされます。
  • f != nil &&: fnil でないことのチェックが追加され、より堅牢になりました。

src/pkg/runtime/traceback_arm.c および src/pkg/runtime/traceback_x86.c の変更

@@ -60,7 +60,7 @@ runtime·gentraceback(byte *pc0, byte *sp, byte *lr0, G *gp, int32 skip, uintptr
 		\tsp = (byte*)stk->gobuf.sp;
 		\tlr = 0;
 		\tfp = nil;
-\t\t\tif(pcbuf == nil)
+\t\t\tif(pcbuf == nil && runtime·showframe(nil, gp == m->curg))
 		\t\t\truntime·printf("----- stack segment boundary -----\\n");
 		\t\t\tstk = (Stktop*)stk->stackbase;
 		\t\t\tcontinue;
@@ -118,7 +118,7 @@ runtime·gentraceback(byte *pc0, byte *sp, byte *lr0, G *gp, int32 skip, uintptr
 		\telse if(pcbuf != nil)
 		\t\tpcbuf[n++] = pc;
 		\telse {
-\t\t\t\tif(runtime·showframe(f)) {
+\t\t\t\tif(runtime·showframe(f, gp == m->curg)) {
 		\t\t\t\t// Print during crash.
 		\t\t\t\t//\tmain(0x1, 0x2, 0x3)
 		\t\t\t\t//\t\t/home/rsc/go/src/runtime/x.go:23 +0xf
@@ -184,7 +184,8 @@ runtime·gentraceback(byte *pc0, byte *sp, byte *lr0, G *gp, int32 skip, uintptr
 		\tsp += 12;
 	}
 	\t
-\tif(pcbuf == nil && (pc = gp->gopc) != 0 && (f = runtime·findfunc(pc)) != nil && gp->goid != 1) {
+\tif(pcbuf == nil && (pc = gp->gopc) != 0 && (f = runtime·findfunc(pc)) != nil
+\t\t\t&& runtime·showframe(f, gp == m->curg) && gp->goid != 1) {
 		\truntime·printf("created by %S\\n", f->name);
 		\ttracepc = pc;\t// back up to CALL instruction for funcline.
 		\tif(n > 0 && pc > f->entry)

runtime·gentraceback は、特定のアーキテクチャ(ARM, x86)向けのスタックトレース生成ロジックです。

  • 「stack segment boundary」メッセージの表示条件に runtime·showframe(nil, gp == m->curg) が追加されました。これにより、GOTRACEBACK の値が 1 より大きい場合にのみこのメッセージが表示されるようになります。
  • runtime·showframe の呼び出し箇所すべてに gp == m->curg という引数が追加されました。これは、現在処理中のゴルーチンが、スタックトレースをダンプしている対象のゴルーチン(m->curg)であるかどうかを示します。これにより、runtime·showframe はパニックを起こしたゴルーチンのスタックを特別に扱うことができるようになります。
  • ゴルーチンが作成された場所を示す「created by」メッセージの表示条件にも runtime·showframe(f, gp == m->curg) が追加され、表示の制御がより細かくなりました。

これらの変更は、Goランタイムのデバッグ機能の柔軟性と詳細度を大幅に向上させ、特にランタイム自体のバグを診断する際に非常に有用です。

関連リンク

参考にした情報源リンク