[インデックス 16472] ファイルの概要
このコミットは、Goランタイムにstackguard0
という新しいフィールドをG
(ゴルーチン)構造体に追加するものです。これは、Goのプリエンプティブスケジューラの実装の一部として導入されました。stackguard0
はスタック分割チェックで使用され、プリエンプションをトリガーするために特別な値(StackPreempt
)に設定されることがあります。一方、既存のstackguard
フィールドは元のスタックガード値を保持し、StackPreempt
には設定されません。
コミット
commit f5becf4233bd12506cbfcb9cbc04b5968ac11ae0
Author: Dmitriy Vyukov <dvyukov@google.com>
Date: Mon Jun 3 12:28:24 2013 +0400
runtime: add stackguard0 to G
This is part of preemptive scheduler.
stackguard0 is checked in split stack checks and can be set to StackPreempt.
stackguard is not set to StackPreempt (holds the original value).
R=golang-dev, daniel.morsing, iant
CC=golang-dev
https://golang.org/cl/9875043
GitHub上でのコミットページへのリンク
https://github.com/golang/go/commit/f5becf4233bd12506cbfcb9cbc04b5968ac11ae0
元コミット内容
runtime: add stackguard0 to G
This is part of preemptive scheduler.
stackguard0 is checked in split stack checks and can be set to StackPreempt.
stackguard is not set to StackPreempt (holds the original value).
変更の背景
この変更は、Goランタイムにおけるプリエンプティブスケジューラ(preemptive scheduler)の導入に向けた重要なステップです。Goの初期のスケジューラは協調的(cooperative)であり、ゴルーチンは明示的にスケジューラに制御を返す(例えば、チャネル操作やシステムコールなど)までCPUを占有し続ける可能性がありました。これにより、計算量の多いゴルーチンが他のゴルーチンの実行を妨げ、レイテンシの増加やデッドロックのような問題を引き起こす可能性がありました。
プリエンプティブスケジューラは、ゴルーチンが長時間CPUを占有するのを防ぎ、より公平なCPU時間の配分を実現するために導入されました。Goのプリエンプションは、スタックチェックのメカニズムを利用して実装されています。関数呼び出し時に行われるスタックチェックは、通常、スタックの拡張が必要かどうかを判断するために使用されますが、このメカニズムを流用して、スケジューラがゴルーチンをプリエンプトする機会を挿入します。
このコミットでは、プリエンプションのトリガーとして使用される新しいスタックガード値StackPreempt
を導入するために、G
構造体にstackguard0
フィールドが追加されました。これにより、実際のスタック境界を示すstackguard
とは別に、プリエンプションの目的で一時的に変更される可能性のあるチェック用のスタックガード値を持つことができるようになります。
前提知識の解説
Goのゴルーチンとスケジューラ
- ゴルーチン (Goroutine): Goにおける軽量な実行単位です。OSのスレッドよりもはるかに軽量で、数百万個を同時に実行することも可能です。Goランタイムが管理し、M:Nスケジューリングモデル(M個のOSスレッド上でN個のゴルーチンを実行)で動作します。
- Goスケジューラ: Goランタイムに組み込まれたスケジューラは、ゴルーチンをOSスレッド(M)にマッピングし、実行を管理します。初期のGoスケジューラは協調的であり、ゴルーチンが自発的に制御を返さない限り、プリエンプションは行われませんでした。
Goのスタック管理とスタック分割 (Segmented Stacks)
Goは「セグメントスタック (segmented stacks)」と呼ばれるスタック管理方式を採用していました(Go 1.7で連続スタックに移行するまで)。これは、ゴルーチンのスタックが固定サイズではなく、必要に応じて小さなセグメント(チャンク)に分割され、動的に拡張・縮小される仕組みです。
- スタックガード (Stack Guard): 各ゴルーチンのスタックには、スタックオーバーフローを検出するための「スタックガードページ」または「スタックガード値」が設定されています。関数呼び出しのプロローグ(関数の冒頭部分)で、現在のスタックポインタがこのスタックガード値を超えていないかチェックされます。
- スタック分割チェック (Split Stack Check): 関数呼び出し時にスタックガード値との比較が行われ、スタックが足りない場合は、より大きなスタックセグメントを割り当ててスタックを拡張する処理(
morestack
関数など)が呼び出されます。このチェックは、Goの関数呼び出しのオーバーヘッドの一部でした。
プリエンプションとStackPreempt
- プリエンプション (Preemption): 実行中のタスク(この場合はゴルーチン)の実行を、そのタスクが自発的に制御を返さなくても、スケジューラが強制的に中断させることです。これにより、スケジューラは他のタスクにCPU時間を割り当てることができます。
StackPreempt
: Goのプリエンプティブスケジューラでは、特定のゴルーチンをプリエンプトしたい場合、そのゴルーチンのstackguard
(このコミット以降はstackguard0
)フィールドに特別な値StackPreempt
を設定します。次にそのゴルーチンが関数呼び出しを行う際、スタックチェックが実行され、stackguard0
がStackPreempt
であると検出されます。これにより、通常のスタック拡張処理ではなく、プリエンプション処理(スケジューラへの制御の返還)がトリガーされます。
技術的詳細
このコミットの核心は、G
構造体(ゴルーチンを表すランタイム内部の構造体)にstackguard0
という新しいフィールドを追加し、既存のstackguard
フィールドとの役割を分離することです。
-
G
構造体の変更:src/pkg/runtime/runtime.h
において、G
構造体にuintptr stackguard0;
が追加されました。コメントには「stackguard0
はStackPreempt
に設定できるが、stackguard
はそうではない」と明記されています。これにより、stackguard
はゴルーチンの実際のスタック境界を常に保持し、stackguard0
はスタックチェックの際に参照される値として、プリエンプションのトリガーのために一時的に変更される役割を担います。 -
アセンブリコードの変更 (
asm_386.s
,asm_amd64.s
,asm_arm.s
): Goのランタイムは、各アーキテクチャ向けにアセンブリコードで書かれた低レベルの初期化処理やスタックチェック処理を含んでいます。これらのファイルでは、_rt0_386
、_rt0_amd64
、_rt0_arm
といった初期化ルーチンにおいて、g0
(初期ゴルーチン)のstackguard
とstackbase
を設定する際に、新しく追加されたg_stackguard0
も同じ値で初期化されるようになりました。 また、_cgo_init
(Cgo関連の初期化)の後にg_stackguard
をg_stackguard0
の値で更新する処理が追加されています。これは、Cgoの初期化プロセスがスタックガード値に影響を与える可能性があり、その後に正しいstackguard
値をstackguard0
からコピーして同期させるためと考えられます。 -
ランタイムCコードの変更 (
panic.c
,proc.c
,stack.c
): GoランタイムのCコードでは、ゴルーチンのスタックが設定または復元される様々な箇所で、stackguard0
もstackguard
と同じ値に設定されるようになりました。runtime·unwindstack
(panic.c): スタックのアンワインド時に、gp->stackguard0 = gp->stackguard;
が追加され、stackguard0
も復元される。runtime·mstart
(proc.c): M(OSスレッド)の開始時に、m->g0->stackguard = m->g0->stackguard0;
が追加され、g0
のstackguard
がstackguard0
からコピーされる。これは、Cgoがstackguard0
のみを設定する場合があるため、stackguard
も同期させる目的です。runtime·needm
(proc.c): 新しいMが必要な場合に、g->stackguard0 = g->stackguard;
が追加される。execute
(proc.c): ゴルーチンが実行される直前に、gp->stackguard0 = gp->stackguard;
が追加される。runtime·malg
(proc.c): 新しいゴルーチンが割り当てられる際に、newg->stackguard0 = newg->stackguard;
が追加される。runtime·oldstack
(stack.c): 古いスタックを復元する際に、gp->stackguard0 = gp->stackguard;
が追加される。runtime·newstack
(stack.c): 新しいスタックを割り当てる際に、gp->stackguard0 = gp->stackguard;
が追加される。
これらの変更により、stackguard0
は常にstackguard
の初期値と同じになるように設定されますが、プリエンプティブスケジューラがゴルーチンをプリエンプトしたい場合、stackguard0
のみをStackPreempt
に設定し、実際のスタック境界を示すstackguard
は変更しないという運用が可能になります。これにより、スタックチェックのロジックを流用しつつ、スタックの健全性を損なうことなくプリエンプションを実現できます。
コアとなるコードの変更箇所
src/pkg/runtime/runtime.h
--- a/src/pkg/runtime/runtime.h
+++ b/src/pkg/runtime/runtime.h
@@ -226,7 +226,8 @@ struct GCStats
};
struct G
{
- uintptr stackguard; // cannot move - also known to linker, libmach, runtime/cgo
+ // stackguard0 can be set to StackPreempt as opposed to stackguard
+ uintptr stackguard0; // cannot move - also known to linker, libmach, runtime/cgo
uintptr stackbase; // cannot move - also known to libmach, runtime/cgo
Defer* defer;
Panic* panic;
@@ -235,6 +236,7 @@ struct G
uintptr gcsp; // if status==Gsyscall, gcsp = sched.sp to use during gc
byte* gcpc; // if status==Gsyscall, gcpc = sched.pc to use during gc
uintptr gcguard; // if status==Gsyscall, gcguard = stackguard to use during gc
+ uintptr stackguard; // same as stackguard0, but not set to StackPreempt
uintptr stack0;
FuncVal* fnstart; // initial function
G* alllink; // on allg
src/pkg/runtime/asm_386.s
(他のアーキテクチャのアセンブリファイルも同様)
--- a/src/pkg/runtime/asm_386.s
+++ b/src/pkg/runtime/asm_386.s
@@ -18,6 +18,7 @@ TEXT _rt0_386(SB),7,$0
MOVL $runtime·g0(SB), BP
LEAL (-64*1024+104)(SP), BX
MOVL BX, g_stackguard(BP)
+ MOVL BX, g_stackguard0(BP)
MOVL SP, g_stackbase(BP)
// find out information about the processor we're on
@@ -41,6 +42,10 @@ nocpuinfo:
MOVL BX, 4(SP)
MOVL BP, 0(SP)
CALL AX
+ // update stackguard after _cgo_init
+ MOVL $runtime·g0(SB), CX
+ MOVL g_stackguard0(CX), AX
+ MOVL AX, g_stackguard(CX)
// skip runtime·ldt0setup(SB) and tls test after _cgo_init for non-windows
CMPL runtime·iswindows(SB), $0
JEQ ok
src/pkg/runtime/proc.c
(他のCファイルも同様)
--- a/src/pkg/runtime/proc.c
+++ b/src/pkg/runtime/proc.c
@@ -468,6 +468,7 @@ runtime·mstart(void)\n // so other calls can reuse this stack space.\n runtime·gosave(&m->g0->sched);\n m->g0->sched.pc = (void*)-1; // make sure it is never used\n+ m->g0->stackguard = m->g0->stackguard0; // cgo sets only stackguard0, copy it to stackguard\n m->seh = &seh;\n runtime·asminit();\n runtime·minit();
@@ -615,6 +616,7 @@ runtime·needm(byte x)\n runtime·setmg(mp, mp->g0);\n g->stackbase = (uintptr)(&x + 1024);\n g->stackguard = (uintptr)(&x - 32*1024);\n+ g->stackguard0 = g->stackguard;\n \n // On windows/386, we need to put an SEH frame (two words)\n // somewhere on the current stack. We are called\n@@ -979,6 +981,7 @@ execute(G *gp)\n \truntime·throw(\"execute: bad g status\");\n }\n gp->status = Grunning;\n+ gp->stackguard0 = gp->stackguard;\n m->p->tick++;\n m->curg = gp;\n gp->m = m;\
@@ -1465,6 +1468,7 @@ runtime·malg(int32 stacksize)\n \t}\n \tnewg->stack0 = (uintptr)stk;\n \tnewg->stackguard = (uintptr)stk + StackGuard;\n+\t\tnewg->stackguard0 = newg->stackguard;\n \tnewg->stackbase = (uintptr)stk + StackSystem + stacksize - sizeof(Stktop);\n \truntime·memclr((byte*)newg->stackbase, sizeof(Stktop));\n }
コアとなるコードの解説
runtime.h
におけるG
構造体の変更
uintptr stackguard0;
: 新しく追加されたフィールドです。このフィールドは、関数プロローグでのスタックチェック時に参照されるスタックガード値として機能します。プリエンプティブスケジューラがゴルーチンをプリエンプトしたい場合、このstackguard0
に特別な値StackPreempt
を設定します。uintptr stackguard;
: 既存のスタックガードフィールドです。このコミット以降、このフィールドはゴルーチンの実際のスタック境界(スタックがこれ以上伸びるとオーバーフローするアドレス)を保持する役割に特化します。StackPreempt
のような特殊な値は設定されません。
この分離により、ランタイムはスタックの物理的な境界を常に把握しつつ、stackguard0
を操作することで、スタックチェックのメカニズムをプリエンプションのトリガーとして利用できるようになります。
アセンブリコードの変更
アセンブリコード(asm_386.s
, asm_amd64.s
, asm_arm.s
)では、Goランタイムの初期化フェーズでg0
(初期ゴルーチン)のスタックが設定される際に、g_stackguard
とg_stackguard0
の両方が同じ初期値で設定されます。これは、両フィールドが初期状態では同じスタック境界を指すことを保証するためです。
また、_cgo_init
呼び出し後にg_stackguard
をg_stackguard0
の値で更新する処理が追加されています。これは、Cgoの初期化がスタックガード値に影響を与える可能性があり、その後にstackguard
がstackguard0
と同期されるようにするためです。これにより、Cgoが関与するシナリオでも、stackguard0
がプリエンプションのトリガーとして正しく機能するための基盤が整います。
ランタイムCコードの変更
panic.c
, proc.c
, stack.c
といったランタイムのCコードでは、ゴルーチンのスタックが初期化、復元、または再割り当てされる様々な場所で、gp->stackguard0 = gp->stackguard;
という行が追加されています。これは、stackguard0
が常にstackguard
の現在の値(つまり、ゴルーチンの実際のスタック境界)を反映するようにするためです。
この同期は、プリエンプションがトリガーされていない通常の実行時には、stackguard0
がスタックの物理的な境界チェックに引き続き使用されることを意味します。スケジューラがプリエンプションを意図的に行いたい場合にのみ、stackguard0
がStackPreempt
に設定され、次の関数呼び出し時のスタックチェックでプリエンプションが発動する、という流れになります。
特にruntime·mstart
関数におけるm->g0->stackguard = m->g0->stackguard0;
の行は、Cgoがstackguard0
のみを設定する可能性があるため、g0
のstackguard
もstackguard0
からコピーして同期させることを明示しています。これにより、Cgoとの連携においても、プリエンプションのメカニズムが正しく機能するようになります。
関連リンク
- Goのプリエンプティブスケジューラに関する公式ブログ記事(このコミットの後の情報ですが、背景理解に役立ちます):
- Go's new non-cooperative preemption (Go 1.14での完全な非協調的プリエンプションについて)
- Go's work-stealing scheduler (Go 1.2でのスケジューラ改善について、プリエンプションの文脈で言及されることもあります)
参考にした情報源リンク
- Goのソースコード (golang/go GitHub repository)
- Goの公式ドキュメントおよびブログ
- Goのランタイムスケジューラに関する技術記事や解説
[インデックス 16472] ファイルの概要
このコミットは、Goランタイムにstackguard0
という新しいフィールドをG
(ゴルーチン)構造体に追加するものです。これは、Goのプリエンプティブスケジューラの実装の一部として導入されました。stackguard0
はスタック分割チェックで使用され、プリエンプションをトリガーするために特別な値(StackPreempt
)に設定されることがあります。一方、既存のstackguard
フィールドは元のスタックガード値を保持し、StackPreempt
には設定されません。
コミット
commit f5becf4233bd12506cbfcb9cbc04b5968ac11ae0
Author: Dmitriy Vyukov <dvyukov@google.com>
Date: Mon Jun 3 12:28:24 2013 +0400
runtime: add stackguard0 to G
This is part of preemptive scheduler.
stackguard0 is checked in split stack checks and can be set to StackPreempt.
stackguard is not set to StackPreempt (holds the original value).
R=golang-dev, daniel.morsing, iant
CC=golang-dev
https://golang.org/cl/9875043
GitHub上でのコミットページへのリンク
https://github.com/golang/go/commit/f5becf4233bd12506cbfcb9cbc04b5968ac11ae0
元コミット内容
runtime: add stackguard0 to G
This is part of preemptive scheduler.
stackguard0 is checked in split stack checks and can be set to StackPreempt.
stackguard is not set to StackPreempt (holds the original value).
変更の背景
この変更は、Goランタイムにおけるプリエンプティブスケジューラ(preemptive scheduler)の導入に向けた重要なステップです。Goの初期のスケジューラは協調的(cooperative)であり、ゴルーチンは明示的にスケジューラに制御を返す(例えば、I/O操作、チャネル操作、システムコールなど)までCPUを占有し続ける可能性がありました。これにより、計算量の多いゴルーチンが他のゴルーチンの実行を妨げ、レイテンシの増加やデッドロックのような問題を引き起こす可能性がありました。
プリエンプティブスケジューラは、ゴルーチンが長時間CPUを占有するのを防ぎ、より公平なCPU時間の配分を実現するために導入されました。Goのプリエンプションは、スタックチェックのメカニズムを利用して実装されています。関数呼び出し時に行われるスタックチェックは、通常、スタックの拡張が必要かどうかを判断するために使用されますが、このメカニズムを流用して、スケジューラがゴルーチンをプリエンプトする機会を挿入します。
このコミットでは、プリエンプションのトリガーとして使用される新しいスタックガード値StackPreempt
を導入するために、G
構造体にstackguard0
フィールドが追加されました。これにより、実際のスタック境界を示すstackguard
とは別に、プリエンプションの目的で一時的に変更される可能性のあるチェック用のスタックガード値を持つことができるようになります。これは、Go 1.14で導入された非協調的プリエンプションの基盤となる重要な変更の一つです。
前提知識の解説
Goのゴルーチンとスケジューラ
- ゴルーチン (Goroutine): Goにおける軽量な実行単位です。OSのスレッドよりもはるかに軽量で、数百万個を同時に実行することも可能です。Goランタイムが管理し、M:Nスケジューリングモデル(M個のOSスレッド上でN個のゴルーチンを実行)で動作します。
- Goスケジューラ: Goランタイムに組み込まれたスケジューラは、ゴルーチンをOSスレッド(M)にマッピングし、実行を管理します。初期のGoスケジューラは協調的であり、ゴルーチンが自発的に制御を返さない限り、プリエンプションは行われませんでした。Go 1.14以降では、非協調的プリエンプションが導入され、長時間実行されるゴルーチンも強制的に中断できるようになりました。
Goのスタック管理とスタック分割 (Segmented Stacks)
Goは「セグメントスタック (segmented stacks)」と呼ばれるスタック管理方式を採用していました(Go 1.7で連続スタックに移行するまで)。これは、ゴルーチンのスタックが固定サイズではなく、必要に応じて小さなセグメント(チャンク)に分割され、動的に拡張・縮小される仕組みです。
- スタックガード (Stack Guard): 各ゴルーチンのスタックには、スタックオーバーフローを検出するための「スタックガードページ」または「スタックガード値」が設定されています。関数呼び出しのプロローグ(関数の冒頭部分)で、現在のスタックポインタがこのスタックガード値を超えていないかチェックされます。
- スタック分割チェック (Split Stack Check): 関数呼び出し時にスタックガード値との比較が行われ、スタックが足りない場合は、より大きなスタックセグメントを割り当ててスタックを拡張する処理(
morestack
関数など)が呼び出されます。このチェックは、Goの関数呼び出しのオーバーヘッドの一部でした。
プリエンプションとStackPreempt
- プリエンプション (Preemption): 実行中のタスク(この場合はゴルーチン)の実行を、そのタスクが自発的に制御を返さなくても、スケジューラが強制的に中断させることです。これにより、スケジューラは他のタスクにCPU時間を割り当てることができます。
StackPreempt
: Goのプリエンプティブスケジューラでは、特定のゴルーチンをプリエンプトしたい場合、そのゴルーチンのstackguard0
フィールドに特別な値StackPreempt
を設定します。この値は、非常に大きな値(例:0xfffffade
)として定義され、通常のスタックポインタがこの値を超えることはありません。次にそのゴルーチンが関数呼び出しを行う際、スタックチェックが実行され、スタックポインタがstackguard0
(StackPreempt
に設定されている)よりも大きいと判断されます。これにより、通常のスタック拡張処理ではなく、morestack()
関数が呼び出され、その中でプリエンプション処理(スケジューラへの制御の返還)がトリガーされます。
技術的詳細
このコミットの核心は、G
構造体(ゴルーチンを表すランタイム内部の構造体)にstackguard0
という新しいフィールドを追加し、既存のstackguard
フィールドとの役割を分離することです。
-
G
構造体の変更:src/pkg/runtime/runtime.h
において、G
構造体にuintptr stackguard0;
が追加されました。コメントには「stackguard0
はStackPreempt
に設定できるが、stackguard
はそうではない」と明記されています。これにより、stackguard
はゴルーチンの実際のスタック境界を常に保持し、stackguard0
はスタックチェックの際に参照される値として、プリエンプションのトリガーのために一時的に変更される役割を担います。 -
アセンブリコードの変更 (
asm_386.s
,asm_amd64.s
,asm_arm.s
): Goのランタイムは、各アーキテクチャ向けにアセンブリコードで書かれた低レベルの初期化処理やスタックチェック処理を含んでいます。これらのファイルでは、_rt0_386
、_rt0_amd64
、_rt0_arm
といった初期化ルーチンにおいて、g0
(初期ゴルーチン)のstackguard
とstackbase
を設定する際に、新しく追加されたg_stackguard0
も同じ値で初期化されるようになりました。 また、_cgo_init
(Cgo関連の初期化)の後にg_stackguard
をg_stackguard0
の値で更新する処理が追加されています。これは、Cgoの初期化プロセスがスタックガード値に影響を与える可能性があり、その後に正しいstackguard
値をstackguard0
からコピーして同期させるためと考えられます。 -
ランタイムCコードの変更 (
panic.c
,proc.c
,stack.c
): GoランタイムのCコードでは、ゴルーチンのスタックが設定または復元される様々な箇所で、stackguard0
もstackguard
と同じ値に設定されるようになりました。runtime·unwindstack
(panic.c
): スタックのアンワインド時に、gp->stackguard0 = gp->stackguard;
が追加され、stackguard0
も復元される。runtime·mstart
(proc.c
): M(OSスレッド)の開始時に、m->g0->stackguard = m->g0->stackguard0;
が追加され、g0
のstackguard
がstackguard0
からコピーされる。これは、Cgoがstackguard0
のみを設定する場合があるため、stackguard
も同期させる目的です。runtime·needm
(proc.c
): 新しいMが必要な場合に、g->stackguard0 = g->stackguard;
が追加される。execute
(proc.c
): ゴルーチンが実行される直前に、gp->stackguard0 = gp->stackguard;
が追加される。runtime·malg
(proc.c
): 新しいゴルーチンが割り当てられる際に、newg->stackguard0 = newg->stackguard;
が追加される。runtime·oldstack
(stack.c
): 古いスタックを復元する際に、gp->stackguard0 = gp->stackguard;
が追加される。runtime·newstack
(stack.c
): 新しいスタックを割り当てる際に、gp->stackguard0 = gp->stackguard;
が追加される。
これらの変更により、stackguard0
は常にstackguard
の初期値と同じになるように設定されますが、プリエンプティブスケジューラがゴルーチンをプリエンプトしたい場合、stackguard0
のみをStackPreempt
に設定し、実際のスタック境界を示すstackguard
は変更しないという運用が可能になります。これにより、スタックチェックのロジックを流用しつつ、スタックの健全性を損なうことなくプリエンプションを実現できます。
コアとなるコードの変更箇所
src/pkg/runtime/runtime.h
--- a/src/pkg/runtime/runtime.h
+++ b/src/pkg/runtime/runtime.h
@@ -226,7 +226,8 @@ struct GCStats
};
struct G
{
- uintptr stackguard; // cannot move - also known to linker, libmach, runtime/cgo
+ // stackguard0 can be set to StackPreempt as opposed to stackguard
+ uintptr stackguard0; // cannot move - also known to linker, libmach, runtime/cgo
uintptr stackbase; // cannot move - also known to libmach, runtime/cgo
Defer* defer;
Panic* panic;
@@ -235,6 +236,7 @@ struct G
uintptr gcsp; // if status==Gsyscall, gcsp = sched.sp to use during gc
byte* gcpc; // if status==Gsyscall, gcpc = sched.pc to use during gc
uintptr gcguard; // if status==Gsyscall, gcguard = stackguard to use during gc
+ uintptr stackguard; // same as stackguard0, but not set to StackPreempt
uintptr stack0;
FuncVal* fnstart; // initial function
G* alllink; // on allg
src/pkg/runtime/asm_386.s
(他のアーキテクチャのアセンブリファイルも同様)
--- a/src/pkg/runtime/asm_386.s
+++ b/src/pkg/runtime/asm_386.s
@@ -18,6 +18,7 @@ TEXT _rt0_386(SB),7,$0
MOVL $runtime·g0(SB), BP
LEAL (-64*1024+104)(SP), BX
MOVL BX, g_stackguard(BP)
+ MOVL BX, g_stackguard0(BP)
MOVL SP, g_stackbase(BP)
// find out information about the processor we're on
@@ -41,6 +42,10 @@ nocpuinfo:
MOVL BX, 4(SP)
MOVL BP, 0(SP)
CALL AX
+ // update stackguard after _cgo_init
+ MOVL $runtime·g0(SB), CX
+ MOVL g_stackguard0(CX), AX
+ MOVL AX, g_stackguard(CX)
// skip runtime·ldt0setup(SB) and tls test after _cgo_init for non-windows
CMPL runtime·iswindows(SB), $0
JEQ ok
src/pkg/runtime/proc.c
(他のCファイルも同様)
--- a/src/pkg/runtime/proc.c
+++ b/src/pkg/runtime/proc.c
@@ -468,6 +468,7 @@ runtime·mstart(void)\n // so other calls can reuse this stack space.\n runtime·gosave(&m->g0->sched);\n m->g0->sched.pc = (void*)-1; // make sure it is never used\n+ m->g0->stackguard = m->g0->stackguard0; // cgo sets only stackguard0, copy it to stackguard\n m->seh = &seh;\n runtime·asminit();\n runtime·minit();
@@ -615,6 +616,7 @@ runtime·needm(byte x)\n runtime·setmg(mp, mp->g0);\n g->stackbase = (uintptr)(&x + 1024);\n g->stackguard = (uintptr)(&x - 32*1024);\n+ g->stackguard0 = g->stackguard;\n \n // On windows/386, we need to put an SEH frame (two words)\n // somewhere on the current stack. We are called\n@@ -979,6 +981,7 @@ execute(G *gp)\n \truntime·throw(\"execute: bad g status\");\n }\n gp->status = Grunning;\n+ gp->stackguard0 = gp->stackguard;\n m->p->tick++;\n m->curg = gp;\n gp->m = m;\
@@ -1465,6 +1468,7 @@ runtime·malg(int32 stacksize)\n \t}\n \tnewg->stack0 = (uintptr)stk;\n \tnewg->stackguard = (uintptr)stk + StackGuard;\n+\t\tnewg->stackguard0 = newg->stackguard;\n \tnewg->stackbase = (uintptr)stk + StackSystem + stacksize - sizeof(Stktop);\n \truntime·memclr((byte*)newg->stackbase, sizeof(Stktop));\n }
コアとなるコードの解説
runtime.h
におけるG
構造体の変更
uintptr stackguard0;
: 新しく追加されたフィールドです。このフィールドは、関数プロローグでのスタックチェック時に参照されるスタックガード値として機能します。プリエンプティブスケジューラがゴルーチンをプリエンプトしたい場合、このstackguard0
に特別な値StackPreempt
を設定します。uintptr stackguard;
: 既存のスタックガードフィールドです。このコミット以降、このフィールドはゴルーチンの実際のスタック境界(スタックがこれ以上伸びるとオーバーフローするアドレス)を保持する役割に特化します。StackPreempt
のような特殊な値は設定されません。
この分離により、ランタイムはスタックの物理的な境界を常に把握しつつ、stackguard0
を操作することで、スタックチェックのメカニズムをプリエンプションのトリガーとして利用できるようになります。
アセンブリコードの変更
アセンブリコード(asm_386.s
, asm_amd64.s
, asm_arm.s
)では、Goランタイムの初期化フェーズでg0
(初期ゴルーチン)のスタックが設定される際に、g_stackguard
とg_stackguard0
の両方が同じ初期値で設定されます。これは、両フィールドが初期状態では同じスタック境界を指すことを保証するためです。
また、_cgo_init
呼び出し後にg_stackguard
をg_stackguard0
の値で更新する処理が追加されています。これは、Cgoの初期化がスタックガード値に影響を与える可能性があり、その後にstackguard
がstackguard0
と同期されるようにするためです。これにより、Cgoが関与するシナリオでも、stackguard0
がプリエンプションのトリガーとして正しく機能するための基盤が整います。
ランタイムCコードの変更
panic.c
, proc.c
, stack.c
といったランタイムのCコードでは、ゴルーチンのスタックが初期化、復元、または再割り当てされる様々な場所で、gp->stackguard0 = gp->stackguard;
という行が追加されています。これは、stackguard0
が常にstackguard
の現在の値(つまり、ゴルーチンの実際のスタック境界)を反映するようにするためです。
この同期は、プリエンプションがトリガーされていない通常の実行時には、stackguard0
がスタックの物理的な境界チェックに引き続き使用されることを意味します。スケジューラがプリエンプションを意図的に行いたい場合にのみ、stackguard0
がStackPreempt
に設定され、次の関数呼び出し時のスタックチェックでプリエンプションが発動する、という流れになります。
特にruntime·mstart
関数におけるm->g0->stackguard = m->g0->stackguard0;
の行は、Cgoがstackguard0
のみを設定する可能性があるため、g0
のstackguard
もstackguard0
からコピーして同期させることを明示しています。これにより、Cgoとの連携においても、プリエンプションのメカニズムが正しく機能するようになります。
関連リンク
- Goのプリエンプティブスケジューラに関する公式ブログ記事(このコミットの後の情報ですが、背景理解に役立ちます):
- Go's new non-cooperative preemption (Go 1.14での完全な非協調的プリエンプションについて)
- Go's work-stealing scheduler (Go 1.2でのスケジューラ改善について、プリエンプションの文脈で言及されることもあります)
参考にした情報源リンク
- Goのソースコード (golang/go GitHub repository)
- Goの公式ドキュメントおよびブログ
- Goのランタイムスケジューラに関する技術記事や解説
- Web search results for "Go preemptive scheduler stack guard" (unskilled.blog, dzone.com, medium.com, kodekloud.com, github.io, ardanlabs.com, google.com, jiajunhuang.com)