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

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

このコミットは、Goランタイムにおけるゴルーチンのスタックサイズに上限を設ける変更を導入しています。これにより、無限再帰などによってスタックが際限なく成長し、システムメモリを枯渇させることを防ぐことが目的です。具体的には、64ビットシステムでは1GB、32ビットシステムでは250MBのスタックサイズ制限が設定されます。

コミット

commit 757e0de89f80e89626cc8b7d6e670c0e5ea7f192
Author: Russ Cox <rsc@golang.org>
Date:   Thu Aug 15 22:34:06 2013 -0400

    runtime: impose stack size limit
    
    The goal is to stop only those programs that would keep
    going and run the machine out of memory, but before they do that.
    1 GB on 64-bit, 250 MB on 32-bit.
    That seems implausibly large, and it can be adjusted.
    
    Fixes #2556.
    Fixes #4494.
    Fixes #5173.
    
    R=khr, r, dvyukov
    CC=golang-dev
    https://golang.org/cl/12541052

GitHub上でのコミットページへのリンク

https://github.com/golang/go/commit/757e0de89f80e89626cc8b7d6e670c0e5ea7f192

元コミット内容

Goランタイムにおいて、スタックサイズに制限を課す。 目的は、無限にスタックを使い続け、最終的にマシンメモリを使い果たすプログラムを、その前に停止させることである。 64ビットシステムでは1GB、32ビットシステムでは250MBの制限を設定する。 この値は一見すると非常に大きいように思えるが、調整可能である。

以下のIssueを修正する:

  • #2556
  • #4494
  • #5173

変更の背景

Goのゴルーチンは、非常に軽量なスレッドであり、そのスタックは必要に応じて動的に伸縮します。しかし、この動的なスタック成長メカニズムには、無限再帰のようなプログラミングエラーが発生した場合に、スタックが際限なく成長し続け、最終的にシステム全体のメモリを枯渇させてしまうという潜在的な問題がありました。

このコミットは、この問題に対処するために導入されました。具体的な背景としては、以下のIssueが挙げられます。

  • Issue #2556: runtime: stack overflow should be fatal このIssueでは、スタックオーバーフローが発生した場合に、プログラムがクラッシュするのではなく、より明確なエラーメッセージとともに終了すべきであるという議論がなされていました。従来のGoランタイムでは、スタックオーバーフローが直接的な致命的エラーとして扱われず、システム全体の不安定化につながる可能性がありました。

  • Issue #4494: runtime: stack overflow should be detected and reported このIssueは、スタックオーバーフローを検出し、ユーザーに報告するメカニズムの必要性を提起しています。単にクラッシュするだけでなく、何が問題であったかを開発者が理解できるように、適切なエラーメッセージや診断情報を提供することが求められていました。

  • Issue #5173: runtime: infinite recursion should not exhaust system memory このIssueは、無限再帰がシステムメモリを枯渇させるという直接的な問題提起です。Goの動的なスタック成長は、通常は非常に効率的ですが、無限再帰のような異常なケースでは、メモリを無制限に消費し続ける可能性があります。このコミットは、この問題に対する直接的な解決策を提供します。

これらのIssueは、Goランタイムの堅牢性と安定性を向上させるために、スタックの管理方法を見直し、無限のスタック成長を防ぐためのメカニズムを導入する必要があることを示していました。このコミットは、これらの懸念に対処し、Goプログラムがより予測可能で安全に動作するようにするための重要なステップです。

前提知識の解説

このコミットの変更内容を理解するためには、以下のGoランタイムの概念とC言語の基本的な知識が必要です。

1. ゴルーチン (Goroutine) とスタック

  • ゴルーチン: Go言語における軽量な実行単位です。OSのスレッドよりもはるかに軽量で、数百万個のゴルーチンを同時に実行することも可能です。ゴルーチンはGoランタイムによってスケジューリングされ、必要に応じてOSスレッドにマッピングされます。
  • スタック: 各ゴルーチンは独自のスタックを持っています。スタックは、関数呼び出しの際にローカル変数、関数引数、戻りアドレスなどを格納するために使用されるメモリ領域です。Goのスタックは、必要に応じて動的にサイズが変更されます(スタックの伸縮)。これは、初期スタックサイズを小さく保ち、必要に応じて拡張することで、メモリ効率を高めるためのGoの重要な特徴です。
  • スタックオーバーフロー: スタックが利用可能なメモリ領域を超えて成長しようとするときに発生するエラーです。通常、無限再帰のようなプログラミングエラーによって引き起こされます。

