Keyboard shortcuts

Press or to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

[インデックス 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を設定します。次にそのゴルーチンが関数呼び出しを行う際、スタックチェックが実行され、stackguard0StackPreemptであると検出されます。これにより、通常のスタック拡張処理ではなく、プリエンプション処理(スケジューラへの制御の返還)がトリガーされます。

技術的詳細

このコミットの核心は、G構造体(ゴルーチンを表すランタイム内部の構造体)にstackguard0という新しいフィールドを追加し、既存のstackguardフィールドとの役割を分離することです。

  1. G構造体の変更: src/pkg/runtime/runtime.hにおいて、G構造体にuintptr stackguard0;が追加されました。コメントには「stackguard0StackPreemptに設定できるが、stackguardはそうではない」と明記されています。これにより、stackguardはゴルーチンの実際のスタック境界を常に保持し、stackguard0はスタックチェックの際に参照される値として、プリエンプションのトリガーのために一時的に変更される役割を担います。

  2. アセンブリコードの変更 (asm_386.s, asm_amd64.s, asm_arm.s): Goのランタイムは、各アーキテクチャ向けにアセンブリコードで書かれた低レベルの初期化処理やスタックチェック処理を含んでいます。これらのファイルでは、_rt0_386_rt0_amd64_rt0_armといった初期化ルーチンにおいて、g0(初期ゴルーチン)のstackguardstackbaseを設定する際に、新しく追加されたg_stackguard0も同じ値で初期化されるようになりました。 また、_cgo_init(Cgo関連の初期化)の後にg_stackguardg_stackguard0の値で更新する処理が追加されています。これは、Cgoの初期化プロセスがスタックガード値に影響を与える可能性があり、その後に正しいstackguard値をstackguard0からコピーして同期させるためと考えられます。

  3. ランタイムCコードの変更 (panic.c, proc.c, stack.c): GoランタイムのCコードでは、ゴルーチンのスタックが設定または復元される様々な箇所で、stackguard0stackguardと同じ値に設定されるようになりました。

    • runtime·unwindstack (panic.c): スタックのアンワインド時に、gp->stackguard0 = gp->stackguard;が追加され、stackguard0も復元される。
    • runtime·mstart (proc.c): M(OSスレッド)の開始時に、m->g0->stackguard = m->g0->stackguard0;が追加され、g0stackguardstackguard0からコピーされる。これは、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_stackguardg_stackguard0の両方が同じ初期値で設定されます。これは、両フィールドが初期状態では同じスタック境界を指すことを保証するためです。

また、_cgo_init呼び出し後にg_stackguardg_stackguard0の値で更新する処理が追加されています。これは、Cgoの初期化がスタックガード値に影響を与える可能性があり、その後にstackguardstackguard0と同期されるようにするためです。これにより、Cgoが関与するシナリオでも、stackguard0がプリエンプションのトリガーとして正しく機能するための基盤が整います。

ランタイムCコードの変更

panic.c, proc.c, stack.cといったランタイムのCコードでは、ゴルーチンのスタックが初期化、復元、または再割り当てされる様々な場所で、gp->stackguard0 = gp->stackguard;という行が追加されています。これは、stackguard0が常にstackguardの現在の値(つまり、ゴルーチンの実際のスタック境界)を反映するようにするためです。

この同期は、プリエンプションがトリガーされていない通常の実行時には、stackguard0がスタックの物理的な境界チェックに引き続き使用されることを意味します。スケジューラがプリエンプションを意図的に行いたい場合にのみ、stackguard0StackPreemptに設定され、次の関数呼び出し時のスタックチェックでプリエンプションが発動する、という流れになります。

特にruntime·mstart関数におけるm->g0->stackguard = m->g0->stackguard0;の行は、Cgoがstackguard0のみを設定する可能性があるため、g0stackguardstackguard0からコピーして同期させることを明示しています。これにより、Cgoとの連携においても、プリエンプションのメカニズムが正しく機能するようになります。

関連リンク

  • Goのプリエンプティブスケジューラに関する公式ブログ記事(このコミットの後の情報ですが、背景理解に役立ちます):

参考にした情報源リンク

  • 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)として定義され、通常のスタックポインタがこの値を超えることはありません。次にそのゴルーチンが関数呼び出しを行う際、スタックチェックが実行され、スタックポインタがstackguard0StackPreemptに設定されている)よりも大きいと判断されます。これにより、通常のスタック拡張処理ではなく、morestack()関数が呼び出され、その中でプリエンプション処理(スケジューラへの制御の返還)がトリガーされます。

