[インデックス 16441] ファイルの概要
このコミットは、Goランタイムの runtime.goexit
関数に nosplit
フラグを付与する変更です。これは、Goのプリエンプティブスケジューラ導入に伴うスタック管理の整合性問題を解決するために行われました。
コミット
commit 573d25a42342ae094edeafc7066646cf825eb255
Author: Dmitriy Vyukov <dvyukov@google.com>
Date: Thu May 30 14:11:49 2013 +0400
runtime: mark runtime.goexit as nosplit
Required for preemptive scheduler, see the comment.
R=golang-dev, daniel.morsing
CC=golang-dev
https://golang.org/cl/9841047
GitHub上でのコミットページへのリンク
https://github.com/golang/go/commit/573d25a42342ae094edeafc7066646cf825eb255
元コミット内容
src/pkg/runtime/proc.c
ファイルにおいて、runtime·goexit
関数の定義の直前に #pragma textflag 7
というディレクティブが追加されました。これにより、この関数が nosplit
としてマークされます。
--- a/src/pkg/runtime/proc.c
+++ b/src/pkg/runtime/proc.c
@@ -1223,6 +1223,10 @@ gosched0(G *gp)
}
// Finishes execution of the current goroutine.
+// Need to mark it as nosplit, because it runs with sp > stackbase (as runtime·lessstack).
+// Since it does not return it does not matter. But if it is preempted
+// at the split stack check, GC will complain about inconsistent sp.
+#pragma textflag 7
void
runtime·goexit(void)
{
変更の背景
この変更は、Goランタイムにおけるプリエンプティブスケジューラの導入に関連しています。Goの初期のバージョンでは、スケジューラは協調的(cooperative)であり、ゴルーチンが関数呼び出しを行う際に明示的にスケジューラに制御を返す必要がありました。しかし、これにより長時間実行される計算処理が他のゴルーチンの実行を妨げ、レイテンシの問題を引き起こす可能性がありました。
プリエンプティブスケジューラは、実行中のゴルーチンを強制的に中断し、別のゴルーチンに切り替えることを可能にします。これにより、より公平なCPU時間の配分と応答性の向上が期待されます。
runtime.goexit
関数は、現在のゴルーチンの実行を終了させる役割を担っています。この関数が特定のスタック状態(sp > stackbase
、すなわち runtime·lessstack
と呼ばれる状態)で実行される際に、プリエンプティブスケジューラによるスタック分割チェック(stack split check)で中断されると、ガベージコレクタ(GC)がスタックの不整合を検知し、問題を引き起こす可能性がありました。この問題を回避するために、runtime.goexit
を nosplit
としてマークする必要がありました。
前提知識の解説
-
ゴルーチン (Goroutine): Go言語における軽量な実行単位です。OSのスレッドよりもはるかに軽量で、数百万個のゴルーチンを同時に実行することも可能です。Goランタイムがゴルーチンのスケジューリング、スタック管理、同期などを担当します。
-
Goスケジューラ: ゴルーチンをOSスレッドにマッピングし、実行を管理するGoランタイムの一部です。M:Nスケジューリングモデル(M個のゴルーチンをN個のOSスレッドで実行)を採用しています。
-
スタック管理とスタック分割 (Stack Splitting): Goのゴルーチンは、最初は小さなスタック(通常は数KB)で開始されます。関数呼び出しの際に、現在のスタックが不足すると判断された場合、Goランタイムは自動的にスタックを拡張します。このプロセスを「スタック分割」と呼びます。関数エントリ時に、スタックの残量を確認し、必要であれば新しい、より大きなスタックを割り当てて、古いスタックの内容をコピーし、スタックポインタを新しいスタックに切り替えます。
-
プリエンプティブスケジューラ (Preemptive Scheduler): スケジューラの一種で、実行中のタスク(この場合はゴルーチン)が自発的に制御を返さなくても、一定時間経過後や特定のイベント発生時に強制的に中断し、別のタスクに切り替えることができます。これにより、特定のタスクがCPUを占有し続けることを防ぎ、システムの応答性を向上させます。Go 1.2以降で、より本格的なプリエンプティブスケジューリングが導入されました。
-
ガベージコレクタ (GC): Goの自動メモリ管理システムです。GCは、プログラムがもはや参照しないメモリ領域を自動的に解放します。GCが正しく動作するためには、実行中のゴルーチンのスタックが常に整合性の取れた状態である必要があります。スタック上のポインタを正確に識別し、到達可能なオブジェクトをマークするためです。
-
nosplit
ディレクティブ: Goのコンパイラに対する内部的な指示で、特定の関数に対してスタック分割チェックを挿入しないように指示します。通常、Goの関数はエントリ時にスタックの残量を確認し、必要に応じてスタックを拡張しますが、nosplit
が付与された関数は、そのチェックを行いません。これは、スタックが非常に小さい状態や、スタックの拡張が不適切な状況(例えば、ランタイムの非常に低レベルな部分)で使用されます。 -
#pragma textflag 7
: これはGoコンパイラ(gc
)に対するプラグマディレクティブの一つです。textflag 7
は、Goの内部的な定数NOSPLIT
に対応します。つまり、このプラグマを関数に適用することで、その関数がnosplit
として扱われるようになります。
技術的詳細
runtime.goexit
関数は、ゴルーチンの実行を終了させる特殊な関数です。この関数は、通常の関数呼び出しとは異なり、呼び出し元に戻ることはありません。コミットメッセージのコメントにあるように、この関数は sp > stackbase
の状態で実行されることがあります。これは、スタックポインタ(sp
)がスタックの基底アドレス(stackbase
)よりも大きい、つまりスタックが通常よりも「少ない」状態、あるいはスタックの末尾に近い特殊な状態であることを示唆しています。Goランタイムの文脈では、これを runtime·lessstack
と呼ぶことがあります。
問題は、プリエンプティブスケジューラが導入されたことです。プリエンプティブスケジューラは、任意の時点でゴルーチンを中断し、別のゴルーチンに切り替えることができます。もし runtime.goexit
が lessstack
の状態で実行中に、スタック分割チェックのタイミングでプリエンプション(強制中断)された場合、ガベージコレクタがスタックをスキャンする際に、スタックポインタが不整合な状態にあると判断してしまう可能性があります。GCはスタック上のポインタを正確に追跡して到達可能なオブジェクトを特定するため、スタックの不整合はGCの誤動作やクラッシュにつながる重大な問題です。
runtime.goexit
はゴルーチンを終了させるため、スタックを拡張する必要がありませんし、そもそも戻り値もありません。したがって、この関数にスタック分割チェックを挿入することは無意味であり、むしろ前述のGCの問題を引き起こす原因となります。
そこで、#pragma textflag 7
を runtime.goexit
に適用することで、コンパイラはこの関数にスタック分割チェックのコードを生成しなくなります。これにより、runtime.goexit
が lessstack
状態で実行中にプリエンプションされても、GCがスタックの不整合を検知することなく、安全に動作することが保証されます。
この変更は、Goランタイムの堅牢性を高め、プリエンプティブスケジューラの導入を円滑に進める上で不可欠な修正でした。
コアとなるコードの変更箇所
変更は src/pkg/runtime/proc.c
ファイルの runtime·goexit
関数の定義部分です。
// Finishes execution of the current goroutine.
+// Need to mark it as nosplit, because it runs with sp > stackbase (as runtime·lessstack).
+// Since it does not return it does not matter. But if it is preempted
+// at the split stack check, GC will complain about inconsistent sp.
+#pragma textflag 7
void
runtime·goexit(void)
{
具体的には、runtime·goexit
関数のシグネチャの直前に、新しいコメントと #pragma textflag 7
の行が追加されています。
コアとなるコードの解説
追加されたコメントは、この変更の理由を明確に説明しています。
-
// Need to mark it as nosplit, because it runs with sp > stackbase (as runtime·lessstack).
runtime.goexit
がnosplit
である必要がある理由を述べています。この関数が実行される際、スタックポインタ(sp
)がスタックの基底(stackbase
)よりも大きい状態、つまりスタックが通常よりも「少ない」状態(runtime·lessstack
)で動作することがあります。
-
// Since it does not return it does not matter.
runtime.goexit
はゴルーチンを終了させるため、呼び出し元に戻ることはありません。したがって、スタックの拡張や戻り値の処理は関係ありません。
-
// But if it is preempted // at the split stack check, GC will complain about inconsistent sp.
- これがこの変更の最も重要な理由です。もし
runtime.goexit
がスタック分割チェックの最中にプリエンプション(強制中断)された場合、ガベージコレクタ(GC)がスタックをスキャンする際に、スタックポインタが不整合な状態にあると判断し、エラーやクラッシュを引き起こす可能性があります。
- これがこの変更の最も重要な理由です。もし
-
#pragma textflag 7
- この行が実際の変更を実装しています。Goコンパイラ(
gc
)に対するプラグマディレクティブであり、runtime·goexit
関数にNOSPLIT
フラグを付与するよう指示します。これにより、コンパイラはruntime·goexit
のエントリポイントにスタック分割チェックのコードを生成しなくなります。結果として、この関数はスタックの拡張を試みることなく、現在のスタック上で安全に実行され、プリエンプションによるGCの不整合問題が回避されます。
- この行が実際の変更を実装しています。Goコンパイラ(
この変更は、Goのランタイムがプリエンプティブスケジューリングを安全かつ効率的にサポートするために必要な、低レベルながらも重要な修正です。
関連リンク
- Goのスタック管理に関する議論(Go 1.2以前のセグメントスタックとGo 1.3以降の連続スタックへの移行など):
- Go's new stack allocator (Go 1.3でのスタックアロケータの変更に関する公式ブログ)
- Goのプリエンプティブスケジューリングに関する情報:
- Go's preemptive scheduler (Go 1.14での非協調的プリエンプションに関する公式ブログ)
- Goの内部的な
textflag
について(Goのソースコードやアセンブリコードを読む際に役立つ):- Goのソースコード内の
src/cmd/internal/obj/textflag.go
などで定義を確認できます。
- Goのソースコード内の
参考にした情報源リンク
- Goの公式ドキュメントおよびブログ
- Goのソースコード (
src/pkg/runtime/proc.c
および関連ファイル) - Goのコンパイラに関するドキュメントやソースコード (
src/cmd/internal/obj/textflag.go
など) - Goのガベージコレクションに関する技術記事
- Goのスケジューラに関する技術記事
- Goのスタック管理に関する技術記事
- GitHubのGoリポジトリのコミット履歴と関連するIssueやCL (Change List)
[インデックス 16441] ファイルの概要
このコミットは、Goランタイムの runtime.goexit
関数に nosplit
フラグを付与する変更です。これは、Goのプリエンプティブスケジューラ導入に伴うスタック管理の整合性問題を解決するために行われました。
コミット
commit 573d25a42342ae094edeafc7066646cf825eb255
Author: Dmitriy Vyukov <dvyukov@google.com>
Date: Thu May 30 14:11:49 2013 +0400
runtime: mark runtime.goexit as nosplit
Required for preemptive scheduler, see the comment.
R=golang-dev, daniel.morsing
CC=golang-dev
https://golang.org/cl/9841047
GitHub上でのコミットページへのリンク
https://github.com/golang/go/commit/573d25a42342ae094edeafc7066646cf825eb255
元コミット内容
src/pkg/runtime/proc.c
ファイルにおいて、runtime·goexit
関数の定義の直前に #pragma textflag 7
というディレクティブが追加されました。これにより、この関数が nosplit
としてマークされます。
--- a/src/pkg/runtime/proc.c
+++ b/src/pkg/runtime/proc.c
@@ -1223,6 +1223,10 @@ gosched0(G *gp)
}
// Finishes execution of the current goroutine.
+// Need to mark it as nosplit, because it runs with sp > stackbase (as runtime·lessstack).
+// Since it does not return it does not matter. But if it is preempted
+// at the split stack check, GC will complain about inconsistent sp.
+#pragma textflag 7
void
runtime·goexit(void)
{
変更の背景
この変更は、Goランタイムにおけるプリエンプティブスケジューラの導入に関連しています。Goの初期のバージョンでは、スケジューラは協調的(cooperative)であり、ゴルーチンが関数呼び出しを行う際に明示的にスケジューラに制御を返す必要がありました。しかし、これにより長時間実行される計算処理が他のゴルーチンの実行を妨げ、レイテンシの問題を引き起こす可能性がありました。
プリエンプティブスケジューラは、実行中のゴルーチンを強制的に中断し、別のゴルーチンに切り替えることを可能にします。これにより、より公平なCPU時間の配分と応答性の向上が期待されます。
runtime.goexit
関数は、現在のゴルーチンの実行を終了させる役割を担っています。この関数が特定のスタック状態(sp > stackbase
、すなわち runtime·lessstack
と呼ばれる状態)で実行される際に、プリエンプティブスケジューラによるスタック分割チェック(stack split check)で中断されると、ガベージコレクタ(GC)がスタックの不整合を検知し、問題を引き起こす可能性がありました。この問題を回避するために、runtime.goexit
を nosplit
としてマークする必要がありました。
前提知識の解説
-
ゴルーチン (Goroutine): Go言語における軽量な実行単位です。OSのスレッドよりもはるかに軽量で、数百万個のゴルーチンを同時に実行することも可能です。Goランタイムがゴルーチンのスケジューリング、スタック管理、同期などを担当します。
-
Goスケジューラ: ゴルーチンをOSスレッドにマッピングし、実行を管理するGoランタイムの一部です。M:Nスケジューリングモデル(M個のゴルーチンをN個のOSスレッドで実行)を採用しています。
-
スタック管理とスタック分割 (Stack Splitting): Goのゴルーチンは、最初は小さなスタック(通常は数KB)で開始されます。関数呼び出しの際に、現在のスタックが不足すると判断された場合、Goランタイムは自動的にスタックを拡張します。このプロセスを「スタック分割」と呼びます。関数エントリ時に、スタックの残量を確認し、必要であれば新しい、より大きなスタックを割り当てて、古いスタックの内容をコピーし、スタックポインタを新しいスタックに切り替えます。
-
プリエンプティブスケジューラ (Preemptive Scheduler): スケジューラの一種で、実行中のタスク(この場合はゴルーチン)が自発的に制御を返さなくても、一定時間経過後や特定のイベント発生時に強制的に中断し、別のタスクに切り替えることができます。これにより、特定のタスクがCPUを占有し続けることを防ぎ、システムの応答性を向上させます。Go 1.2以降で、より本格的なプリエンプティブスケジューリングが導入されました。
-
ガベージコレクタ (GC): Goの自動メモリ管理システムです。GCは、プログラムがもはや参照しないメモリ領域を自動的に解放します。GCが正しく動作するためには、実行中のゴルーチンのスタックが常に整合性の取れた状態である必要があります。スタック上のポインタを正確に識別し、到達可能なオブジェクトをマークするためです。
-
nosplit
ディレクティブ: Goのコンパイラに対する内部的な指示で、特定の関数に対してスタック分割チェックを挿入しないように指示します。通常、Goの関数はエントリ時にスタックの残量を確認し、必要に応じてスタックを拡張しますが、nosplit
が付与された関数は、そのチェックを行いません。これは、スタックが非常に小さい状態や、スタックの拡張が不適切な状況(例えば、ランタイムの非常に低レベルな部分)で使用されます。 -
#pragma textflag 7
: これはGoコンパイラ(gc
)に対するプラグマディレクティブの一つです。textflag 7
は、Goの内部的な定数NOSPLIT
に対応します。つまり、このプラグマを関数に適用することで、その関数がnosplit
として扱われるようになります。
技術的詳細
runtime.goexit
関数は、ゴルーチンの実行を終了させる特殊な関数です。この関数は、通常の関数呼び出しとは異なり、呼び出し元に戻ることはありません。コミットメッセージのコメントにあるように、この関数は sp > stackbase
の状態で実行されることがあります。これは、スタックポインタ(sp
)がスタックの基底アドレス(stackbase
)よりも大きい、つまりスタックが通常よりも「少ない」状態、あるいはスタックの末尾に近い特殊な状態であることを示唆しています。Goランタイムの文脈では、これを runtime·lessstack
と呼ぶことがあります。
問題は、プリエンプティブスケジューラが導入されたことです。プリエンプティブスケジューラは、任意の時点でゴルーチンを中断し、別のゴルーチンに切り替えることができます。もし runtime.goexit
が lessstack
の状態で実行中に、スタック分割チェックのタイミングでプリエンプション(強制中断)された場合、ガベージコレクタがスタックをスキャンする際に、スタックポインタが不整合な状態にあると判断してしまう可能性があります。GCはスタック上のポインタを正確に追跡して到達可能なオブジェクトを特定するため、スタックの不整合はGCの誤動作やクラッシュにつながる重大な問題です。
runtime.goexit
はゴルーチンを終了させるため、スタックを拡張する必要がありませんし、そもそも戻り値もありません。したがって、この関数にスタック分割チェックを挿入することは無意味であり、むしろ前述のGCの問題を引き起こす原因となります。
そこで、#pragma textflag 7
を runtime.goexit
に適用することで、コンパイラはこの関数にスタック分割チェックのコードを生成しなくなります。これにより、runtime.goexit
が lessstack
状態で実行中にプリエンプションされても、GCがスタックの不整合を検知することなく、安全に動作することが保証されます。
この変更は、Goランタイムの堅牢性を高め、プリエンプティブスケジューラの導入を円滑に進める上で不可欠な修正でした。
コアとなるコードの変更箇所
変更は src/pkg/runtime/proc.c
ファイルの runtime·goexit
関数の定義部分です。
// Finishes execution of the current goroutine.
+// Need to mark it as nosplit, because it runs with sp > stackbase (as runtime·lessstack).
+// Since it does not return it does not matter. But if it is preempted
+// at the split stack check, GC will complain about inconsistent sp.
+#pragma textflag 7
void
runtime·goexit(void)
{
具体的には、runtime·goexit
関数のシグネチャの直前に、新しいコメントと #pragma textflag 7
の行が追加されています。
コアとなるコードの解説
追加されたコメントは、この変更の理由を明確に説明しています。
-
// Need to mark it as nosplit, because it runs with sp > stackbase (as runtime·lessstack).
runtime.goexit
がnosplit
である必要がある理由を述べています。この関数が実行される際、スタックポインタ(sp
)がスタックの基底(stackbase
)よりも大きい状態、つまりスタックが通常よりも「少ない」状態(runtime·lessstack
)で動作することがあります。
-
// Since it does not return it does not matter.
runtime.goexit
はゴルーチンを終了させるため、呼び出し元に戻ることはありません。したがって、スタックの拡張や戻り値の処理は関係ありません。
-
// But if it is preempted // at the split stack check, GC will complain about inconsistent sp.
- これがこの変更の最も重要な理由です。もし
runtime.goexit
がスタック分割チェックの最中にプリエンプション(強制中断)された場合、ガベージコレクタ(GC)がスタックをスキャンする際に、スタックポインタが不整合な状態にあると判断し、エラーやクラッシュを引き起こす可能性があります。
- これがこの変更の最も重要な理由です。もし
-
#pragma textflag 7
- この行が実際の変更を実装しています。Goコンパイラ(
gc
)に対するプラグマディレクティブであり、runtime·goexit
関数にNOSPLIT
フラグを付与するよう指示します。これにより、コンパイラはruntime·goexit
のエントリポイントにスタック分割チェックのコードを生成しなくなります。結果として、この関数はスタックの拡張を試みることなく、現在のスタック上で安全に実行され、プリエンプションによるGCの不整合問題が回避されます。
- この行が実際の変更を実装しています。Goコンパイラ(
この変更は、Goのランタイムがプリエンプティブスケジューリングを安全かつ効率的にサポートするために必要な、低レベルながらも重要な修正です。
関連リンク
- Goのスタック管理に関する議論(Go 1.2以前のセグメントスタックとGo 1.3以降の連続スタックへの移行など):
- Go's new stack allocator (Go 1.3でのスタックアロケータの変更に関する公式ブログ)
- Goのプリエンプティブスケジューリングに関する情報:
- Go's preemptive scheduler (Go 1.14での非協調的プリエンプションに関する公式ブログ)
- Goの内部的な
textflag
について(Goのソースコードやアセンブリコードを読む際に役立つ):- Goのソースコード内の
src/cmd/internal/obj/textflag.go
などで定義を確認できます。
- Goのソースコード内の
参考にした情報源リンク
- Goの公式ドキュメントおよびブログ
- Goのソースコード (
src/pkg/runtime/proc.c
および関連ファイル) - Goのコンパイラに関するドキュメントやソースコード (
src/cmd/internal/obj/textflag.go
など) - Goのガベージコレクションに関する技術記事
- Goのスケジューラに関する技術記事
- Goのスタック管理に関する技術記事
- GitHubのGoリポジトリのコミット履歴と関連するIssueやCL (Change List)