2. Goランタイムのメモリ管理

Goランタイムは、ガベージコレクション(GC)を含む独自のメモリ管理システムを持っています。スタックもこのメモリ管理の一部であり、必要に応じて確保・解放されます。スタックの動的な伸縮は、Goランタイムがメモリを効率的に利用するための重要なメカニズムです。

3. C言語の基本的な概念

Goランタイムの多くの部分はC言語で実装されています(特に初期のバージョンでは)。このコミットで変更されているファイルも、.c.hといったC言語の拡張子を持つものが含まれています。

  • uintptr: ポインタを保持できる符号なし整数型です。メモリアドレスを扱う際に使用されます。
  • sizeof(void*): void*型(汎用ポインタ)のサイズをバイト単位で返します。これは、システムが32ビットか64ビットかを判断するためによく使用されます。64ビットシステムでは通常8バイト、32ビットシステムでは4バイトです。
  • runtime·throw: Goランタイム内部で致命的なエラーが発生した場合に呼び出される関数です。プログラムをクラッシュさせます。
  • runtime·printf: Goランタイム内部で使用されるprintfライクな関数です。デバッグ情報やエラーメッセージを出力するために使用されます。
  • FLUSH(&out): C言語のマクロまたは関数で、out変数の値をメモリに確実に書き込むことを保証します。コンパイラの最適化によって値がレジスタに保持されたままになるのを防ぐために使用されることがあります。

4. src/pkg/runtime/debug パッケージ

runtime/debugパッケージは、Goプログラムのデバッグ情報やランタイムの動作に関する情報を提供するパッケージです。このコミットでは、このパッケージにスタックサイズ制限を設定するための新しい関数が追加されています。

これらの前提知識を理解することで、コミットがGoランタイムのどの部分に影響を与え、どのようなメカニズムでスタックサイズ制限が実装されているのかを深く理解することができます。

技術的詳細

このコミットは、Goランタイムにゴルーチンのスタックサイズ制限を導入し、無限再帰などによるメモリ枯渇を防ぐためのものです。主要な変更点は以下の通りです。

  1. runtime·maxstacksize 変数の導入と初期化:

    • src/pkg/runtime/runtime.huintptr runtime·maxstacksize; が追加され、グローバルなスタックサイズ制限を保持する変数が宣言されました。
    • src/pkg/runtime/proc.cruntime·main 関数内で、この runtime·maxstacksize がシステムのビット数に応じて初期化されます。
      • 64ビットシステム (sizeof(void*) == 8) の場合、1,000,000,000 バイト (1GB) に設定されます。
      • 32ビットシステムの場合、250,000,000 バイト (250MB) に設定されます。
      • この初期値は、src/pkg/runtime/stack.c1<<20 (1MB) として仮に初期化されていますが、runtime·main で上書きされます。
  2. スタック成長時のサイズチェック:

    • src/pkg/runtime/stack.cruntime·newstack 関数(新しいスタックセグメントが必要になったときに呼び出される)内で、スタックの成長時に現在のゴルーチンのスタックサイズが runtime·maxstacksize を超えていないかチェックするロジックが追加されました。
    • gp->stacksize += framesize; で新しいフレームサイズが現在のスタックサイズに追加された後、if(gp->stacksize > runtime·maxstacksize) の条件でチェックが行われます。
    • もし制限を超えた場合、runtime·printf で「runtime: goroutine stack exceeds %D-byte limit」というメッセージが出力され、runtime·throw("stack overflow") が呼び出されてプログラムが致命的なエラーで終了します。
  3. runtime/debug.SetMaxStack 関数の追加:

    • src/pkg/runtime/debug/garbage.goSetMaxStack 関数が追加されました。これはGo言語から呼び出すことができるAPIで、プログラム実行中にスタックサイズ制限を動的に変更することを可能にします。
    • このGo言語の関数は、内部的に src/pkg/runtime/stack.c に追加されたC言語の関数 runtime∕debug·setMaxStack を呼び出します。
    • runtime∕debug·setMaxStack は、現在の runtime·maxstacksize の値を返した後、新しい制限値で runtime·maxstacksize を更新します。
  4. スタックサイズ情報の更新:

    • src/pkg/runtime/runtime.hG 構造体(ゴルーチンを表す構造体)に uintptr stacksize; フィールドが追加されました。これにより、各ゴルーチンが現在使用しているスタックの合計サイズを追跡できるようになります。
    • src/pkg/runtime/proc.cruntime·malg 関数(新しいゴルーチンを割り当てる関数)で、新しく割り当てられるゴルーチンの g->stacksize が初期化されます。
    • src/pkg/runtime/panic.csrc/pkg/runtime/stack.cruntime·unwindstack および runtime·oldstack 関数内で、スタックセグメントの解放時に gp->stacksize から解放されたメモリサイズが減算されるようになりました。これにより、gp->stacksize が常に正確なスタック使用量を反映するように保たれます。
  5. テストケースの追加:

    • src/pkg/runtime/crash_test.goTestStackOverflow という新しいテストケースが追加されました。このテストは、debug.SetMaxStack を使用して意図的にスタックサイズ制限を小さく設定し、無限再帰を発生させることで、スタックオーバーフローが正しく検出され、プログラムが期待通りにクラッシュすることを確認します。

