[インデックス 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ランタイムのスタックトレース生成ロジックに複数の変更を加えています。主な変更点は以下の通りです。
-
m->throwingフラグの導入と利用:M構造体(OSスレッドを表すランタイム内部の構造体)にthrowingという新しいフィールドが追加されました。これは、現在パニック処理中のゴルーチンが存在するかどうか、およびその詳細度を制御するためのフラグとして機能します。runtime·throw関数(パニックを引き起こす関数)が呼び出された際に、m->throwingが0であれば1に設定されます。これにより、ランタイムがパニック状態にあることを示します。- デッドロック検出時など、特定の状況下でスタックダンプを抑制したい場合には、
m->throwing = -1が設定されます。これは、完全なスタックダンプを行わないことを意味します。
-
システムゴルーチンの識別と制御:
G構造体(ゴルーチンを表すランタイム内部の構造体)にissystemという新しいブール型フィールドが追加されました。これにより、Goランタイム自身が内部的に作成・管理するゴルーチン(例: スケジューラ、ガベージコレクタ、タイマープロセッサなど)を識別できるようになります。runtime·MHeap_Scavengerやtimerprocといったシステムゴルーチンが作成される際に、このissystemフラグがtrueに設定されます。runtime·tracebackothers関数(他のゴルーチンのトレースバックを行う関数)において、GOTRACEBACKの値が2(system) 未満の場合、issystemがtrueのゴルーチンはトレースバックの対象から除外されるようになりました。これにより、通常のデバッグ時にはシステムゴルーチンの詳細が隠蔽され、GOTRACEBACK=system以上の場合にのみ表示されるようになります。
-
runtime·showframe関数の変更:runtime·showframe関数は、スタックトレースにおいて特定のフレームを表示するかどうかを決定します。この関数にbool currentという新しい引数が追加されました。これは、現在トレースバック中のフレームが、パニックを起こしたゴルーチンのフレームであるかどうかを示します。currentがtrueかつm->throwing > 0の場合、常にフレームを表示するようになりました。これにより、パニックを起こしたゴルーチンのスタックは常に完全な形でダンプされるようになります。- また、
GOTRACEBACKの値が1より大きい場合(つまりsystemまたはcrashレベルの場合)や、関数名にドットが含まれ、かつruntime.プレフィックスを持たない関数(ユーザーコードの関数)の場合にフレームを表示するという既存のロジックも維持されています。
-
スタックセグメント境界の表示制御:
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;: この行が追加されたことで、もし現在トレースバック中のフレームがパニックを起こしたゴルーチンのものであり(currentがtrue)、かつランタイムがパニック状態にある(m->throwing > 0)ならば、そのフレームは常に表示されるようになります。これにより、パニックを起こしたゴルーチンのスタックトレースは常に完全な形でダンプされます。f != nil &&:fがnilでないことのチェックが追加され、より堅牢になりました。
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ランタイムのデバッグ機能の柔軟性と詳細度を大幅に向上させ、特にランタイム自体のバグを診断する際に非常に有用です。
関連リンク
- Go言語の公式ドキュメント: https://go.dev/
GOTRACEBACK環境変数に関するGoのドキュメント(Go 1.5以降のdebugパッケージのドキュメントに統合されていることが多いです): https://pkg.go.dev/runtime/debug#SetTraceback
参考にした情報源リンク
- GoのスタックトレースとGOTRACEBACKについて:
- https://go.dev/doc/diagnostics
- https://betterprogramming.pub/understanding-go-stack-traces-and-gotraceback-for-debugging-c7221122122d
- https://www.debuginn.com/post/go-gotraceback/
- https://www.cheney.net/articles/gotraceback-and-go-1-5
- https://stackoverflow.com/questions/20620077/how-to-get-a-full-stack-trace-in-go
- Goのランタイム内部構造に関する一般的な情報: