[インデックス 17048] ファイルの概要
このコミットは、Goランタイムにおけるゴルーチンのトレースバック処理、特にシステムコール中にブロックされているゴルーチンのスタックトレースの正確性を向上させるための変更です。具体的には、gp->schedフィールドの代わりに、より安定したgcpc/gcsp(後にsyscallpc/syscallspに改名)を使用するように修正されています。
コミット
commit 9c0500b466196388ab40e03c94759066bb1c7fe6
Author: Dmitriy Vyukov <dvyukov@google.com>
Date: Tue Aug 6 13:38:44 2013 +0400
runtime: use gcpc/gcsp during traceback of goroutines in syscalls
gcpc/gcsp are used by GC in similar situation.
gcpc/gcsp are also more stable than gp->sched,
because gp->sched is mutated by entersyscall/exitsyscall
in morestack and mcall. So it has higher chances of being inconsistent.
Also, rename gcpc/gcsp to syscallpc/syscallsp.
This is the same as reverted change 12250043
with save marked as textflag 7.
The problem was that if save calls morestack,
then subsequent lessstack spoils g->sched.pc/sp.
And that bad values were remembered in g->syscallpc/sp.
Entersyscallblock had the same problem,
but it was never triggered to date.
R=golang-dev, rsc
CC=golang-dev
https://golang.org/cl/12478043
GitHub上でのコミットページへのリンク
https://github.com/golang/go/commit/9c0500b466196388ab40e03c94759066bb1c7fe6
元コミット内容
runtime: use gcpc/gcsp during traceback of goroutines in syscalls
gcpc/gcsp are used by GC in similar situation.
gcpc/gcsp are also more stable than gp->sched,
because gp->sched is mutated by entersyscall/exitsyscall
in morestack and mcall. So it has higher chances of being inconsistent.
Also, rename gcpc/gcsp to syscallpc/syscallsp.
This is the same as reverted change 12250043
with save marked as textflag 7.
The problem was that if save calls morestack,
then subsequent lessstack spoils g->sched.pc/sp.
And that bad values were remembered in g->syscallpc/sp.
Entersyscallblock had the same problem,
but it was never triggered to date.
変更の背景
このコミットの背景には、Goランタイムにおけるゴルーチンのスタックトレースの正確性の問題がありました。特に、ゴルーチンがシステムコール中にブロックされている場合、そのゴルーチンのプログラムカウンタ(PC)とスタックポインタ(SP)を正確に取得することが困難でした。
以前のGoランタイムでは、システムコール中のゴルーチンのPC/SPをgp->sched.pcとgp->sched.spから取得しようとしていました。しかし、gp->schedはentersyscallやexitsyscallといったランタイム関数、特にmorestackやmcallの処理中に頻繁に更新されるため、その値が一時的に不整合になる可能性がありました。この不整合な値がトレースバック時に使用されると、誤ったスタックトレースが生成される原因となっていました。
この問題は、以前の変更(リバートされた変更12250043)で一度対処が試みられましたが、save関数がmorestackを呼び出す場合に、その後のlessstackがg->sched.pc/spを破壊し、その不正な値がg->syscallpc/spに記憶されてしまうという新たな問題を引き起こしました。entersyscallblockも同様の問題を抱えていましたが、これまでは顕在化していませんでした。
このコミットは、これらの問題を解決し、システムコール中のゴルーチンのトレースバックをより堅牢にするために導入されました。
前提知識の解説
このコミットを理解するためには、以下のGoランタイムの概念について理解しておく必要があります。
- ゴルーチン (Goroutine): Go言語における軽量な実行スレッドです。Goランタイムによってスケジューリングされ、OSのスレッドに多重化されて実行されます。
- M (Machine) と P (Processor): Goランタイムのスケジューラを構成する要素です。MはOSのスレッドを表し、PはGoコードを実行するための論理プロセッサを表します。ゴルーチンはP上で実行されます。
- G (Goroutine): ゴルーチンを表すランタイム内部のデータ構造です。各ゴルーチンは
G構造体によって管理され、そのスタック情報、スケジューリング情報などが含まれます。 - スタック (Stack): ゴルーチンが関数呼び出しを行う際に使用するメモリ領域です。Goのゴルーチンスタックは可変長であり、必要に応じて拡張・縮小されます。
- システムコール (System Call): プログラムがOSの機能(ファイルI/O、ネットワーク通信など)を利用するために、OSカーネルに処理を要求する仕組みです。システムコール中は、ゴルーチンはOSのスレッドに切り替わり、Goランタイムの管理下から一時的に離れます。
- トレースバック (Traceback): プログラムの実行中に、現在の関数呼び出しの履歴(スタックトレース)を遡って表示する機能です。デバッグやエラー解析に不可欠です。
gp->sched:G構造体内のフィールドで、ゴルーチンのスケジューリングに関連する情報(PC、SPなど)を保持します。gcpc/gcsp(後のsyscallpc/syscallsp): ゴルーチンがシステムコールに入る直前のPCとSPを保存するためのフィールドです。これらはガベージコレクション(GC)がシステムコール中のゴルーチンをスキャンする際にも使用されます。morestack/lessstack: Goランタイムがゴルーチンのスタックを拡張・縮小する際に呼び出される内部関数です。entersyscall/exitsyscall: ゴルーチンがシステムコールに入る際と出る際に呼び出されるランタイム関数です。これらの関数は、ゴルーチンの状態をGsyscallに設定したり、スタック情報を保存したりする役割を担います。textflag 7(NOSPLIT): Goコンパイラにおける関数属性の一つで、関数がスタックの拡張を必要としないことを示します。これにより、morestackの呼び出しを抑制し、スタックの不整合を防ぐことができます。
技術的詳細
このコミットの核心は、システムコール中のゴルーチンのスタックトレースの信頼性を高めるために、G構造体内の特定のフィールドの役割と使用方法を変更した点にあります。
以前は、システムコール中のゴルーチンのPCとSPは主にgp->sched.pcとgp->sched.spから取得されていました。しかし、これらのフィールドはゴルーチンのスケジューリング状態の変更(特にentersyscall/exitsyscall、morestack、mcallといったランタイム内部の処理)によって頻繁に更新されるため、一時的に不整合な状態になる可能性がありました。この不整合な値がトレースバック時に使用されると、誤ったスタックトレースが表示されるという問題がありました。
このコミットでは、この問題を解決するために以下の変更が行われました。
-
gcpc/gcspからsyscallpc/syscallspへの改名:G構造体内のgcpc、gcsp、gcstack、gcguardフィールドが、それぞれsyscallpc、syscallsp、syscallstack、syscallguardに改名されました。この改名は、これらのフィールドがガベージコレクションだけでなく、システムコール中のゴルーチンの状態を保存するためにも使用されることをより明確にするためのものです。 -
syscallpc/syscallspの利用:entersyscallおよびentersyscallblock関数内で、ゴルーチンがシステムコールに入る直前のPCとSPをgp->sched.pcとgp->sched.spから取得し、それを直接g->syscallpcとg->syscallspに保存するように変更されました。これにより、gp->schedの一時的な不整合な状態に影響されることなく、システムコール開始時の正確なPC/SPを記録できるようになります。 また、g->syscallstackとg->syscallguardも同様に、システムコール開始時のスタックベースとスタックガードの値を保存するように変更されました。 -
トレースバック時の
syscallpc/syscallspの使用:runtime·traceback関数(traceback_arm.cとtraceback_x86.c)において、ゴルーチンのステータスがGsyscall(システムコール中)である場合、トレースバックのPCとSPをgp->sched.pc/gp->sched.spからではなく、新しく導入されたgp->syscallpc/gp->syscallspから取得するように変更されました。これにより、システムコール中のゴルーチンのスタックトレースがより正確になります。 -
save関数の#pragma textflag 7(NOSPLIT) 指定:proc.c内のsave関数に#pragma textflag 7が追加されました。これは、save関数がスタックの拡張を必要としない(つまり、morestackを呼び出さない)ことをコンパイラに指示するものです。以前のリバートされた変更では、saveがmorestackを呼び出す可能性があり、その後のlessstackがg->sched.pc/spを破壊し、不正な値がg->syscallpc/spに記憶される問題がありました。NOSPLITを指定することで、この問題が回避され、syscallpc/syscallspに保存される値の信頼性が向上します。
これらの変更により、Goランタイムはシステムコール中のゴルーチンのスタック情報をより堅牢に管理できるようになり、デバッグ時やプロファイリング時におけるスタックトレースの正確性が大幅に向上しました。
コアとなるコードの変更箇所
このコミットにおける主要なコード変更は以下のファイルに集中しています。
src/pkg/runtime/mgc0.c: ガベージコレクション関連のコードで、システムコール中のゴルーチンのスタックをスキャンする際にgcpc/gcsp(改名後はsyscallpc/syscallsp)を使用するように変更されています。src/pkg/runtime/proc.c: ゴルーチンのスケジューリングとシステムコールへの出入りを管理するコードです。entersyscallおよびentersyscallblock関数内でsyscallpc/syscallspへの値の保存ロジックが変更され、save関数に#pragma textflag 7が追加されています。src/pkg/runtime/runtime.h:G構造体の定義が含まれており、gcpc/gcspなどのフィールドがsyscallpc/syscallspに改名されています。src/pkg/runtime/traceback_arm.cおよびsrc/pkg/runtime/traceback_x86.c: ARMおよびx86アーキテクチャ向けのトレースバック処理のコードです。システムコール中のゴルーチンのPC/SPをsyscallpc/syscallspから取得するように変更されています。
コアとなるコードの解説
src/pkg/runtime/mgc0.c の変更
--- a/src/pkg/runtime/mgc0.c
+++ b/src/pkg/runtime/mgc0.c
@@ -1457,17 +1457,17 @@ addstackroots(G *gp)
runtime·throw("can't scan our own stack");
if((mp = gp->m) != nil && mp->helpgc)
runtime·throw("can't scan gchelper stack");
- if(gp->gcstack != (uintptr)nil) {
+ if(gp->syscallstack != (uintptr)nil) {
// Scanning another goroutine that is about to enter or might
// have just exited a system call. It may be executing code such
// as schedlock and may have needed to start a new stack segment.
// Use the stack segment and stack pointer at the time of
// the system call instead, since that won't change underfoot.
- sp = gp->gcsp;
- pc = gp->gcpc;
+ sp = gp->syscallsp;
+ pc = gp->syscallpc;
lr = 0;
- stk = (Stktop*)gp->gcstack;
- guard = gp->gcguard;
+ stk = (Stktop*)gp->syscallstack;
+ guard = gp->syscallguard;
} else {
// Scanning another goroutine's stack.
// The goroutine is usually asleep (the world is stopped).
この部分では、ガベージコレクタが他のゴルーチンのスタックをスキャンする際に、システムコール中のゴルーチンであれば、gcstack/gcsp/gcpc/gcguardの代わりに、新しく改名されたsyscallstack/syscallsp/syscallpc/syscallguardを使用するように変更されています。これにより、システムコール中のゴルーチンのスタック情報がGCによって正確に参照されるようになります。
src/pkg/runtime/proc.c の変更
--- a/src/pkg/runtime/proc.c
+++ b/src/pkg/runtime/proc.c
@@ -1355,11 +1355,10 @@ goexit0(G *gp)
schedule();
}
+#pragma textflag 7
static void
save(void *pc, uintptr sp)
{
-\tg->gcpc = (uintptr)pc;
-\tg->gcsp = sp;
g->sched.pc = (uintptr)pc;
g->sched.sp = sp;
g->sched.lr = 0;
@@ -1384,15 +1383,16 @@ void
// but can have inconsistent g->sched, do not let GC observe it.
m->locks++;
- // Leave SP around for gc and traceback.
+ // Leave SP around for GC and traceback.
save(runtime·getcallerpc(&dummy), runtime·getcallersp(&dummy));
-\
-\tg->gcstack = g->stackbase;
-\tg->gcguard = g->stackguard;
+\tg->syscallsp = g->sched.sp;
+\tg->syscallpc = g->sched.pc;
+\tg->syscallstack = g->stackbase;
+\tg->syscallguard = g->stackguard;
g->status = Gsyscall;
-\tif(g->gcsp < g->gcguard-StackGuard || g->gcstack < g->gcsp) {
+\tif(g->syscallsp < g->syscallguard-StackGuard || g->syscallstack < g->syscallsp) {
// runtime·printf("entersyscall inconsistent %p [%p,%p]\n",
-\t\t//\tg->gcsp, g->gcguard-StackGuard, g->gcstack);\
+\t\t//\tg->syscallsp, g->syscallguard-StackGuard, g->syscallstack);\
runtime·throw("entersyscall");
}
@@ -1436,16 +1436,16 @@ void
m->locks++; // see comment in entersyscall
- // Leave SP around for gc and traceback.
+ // Leave SP around for GC and traceback.
save(runtime·getcallerpc(&dummy), runtime·getcallersp(&dummy));
-\tg->gcsp = g->sched.sp;
-\tg->gcpc = g->sched.pc;
-\tg->gcstack = g->stackbase;
-\tg->gcguard = g->stackguard;
+\tg->syscallsp = g->sched.sp;
+\tg->syscallpc = g->sched.pc;
+\tg->syscallstack = g->stackbase;
+\tg->syscallguard = g->stackguard;
g->status = Gsyscall;
-\tif(g->gcsp < g->gcguard-StackGuard || g->gcstack < g->gcsp) {
-\t\t// runtime·printf("entersyscallblock inconsistent %p [%p,%p]\n",
-\t\t//\tg->gcsp, g->gcguard-StackGuard, g->gcstack);\
+\tif(g->syscallsp < g->syscallguard-StackGuard || g->syscallstack < g->syscallsp) {
+\t\t// runtime·printf("entersyscall inconsistent %p [%p,%p]\n",
+\t\t//\tg->syscallsp, g->syscallguard-StackGuard, g->syscallstack);\
runtime·throw("entersyscallblock");
}
@@ -1480,8 +1480,8 @@ runtime·exitsyscall(void)
g->status = Grunning;
// Garbage collector isn't running (since we are),
// so okay to clear gcstack and gcsp.
-\t\tg->gcstack = (uintptr)nil;\
-\t\tg->gcsp = (uintptr)nil;\
+\t\tg->syscallstack = (uintptr)nil;\
+\t\tg->syscallsp = (uintptr)nil;\
m->locks--;
if(g->preempt) {
// restore the preemption request in case we've cleared it in newstack
@@ -1504,8 +1504,8 @@ runtime·exitsyscall(void)
// Must wait until now because until gosched returns
// we don't know for sure that the garbage collector
// is not running.
-\tg->gcstack = (uintptr)nil;\
-\tg->gcsp = (uintptr)nil;\
+\tg->syscallstack = (uintptr)nil;\
+\tg->syscallsp = (uintptr)nil;\
}
#pragma textflag 7
このファイルでは、save関数に#pragma textflag 7が追加され、この関数がスタックの拡張を伴わないように指示されています。これにより、save関数がg->sched.pc/spを破壊する可能性が排除されます。
また、entersyscallとentersyscallblock関数内で、システムコールに入る直前のゴルーチンのPC、SP、スタックベース、スタックガードの値を、g->schedから直接g->syscallpc、g->syscallsp、g->syscallstack、g->syscallguardにコピーするように変更されています。これにより、g->schedの一時的な不整合に影響されずに、システムコール開始時の正確なスタック情報を保存できるようになります。
exitsyscall関数では、システムコール終了時にsyscallstackとsyscallspをクリアする処理が追加されています。
src/pkg/runtime/runtime.h の変更
--- a/src/pkg/runtime/runtime.h
+++ b/src/pkg/runtime/runtime.h
@@ -253,10 +253,10 @@ struct G
Defer* defer;
Panic* panic;
Gobuf sched;
-\tuintptr gcstack; // if status==Gsyscall, gcstack = stackbase to use during gc
-\tuintptr gcsp; // if status==Gsyscall, gcsp = sched.sp to use during gc
-\tuintptr gcpc; // if status==Gsyscall, gcpc = sched.pc to use during gc
-\tuintptr gcguard; // if status==Gsyscall, gcguard = stackguard to use during gc
+\tuintptr syscallstack; // if status==Gsyscall, syscallstack = stackbase to use during gc
+\tuintptr syscallsp; // if status==Gsyscall, syscallsp = sched.sp to use during gc
+\tuintptr syscallpc; // if status==Gsyscall, syscallpc = sched.pc to use during gc
+\tuintptr syscallguard; // if status==Gsyscall, syscallguard = stackguard to use during gc
uintptr stackguard; // same as stackguard0, but not set to StackPreempt
uintptr stack0;
G* alllink; // on allg
G構造体内のフィールド名がgcstack、gcsp、gcpc、gcguardから、それぞれsyscallstack、syscallsp、syscallpc、syscallguardに改名されています。これは、これらのフィールドがガベージコレクションだけでなく、システムコール中のゴルーチンの状態を保存するためにも使用されることをより明確にするためのものです。
src/pkg/runtime/traceback_arm.c および src/pkg/runtime/traceback_x86.c の変更
--- a/src/pkg/runtime/traceback_arm.c
+++ b/src/pkg/runtime/traceback_arm.c
@@ -221,8 +221,8 @@ runtime·traceback(uintptr pc, uintptr sp, uintptr lr, G *gp)
{
if(gp->status == Gsyscall) {
// Override signal registers if blocked in system call.
-\t\tpc = gp->sched.pc;
-\t\tsp = gp->sched.sp;
+\t\tpc = gp->syscallpc;
+\t\tsp = gp->syscallsp;
lr = 0;
}
--- a/src/pkg/runtime/traceback_x86.c
+++ b/src/pkg/runtime/traceback_x86.c
@@ -229,8 +229,8 @@ runtime·traceback(uintptr pc, uintptr sp, uintptr lr, G *gp)
if(gp->status == Gsyscall) {
// Override signal registers if blocked in system call.
-\t\tpc = gp->sched.pc;
-\t\tsp = gp->sched.sp;
+\t\tpc = gp->syscallpc;
+\t\tsp = gp->syscallsp;
}
// Print traceback. By default, omits runtime frames.
これらのファイルでは、runtime·traceback関数内で、ゴルーチンがシステムコール中(gp->status == Gsyscall)である場合に、トレースバックに使用するPCとSPをgp->sched.pc/gp->sched.spからではなく、gp->syscallpc/gp->syscallspから取得するように変更されています。これにより、システムコール中のゴルーチンのスタックトレースが、システムコール開始時の正確な情報に基づいて生成されるようになります。
関連リンク
- Goの公式リポジトリでのコミット: https://github.com/golang/go/commit/9c0500b466196388ab40e03c94759066bb1c7fe6
- Gerrit Code Review (CL 12478043): https://golang.org/cl/12478043
参考にした情報源リンク
- Goの公式ドキュメント (Goランタイム、ゴルーチン、スケジューラに関する情報): https://go.dev/doc/
- Goのソースコード (特に
src/runtimeディレクトリ): https://github.com/golang/go/tree/master/src/runtime - Goのガベージコレクションに関する資料 (
gcpc/gcspの初期の利用文脈): https://go.dev/blog/go1.2gc (Go 1.2のGCに関するブログ記事など) - Goのスタック管理に関する資料 (
morestack/lessstack): https://go.dev/blog/go-stacks (Goのスタックに関するブログ記事など) - Goの
textflagに関する資料: https://go.dev/src/cmd/compile/internal/gc/go.go (Goコンパイラのソースコード内のコメントなど) - Goのシステムコールに関する資料: https://go.dev/src/runtime/syscall.go (Goランタイムのシステムコール関連のソースコード)
- Goのトレースバックに関する資料: https://go.dev/src/runtime/traceback.go (Goランタイムのトレースバック関連のソースコード)
- GoのGerrit Code Reviewシステム: https://go-review.googlesource.com/
- GoのIssue Tracker: https://go.dev/issue/
- Goのメーリングリスト (golang-dev): https://groups.google.com/g/golang-dev