これらの変更により、Goランタイムはスタックの過度な成長を検出し、システムリソースの枯渇を防ぐことができるようになりました。SetMaxStack 関数は、デバッグや特定のユースケースにおいて、この制限を調整する柔軟性を提供します。

コアとなるコードの変更箇所

このコミットにおけるコアとなるコードの変更箇所は以下のファイルに集中しています。

  1. src/pkg/runtime/runtime.h:

    • G 構造体に stacksize フィールドが追加され、各ゴルーチンのスタックの合計サイズを追跡できるようになりました。
    • runtime·maxstacksize グローバル変数が宣言されました。
  2. src/pkg/runtime/proc.c:

    • runtime·main 関数内で、runtime·maxstacksize がシステムのビット数(32bit/64bit)に応じて初期化されるようになりました。
    • runtime·malg 関数で、新しく割り当てられるゴルーチンの g->stacksize が初期化されるようになりました。
  3. src/pkg/runtime/stack.c:

    • runtime·newstack 関数に、スタック成長時に gp->stacksizeruntime·maxstacksize を超えていないかチェックするロジックが追加されました。制限を超えた場合は runtime·throw("stack overflow") が呼び出されます。
    • runtime∕debug·setMaxStack というC言語関数が追加され、Goの debug.SetMaxStack から呼び出されることで、runtime·maxstacksize を動的に変更できるようになりました。
    • runtime·oldstack 関数で、スタックセグメントの解放時に gp->stacksize が更新されるようになりました。
  4. src/pkg/runtime/panic.c:

    • runtime·unwindstack 関数で、スタックセグメントの解放時に gp->stacksize が更新されるようになりました。
  5. src/pkg/runtime/debug/garbage.go:

    • SetMaxStack というGo言語の関数が追加されました。これは、ユーザーがGoプログラムからスタックサイズ制限を設定するためのAPIです。
  6. src/pkg/runtime/crash_test.go:

    • TestStackOverflow という新しいテストケースが追加され、スタックオーバーフローの検出と処理が正しく行われることを検証します。

これらのファイルが連携して、Goランタイムにおけるスタックサイズ制限の導入と管理を実現しています。

コアとなるコードの解説

src/pkg/runtime/proc.cruntime·main 関数における runtime·maxstacksize の初期化

// src/pkg/runtime/proc.c
void
runtime·main(void)
{
 	Defer d;
 	
 	// Max stack size is 1 GB on 64-bit, 250 MB on 32-bit.
 	// Using decimal instead of binary GB and MB because
 	// they look nicer in the stack overflow failure message.
 	if(sizeof(void*) == 8)
 		runtime·maxstacksize = 1000000000;
 	else
 		runtime·maxstacksize = 250000000;

 	newm(sysmon, nil);
 // ... (後略)
}

