[インデックス 1275] ファイルの概要
このコミットは、Go言語のランタイムにおけるスタック管理の初期段階の改善に関するものです。特に、ゴルーチンのスタック切り替え時に、古いスタックを安全に解放できるようにするための基盤を構築しています。stackalloc
とstackfree
というスタブ関数が導入され、oldstack
ルーチンがg0
(スケジューラのゴルーチン)のスタック上で実行されるように変更されています。
コミット
commit 79e1db2da13b0d9aafe39831bdb0c1b7940aab0c
Author: Russ Cox <rsc@golang.org>
Date: Thu Dec 4 08:30:54 2008 -0800
add stub routines stackalloc() and stackfree().
run oldstack on g0's stack, just like newstack does,
so that oldstack can free the old stack.
R=r
DELTA=53 (44 added, 0 deleted, 9 changed)
OCL=20404
CL=20433
GitHub上でのコミットページへのリンク
https://github.com/golang/go/commit/79e1db2da13b0d9aafe39831bdb0c1b7940aab0c
元コミット内容
stackalloc()
とstackfree()
というスタブルーチンを追加しました。
newstack
と同様に、oldstack
をg0
のスタック上で実行するようにしました。これにより、oldstack
が古いスタックを解放できるようになります。
変更の背景
Go言語の初期開発段階において、ゴルーチンのスタック管理は非常に重要な課題でした。Goのゴルーチンは、必要に応じてスタックサイズを動的に増減させる「可変スタック」を採用しています。スタックが不足した際には新しい大きなスタックに切り替える(newstack
)、そして不要になった古いスタックを解放する(oldstack
)という処理が必要です。
このコミット以前は、oldstack
ルーチンが、解放しようとしているまさにその古いスタック上で実行されている可能性がありました。これは、自身の足元を掘るようなもので、スタックの解放処理中にそのスタックが使われていると、メモリ破壊やクラッシュの原因となり得ます。安全にスタックを解放するためには、解放対象のスタックとは別の、安定したスタック上で解放処理を行う必要があります。
この問題に対処するため、oldstack
の実行コンテキストを、Goランタイムのスケジューラが使用する特別なゴルーチンであるg0
のスタックに切り替える必要がありました。g0
のスタックは、Goランタイムの内部処理やスタック管理のために予約されており、安定した環境を提供します。
また、スタックの確保と解放のロジックを抽象化するために、stackalloc
とstackfree
という関数が導入されました。これらは当初はシンプルなスタブとして実装されていますが、将来的にGoのガベージコレクションと統合された、より洗練されたスタックメモリ管理メカニズムへの移行を見据えたものです。
前提知識の解説
Goランタイムとゴルーチン
Go言語の最大の特徴の一つは、軽量な並行処理の単位である「ゴルーチン(goroutine)」です。ゴルーチンはOSのスレッドよりもはるかに軽量で、数百万個を同時に実行することも可能です。Goランタイムは、これらのゴルーチンをOSスレッドにマッピングし、スケジューリングを行います。
可変スタック(Contiguous Stack)
Goのゴルーチンは、初期スタックサイズが非常に小さく(数KB程度)、関数呼び出しの深さに応じて必要に応じてスタックを動的に拡張します。これは「可変スタック」または「連続スタック」と呼ばれ、スタックオーバーフローを防ぎつつ、メモリ効率を最大化するためのGoの重要な設計です。スタックが拡張される際には、より大きな新しいスタックが確保され、古いスタックの内容が新しいスタックにコピーされます。
g0
ゴルーチン
Goランタイムには、通常のユーザーゴルーチンとは別に、特別な「g0
ゴルーチン」が存在します。g0
は各OSスレッド(M: Machine)に紐付けられており、Goスケジューラやガベージコレクタ、スタック管理など、ランタイムの低レベルな処理を実行するために使用されます。g0
のスタックは固定サイズであり、ユーザーゴルーチンのスタック切り替えなどのクリティカルな操作を安全に行うための安定した基盤を提供します。
newstack
とoldstack
newstack
: ゴルーチンのスタックが不足した際に呼び出されるランタイム関数です。より大きな新しいスタックを確保し、現在のスタックの内容を新しいスタックにコピーし、実行コンテキストを新しいスタックに切り替えます。この処理はg0
のスタック上で行われます。oldstack
:newstack
によってスタックが拡張された後、古いスタックが不要になった際に、その古いスタックを解放するために呼び出されるランタイム関数です。
mal
関数
Goランタイム内部で使用されるメモリ確保関数です。このコミットの時点では、stackalloc
は単にmal
を呼び出してメモリを確保しています。
技術的詳細
このコミットの核心は、Goランタイムのスタック管理における安全性と効率性の向上です。
-
stackalloc
とstackfree
の導入:src/runtime/runtime.h
にvoid* stackalloc(uint32);
とvoid stackfree(void*);
が宣言されました。src/runtime/stack.c
という新しいファイルが作成され、これらの関数のスタブ実装が含まれています。stackalloc
はmal(n)
を呼び出し、単純にメモリを確保します。stackfree
は空の関数であり、何も行いません。
- これは、スタックのメモリ管理を抽象化し、将来的にガベージコレクタと連携させるための準備です。スタックの確保と解放のロジックが独立した関数として定義されることで、後から実装を容易に変更できるようになります。
-
oldstack
のg0
スタック上での実行:src/runtime/proc.c
のoldstack
関数が変更されました。- 最も重要な変更は、
stackfree
の呼び出しが追加されたことです。stackfree((byte*)m->curg->stackguard - 512 - 160);
という行が追加され、これにより古いスタックの解放が試みられます。 - この解放処理を安全に行うため、
oldstack
がg0
のスタック上で実行されるように、制御フローが変更されました。
-
lessstack
関数の導入と制御フローの変更:src/runtime/proc.c
にlessstack()
という新しい関数が追加されました。lessstack
は、まず現在のゴルーチンをm->g0
(現在のMに紐付けられたg0
ゴルーチン)に切り替え、その後setspgoto(m->sched.SP, oldstack, nil);
を呼び出します。setspgoto
は、指定されたスタックポインタ(m->sched.SP
はg0
のスタックポインタ)に切り替え、指定された関数(oldstack
)にジャンプするアセンブリルーチンです。
src/runtime/rt0_amd64.s
のretfromnewstack
(新しいスタックから戻る際のエントリポイント)が、oldstack(SB)
に直接ジャンプする代わりに、lessstack(SB)
にジャンプするように変更されました。- この変更により、スタック拡張後の関数呼び出しから戻る際、まず
lessstack
がg0
のスタックに切り替えてからoldstack
を呼び出すという流れが確立されました。これにより、oldstack
は安全なg0
のスタック上で、古いスタックを解放できるようになります。
-
newstack
でのstackalloc
の使用:src/runtime/proc.c
のnewstack
関数内で、スタックメモリの確保にmal
を直接呼び出す代わりに、新しく導入されたstackalloc
が使用されるようになりました。stk = stackalloc(siz1 + 1024);
これらの変更により、Goランタイムはゴルーチンのスタックをより堅牢かつ安全に管理できるようになり、メモリリークやクラッシュのリスクを低減しました。
コアとなるコードの変更箇所
src/runtime/proc.c
--- a/src/runtime/proc.c
+++ b/src/runtime/proc.c
@@ -567,6 +567,7 @@ oldstack(void)
Stktop *top;
uint32 siz2;
byte *sp;
+ uint64 oldsp, oldpc, oldbase, oldguard;
// printf("oldstack m->cret=%p\n", m->cret);
@@ -581,15 +582,36 @@ oldstack(void)
mcpy(top->oldsp+16, sp, siz2);
}
- // call no more functions after this point - stackguard disagrees with SP
- m->curg->stackbase = top->oldbase;
- m->curg->stackguard = top->oldguard;
- m->morestack.SP = top->oldsp+8;
- m->morestack.PC = (byte*)(*(uint64*)(top->oldsp+8));
-
+ oldsp = (uint64)top->oldsp + 8;
+ oldpc = *(uint64*)(top->oldsp + 8);
+ oldbase = (uint64)top->oldbase;
+ oldguard = (uint64)top->oldguard;
+
+ stackfree((byte*)m->curg->stackguard - 512 - 160);
+
+ m->curg->stackbase = (byte*)oldbase;
+ m->curg->stackguard = (byte*)oldguard;
+ m->morestack.SP = (byte*)oldsp;
+ m->morestack.PC = (byte*)oldpc;
+
+ // These two lines must happen in sequence;
+ // once g has been changed, must switch to g's stack
+ // before calling any non-assembly functions.
+ // TODO(rsc): Perhaps make the new g a parameter
+ // to gogoret and setspgoto, so that g is never
+ // explicitly assigned to without also setting
+ // the stack pointer.
+ g = m->curg;
gogoret(&m->morestack, m->cret);
}
+void
+lessstack(void)
+{
+ g = m->g0;
+ setspgoto(m->sched.SP, oldstack, nil);
+}
+
void
newstack(void)
{
@@ -611,7 +633,7 @@ newstack(void)
if(siz1 < 4096)
siz1 = 4096;
- stk = mal(siz1 + 1024);
+ stk = stackalloc(siz1 + 1024);
stk += 512;
top = (Stktop*)(stk+siz1-sizeof(*top));
src/runtime/rt0_amd64.s
--- a/src/runtime/rt0_amd64.s
+++ b/src/runtime/rt0_amd64.s
@@ -89,10 +89,10 @@ TEXT gosave(SB), 7, $0
* support for morestack
*/
-// return point when leaving new stack. save AX, jmp to oldstack to switch back
+// return point when leaving new stack. save AX, jmp to lessstack to switch back
TEXT retfromnewstack(SB), 7, $0
MOVQ AX, 16(R14) // save AX in m->cret
- MOVQ $oldstack(SB), AX
+ MOVQ $lessstack(SB), AX
JMP AX
// gogo, returning 2nd arg instead of 1
src/runtime/stack.c
(新規ファイル)
// Copyright 2009 The Go Authors. All rights reserved.
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file.
#include "runtime.h"
// Stubs for stack management.
// In a separate file so they can be overridden during testing of gc.
void*
stackalloc(uint32 n)
{
return mal(n);
}
void
stackfree(void*)
{
}
コアとなるコードの解説
src/runtime/proc.c
の変更点
-
oldstack
関数の修正:oldsp
,oldpc
,oldbase
,oldguard
といったスタック関連の情報をuint64
型で一時変数に保存するように変更されました。これにより、スタック解放処理中にこれらの値が変更されることを防ぎます。stackfree((byte*)m->curg->stackguard - 512 - 160);
という行が追加されました。これは、現在のゴルーチン(m->curg
)の古いスタックを解放するための呼び出しです。stackguard
はスタックの境界を示すポインタであり、そこからオフセットを計算してスタックの基底アドレスを特定し、解放しています。- コメントアウトされていた古いスタック切り替えロジックが削除され、新しい
g = m->curg;
とgogoret(&m->morestack, m->cret);
の組み合わせに置き換えられました。これは、スタック切り替えの安全性を高めるためのものです。
-
lessstack
関数の追加:lessstack
は、g = m->g0;
によって現在のゴルーチンをg0
に切り替えます。これは、oldstack
がユーザーゴルーチンのスタックではなく、安定したg0
のスタック上で実行されるようにするための重要なステップです。- その後、
setspgoto(m->sched.SP, oldstack, nil);
を呼び出します。m->sched.SP
はg0
のスタックポインタであり、この呼び出しによって実行コンテキストがg0
のスタックに切り替わり、oldstack
関数が呼び出されます。
-
newstack
関数の修正:stk = mal(siz1 + 1024);
というスタック確保の行が、stk = stackalloc(siz1 + 1024);
に変更されました。これにより、スタックの確保処理がstackalloc
関数に委譲され、将来的なメモリ管理の変更に対応しやすくなりました。
src/runtime/rt0_amd64.s
の変更点
retfromnewstack
の修正:retfromnewstack
は、スタック拡張後の関数呼び出しから戻る際のエントリポイントです。- 以前は
oldstack(SB)
に直接ジャンプしていましたが、このコミットでlessstack(SB)
にジャンプするように変更されました。これにより、lessstack
がg0
のスタックへの切り替えを仲介し、その後にoldstack
が安全に呼び出されるという新しいフローが確立されました。
src/runtime/stack.c
の新規追加
- このファイルは、
stackalloc
とstackfree
の初期実装を提供します。 stackalloc
は、引数で指定されたサイズn
のメモリをmal
関数を使って確保し、そのポインタを返します。stackfree
は、引数を受け取りますが、現時点では何も処理を行いません。これは、スタックの解放ロジックがまだ完全に実装されていないか、ガベージコレクタとの連携が将来的に行われることを示唆しています。
これらの変更は、Goランタイムのスタック管理メカニズムをより堅牢にし、ゴルーチンの動的なスタックサイズ変更を安全かつ効率的に行うための重要な基盤を築きました。
関連リンク
- Go言語の公式リポジトリ: https://github.com/golang/go
- Goランタイムのドキュメント(現在のバージョン): https://go.dev/doc/go1.22 (当時のドキュメントは入手困難なため、現在のものを参照)
参考にした情報源リンク
- Goのスタック管理に関する議論やドキュメント(当時のものは公開されていない可能性が高いですが、関連する概念を理解するために一般的なGoランタイムの資料を参照しました)
- Goのソースコード(特に
src/runtime
ディレクトリ) - Goのガベージコレクションとスタックに関するブログ記事や論文(一般的な概念理解のため)
- Goのスタック管理に関する一般的な情報: https://go.dev/doc/articles/go_programming_language_faq#goroutines
- Goのランタイムに関するより詳細な情報(現在のバージョンに基づくが、基本的な概念は共通): https://go.dev/src/runtime/README.md
- Goのスタック拡張に関するブログ記事(例: "Go's Execution Tracer" by Dmitry Vyukov, "Go: The Good, The Bad, and The Ugly" by Dave Cheneyなど、当時の情報に直接アクセスできないため、関連する概念を説明している記事を参照)
- Goの
g0
ゴルーチンに関する情報: https://go.dev/src/runtime/proc.go (現在のソースコードからg0
の役割を推測)
(注:2008年当時のGo言語のドキュメントや詳細な設計資料は一般に公開されていないため、現在のGoランタイムの設計思想や関連する概念から当時の変更の意図を推測し、解説を構成しています。)