技術的詳細

このコミットの核心は、G構造体(ゴルーチンを表すランタイム内部の構造体)にstackguard0という新しいフィールドを追加し、既存のstackguardフィールドとの役割を分離することです。

  1. G構造体の変更: src/pkg/runtime/runtime.hにおいて、G構造体にuintptr stackguard0;が追加されました。コメントには「stackguard0StackPreemptに設定できるが、stackguardはそうではない」と明記されています。これにより、stackguardはゴルーチンの実際のスタック境界を常に保持し、stackguard0はスタックチェックの際に参照される値として、プリエンプションのトリガーのために一時的に変更される役割を担います。

  2. アセンブリコードの変更 (asm_386.s, asm_amd64.s, asm_arm.s): Goのランタイムは、各アーキテクチャ向けにアセンブリコードで書かれた低レベルの初期化処理やスタックチェック処理を含んでいます。これらのファイルでは、_rt0_386_rt0_amd64_rt0_armといった初期化ルーチンにおいて、g0(初期ゴルーチン)のstackguardstackbaseを設定する際に、新しく追加されたg_stackguard0も同じ値で初期化されるようになりました。 また、_cgo_init(Cgo関連の初期化)の後にg_stackguardg_stackguard0の値で更新する処理が追加されています。これは、Cgoの初期化プロセスがスタックガード値に影響を与える可能性があり、その後に正しいstackguard値をstackguard0からコピーして同期させるためと考えられます。

  3. ランタイムCコードの変更 (panic.c, proc.c, stack.c): GoランタイムのCコードでは、ゴルーチンのスタックが設定または復元される様々な箇所で、stackguard0stackguardと同じ値に設定されるようになりました。

    • runtime·unwindstack (panic.c): スタックのアンワインド時に、gp->stackguard0 = gp->stackguard;が追加され、stackguard0も復元される。
    • runtime·mstart (proc.c): M(OSスレッド)の開始時に、m->g0->stackguard = m->g0->stackguard0;が追加され、g0stackguardstackguard0からコピーされる。これは、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_stackguardg_stackguard0の両方が同じ初期値で設定されます。これは、両フィールドが初期状態では同じスタック境界を指すことを保証するためです。

また、_cgo_init呼び出し後にg_stackguardg_stackguard0の値で更新する処理が追加されています。これは、Cgoの初期化がスタックガード値に影響を与える可能性があり、その後にstackguardstackguard0と同期されるようにするためです。これにより、Cgoが関与するシナリオでも、stackguard0がプリエンプションのトリガーとして正しく機能するための基盤が整います。

ランタイムCコードの変更

panic.c, proc.c, stack.cといったランタイムのCコードでは、ゴルーチンのスタックが初期化、復元、または再割り当てされる様々な場所で、gp->stackguard0 = gp->stackguard;という行が追加されています。これは、stackguard0が常にstackguardの現在の値(つまり、ゴルーチンの実際のスタック境界)を反映するようにするためです。

この同期は、プリエンプションがトリガーされていない通常の実行時には、stackguard0がスタックの物理的な境界チェックに引き続き使用されることを意味します。スケジューラがプリエンプションを意図的に行いたい場合にのみ、stackguard0StackPreemptに設定され、次の関数呼び出し時のスタックチェックでプリエンプションが発動する、という流れになります。

特にruntime·mstart関数におけるm->g0->stackguard = m->g0->stackguard0;の行は、Cgoがstackguard0のみを設定する可能性があるため、g0stackguardstackguard0からコピーして同期させることを明示しています。これにより、Cgoとの連携においても、プリエンプションのメカニズムが正しく機能するようになります。

関連リンク

  • Goのプリエンプティブスケジューラに関する公式ブログ記事(このコミットの後の情報ですが、背景理解に役立ちます):

参考にした情報源リンク

  • 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)