このコードは、Goランタイムのメインエントリポイントである runtime·main 関数内で、ゴルーチンの最大スタックサイズ runtime·maxstacksize を初期化しています。sizeof(void*) を使用してシステムのビット数(64ビットか32ビットか)を判別し、それに応じて異なる制限値を設定しています。64ビットシステムでは1GB、32ビットシステムでは250MBという、当時としては非常に大きな値が設定されています。これは、通常のプログラムでは到達しにくいが、無限再帰のような異常なケースでメモリ枯渇を防ぐための安全弁として機能します。

src/pkg/runtime/stack.cruntime·newstack 関数におけるスタックサイズチェック

// src/pkg/runtime/stack.c
void
runtime·newstack(void)
{
    // ... (前略)
 	if(framesize < StackMin)
 		framesize = StackMin;
 	framesize += StackSystem;
 	gp->stacksize += framesize; // 現在のスタックサイズに新しいフレームサイズを加算
 	if(gp->stacksize > runtime·maxstacksize) { // 最大スタックサイズを超えたかチェック
 		runtime·printf("runtime: goroutine stack exceeds %D-byte limit\n", (uint64)runtime·maxstacksize);
 		runtime·throw("stack overflow"); // 超えた場合は致命的エラー
 	}
 	stk = runtime·stackalloc(framesize);
    // ... (後略)
}

runtime·newstack 関数は、ゴルーチンのスタックが不足し、新しいスタックセグメントが必要になったときに呼び出されます。この変更により、新しいフレームを割り当てる前に、現在のゴルーチン (gp) の合計スタックサイズ (gp->stacksize) に新しいフレームサイズ (framesize) を加算し、それがグローバルな最大スタックサイズ (runtime·maxstacksize) を超えていないかチェックするようになりました。もし超えていた場合、runtime·printf でエラーメッセージを出力し、runtime·throw("stack overflow") を呼び出してプログラムを強制終了させます。これにより、無限再帰などによるスタックの無制限な成長とそれに伴うシステムメモリの枯渇を防ぎます。

src/pkg/runtime/debug/garbage.goSetMaxStack 関数

// src/pkg/runtime/debug/garbage.go
package debug

// ... (前略)

// SetMaxStack sets the maximum amount of memory that
// can be used by a single goroutine stack.
// If any goroutine exceeds this limit while growing its stack,
// the program crashes.
// SetMaxStack returns the previous setting.
// The initial setting is 1 GB on 64-bit systems, 250 MB on 32-bit systems.
//
// SetMaxStack is useful mainly for limiting the damage done by
// goroutines that enter an infinite recursion. It only limits future
// stack growth.
func SetMaxStack(bytes int) int {
	return setMaxStack(bytes)
}

//go:linkname setMaxStack runtime.setMaxStack
func setMaxStack(int) int

このGo言語の関数は、runtime/debug パッケージの一部として公開され、Goプログラムからスタックサイズ制限を動的に設定するためのAPIを提供します。//go:linkname setMaxStack runtime.setMaxStack ディレクティブは、Goコンパイラに対して、この setMaxStack 関数が runtime パッケージ内の同名のC言語関数(またはGoアセンブリ関数)にリンクされるべきであることを指示しています。これにより、Goプログラムからランタイムの内部設定を変更することが可能になります。この関数は、主にデバッグやテスト、あるいは特定のパフォーマンス要件を持つアプリケーションで、デフォルトのスタックサイズ制限を調整するために使用されます。

src/pkg/runtime/stack.cruntime∕debug·setMaxStack 関数

// src/pkg/runtime/stack.c
void
runtime∕debug·setMaxStack(intgo in, intgo out)
{
	out = runtime·maxstacksize; // 現在の値を out に設定
	runtime·maxstacksize = in; // 新しい値を runtime·maxstacksize に設定
	FLUSH(&out); // out の値を確実にメモリに書き込む
}

これは、Goの debug.SetMaxStack から呼び出されるC言語の関数です。in 引数として新しいスタックサイズ制限を受け取り、runtime·maxstacksize グローバル変数を更新します。また、変更前の runtime·maxstacksize の値を out 引数(Go側では戻り値として受け取られる)に設定し、FLUSH(&out) でその値が確実にメモリに書き込まれるようにします。これにより、Goプログラムはスタックサイズ制限を動的に変更し、その変更前の値を取得することができます。

これらのコード変更は、Goランタイムのスタック管理メカニズムに根本的な変更を加え、プログラムの安定性とリソース管理を向上させています。

関連リンク

参考にした情報源リンク