[インデックス 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のランタイム内部構造に関する一般的な情報: