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

[インデックス 1275] ファイルの概要

このコミットは、Go言語のランタイムにおけるスタック管理の初期段階の改善に関するものです。特に、ゴルーチンのスタック切り替え時に、古いスタックを安全に解放できるようにするための基盤を構築しています。stackallocstackfreeというスタブ関数が導入され、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と同様に、oldstackg0のスタック上で実行するようにしました。これにより、oldstackが古いスタックを解放できるようになります。

変更の背景

Go言語の初期開発段階において、ゴルーチンのスタック管理は非常に重要な課題でした。Goのゴルーチンは、必要に応じてスタックサイズを動的に増減させる「可変スタック」を採用しています。スタックが不足した際には新しい大きなスタックに切り替える(newstack)、そして不要になった古いスタックを解放する(oldstack)という処理が必要です。

このコミット以前は、oldstackルーチンが、解放しようとしているまさにその古いスタック上で実行されている可能性がありました。これは、自身の足元を掘るようなもので、スタックの解放処理中にそのスタックが使われていると、メモリ破壊やクラッシュの原因となり得ます。安全にスタックを解放するためには、解放対象のスタックとは別の、安定したスタック上で解放処理を行う必要があります。

この問題に対処するため、oldstackの実行コンテキストを、Goランタイムのスケジューラが使用する特別なゴルーチンであるg0のスタックに切り替える必要がありました。g0のスタックは、Goランタイムの内部処理やスタック管理のために予約されており、安定した環境を提供します。

また、スタックの確保と解放のロジックを抽象化するために、stackallocstackfreeという関数が導入されました。これらは当初はシンプルなスタブとして実装されていますが、将来的にGoのガベージコレクションと統合された、より洗練されたスタックメモリ管理メカニズムへの移行を見据えたものです。

前提知識の解説

Goランタイムとゴルーチン

Go言語の最大の特徴の一つは、軽量な並行処理の単位である「ゴルーチン(goroutine)」です。ゴルーチンはOSのスレッドよりもはるかに軽量で、数百万個を同時に実行することも可能です。Goランタイムは、これらのゴルーチンをOSスレッドにマッピングし、スケジューリングを行います。

可変スタック(Contiguous Stack)

Goのゴルーチンは、初期スタックサイズが非常に小さく(数KB程度)、関数呼び出しの深さに応じて必要に応じてスタックを動的に拡張します。これは「可変スタック」または「連続スタック」と呼ばれ、スタックオーバーフローを防ぎつつ、メモリ効率を最大化するためのGoの重要な設計です。スタックが拡張される際には、より大きな新しいスタックが確保され、古いスタックの内容が新しいスタックにコピーされます。

g0ゴルーチン

Goランタイムには、通常のユーザーゴルーチンとは別に、特別な「g0ゴルーチン」が存在します。g0は各OSスレッド(M: Machine)に紐付けられており、Goスケジューラやガベージコレクタ、スタック管理など、ランタイムの低レベルな処理を実行するために使用されます。g0のスタックは固定サイズであり、ユーザーゴルーチンのスタック切り替えなどのクリティカルな操作を安全に行うための安定した基盤を提供します。

newstackoldstack

  • newstack: ゴルーチンのスタックが不足した際に呼び出されるランタイム関数です。より大きな新しいスタックを確保し、現在のスタックの内容を新しいスタックにコピーし、実行コンテキストを新しいスタックに切り替えます。この処理はg0のスタック上で行われます。
  • oldstack: newstackによってスタックが拡張された後、古いスタックが不要になった際に、その古いスタックを解放するために呼び出されるランタイム関数です。

mal関数

Goランタイム内部で使用されるメモリ確保関数です。このコミットの時点では、stackallocは単にmalを呼び出してメモリを確保しています。

技術的詳細

このコミットの核心は、Goランタイムのスタック管理における安全性と効率性の向上です。

  1. stackallocstackfreeの導入:

    • src/runtime/runtime.hvoid* stackalloc(uint32);void stackfree(void*);が宣言されました。
    • src/runtime/stack.cという新しいファイルが作成され、これらの関数のスタブ実装が含まれています。
      • stackallocmal(n)を呼び出し、単純にメモリを確保します。
      • stackfreeは空の関数であり、何も行いません。
    • これは、スタックのメモリ管理を抽象化し、将来的にガベージコレクタと連携させるための準備です。スタックの確保と解放のロジックが独立した関数として定義されることで、後から実装を容易に変更できるようになります。
  2. oldstackg0スタック上での実行:

    • src/runtime/proc.coldstack関数が変更されました。
    • 最も重要な変更は、stackfreeの呼び出しが追加されたことです。stackfree((byte*)m->curg->stackguard - 512 - 160);という行が追加され、これにより古いスタックの解放が試みられます。
    • この解放処理を安全に行うため、oldstackg0のスタック上で実行されるように、制御フローが変更されました。
  3. lessstack関数の導入と制御フローの変更:

    • src/runtime/proc.clessstack()という新しい関数が追加されました。
    • lessstackは、まず現在のゴルーチンをm->g0(現在のMに紐付けられたg0ゴルーチン)に切り替え、その後setspgoto(m->sched.SP, oldstack, nil);を呼び出します。
      • setspgotoは、指定されたスタックポインタ(m->sched.SPg0のスタックポインタ)に切り替え、指定された関数(oldstack)にジャンプするアセンブリルーチンです。
    • src/runtime/rt0_amd64.sretfromnewstack(新しいスタックから戻る際のエントリポイント)が、oldstack(SB)に直接ジャンプする代わりに、lessstack(SB)にジャンプするように変更されました。
    • この変更により、スタック拡張後の関数呼び出しから戻る際、まずlessstackg0のスタックに切り替えてからoldstackを呼び出すという流れが確立されました。これにより、oldstackは安全なg0のスタック上で、古いスタックを解放できるようになります。
  4. newstackでのstackallocの使用:

    • src/runtime/proc.cnewstack関数内で、スタックメモリの確保に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.SPg0のスタックポインタであり、この呼び出しによって実行コンテキストがg0のスタックに切り替わり、oldstack関数が呼び出されます。
  • newstack関数の修正:

    • stk = mal(siz1 + 1024);というスタック確保の行が、stk = stackalloc(siz1 + 1024);に変更されました。これにより、スタックの確保処理がstackalloc関数に委譲され、将来的なメモリ管理の変更に対応しやすくなりました。

src/runtime/rt0_amd64.sの変更点

  • retfromnewstackの修正:
    • retfromnewstackは、スタック拡張後の関数呼び出しから戻る際のエントリポイントです。
    • 以前はoldstack(SB)に直接ジャンプしていましたが、このコミットでlessstack(SB)にジャンプするように変更されました。これにより、lessstackg0のスタックへの切り替えを仲介し、その後にoldstackが安全に呼び出されるという新しいフローが確立されました。

src/runtime/stack.cの新規追加

  • このファイルは、stackallocstackfreeの初期実装を提供します。
  • stackallocは、引数で指定されたサイズnのメモリをmal関数を使って確保し、そのポインタを返します。
  • stackfreeは、引数を受け取りますが、現時点では何も処理を行いません。これは、スタックの解放ロジックがまだ完全に実装されていないか、ガベージコレクタとの連携が将来的に行われることを示唆しています。

これらの変更は、Goランタイムのスタック管理メカニズムをより堅牢にし、ゴルーチンの動的なスタックサイズ変更を安全かつ効率的に行うための重要な基盤を築きました。

関連リンク

参考にした情報源リンク

  • 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ランタイムの設計思想や関連する概念から当時の変更の意図を推測し、解説を構成しています。)