[インデックス 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