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

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

このコミットは、Goランタイムにおけるパニック処理の堅牢性を向上させることを目的としています。特に、メモリ割り当てシステム(malloc)がまだ完全に初期化されていない、非常に早い段階でパニックが発生した場合のデバッグを容易にするための変更が加えられています。これにより、起動時の問題がより明確に診断できるようになります。

コミット

commit f84d5dd4753890f32947e67c8a16d8ca22086551
Author: Russ Cox <rsc@golang.org>
Date:   Thu Mar 14 10:10:12 2013 -0400

    runtime: make panic possible before malloc is ready
    
    Otherwise startup problems can be difficult to debug.
    
    R=golang-dev, r
    CC=golang-dev
    https://golang.org/cl/7522046

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

https://github.com/golang/go/commit/f84d5dd4753890f32947e67c8a16d8ca22086551

元コミット内容

Goランタイムにおいて、malloc(メモリ割り当て)が準備できる前にパニックが発生した場合でも、適切にパニック処理が行われるようにする。そうしないと、起動時の問題のデバッグが困難になる。

変更の背景

Goプログラムの起動プロセスは複雑であり、ランタイムの初期化フェーズでは、メモリ管理システム(ヒープアロケータ、特にmalloc)がまだ完全にセットアップされていない可能性があります。この非常に早い段階で、例えばメモリ不足や不正なポインタアクセスなどの致命的なエラーが発生した場合、従来のパニックハンドラ自体がメモリ割り当てを試みることがありました。その結果、パニックハンドラがさらに別のエラーを引き起こし、デバッグに役立つ情報(スタックトレースなど)を出力する前にプログラムがクラッシュしてしまうという問題がありました。

このコミットは、このような「パニック中のパニック」を防ぎ、起動時のデバッグを改善するために導入されました。mallocが利用可能になる前のパニックでも、少なくともエラーメッセージを出力し、デバッグの足がかりを提供できるようにすることが目的です。

前提知識の解説

このコミットを理解するためには、以下のGoランタイムの概念に関する知識が必要です。

  1. Goランタイム (Go Runtime): Goプログラムは、OS上で直接実行されるのではなく、Goランタイムと呼ばれる軽量な実行環境上で動作します。ランタイムは、ガベージコレクション、スケジューラ(ゴルーチンの管理)、メモリ管理、パニック処理など、Go言語のコア機能を提供します。
  2. mallocとメモリ管理: mallocはC言語の標準ライブラリ関数で、動的にメモリを割り当てるために使用されます。Goランタイムも内部的にはC言語で書かれた部分が多く、独自のメモリ管理システム(ヒープアロケータ)を持っています。これは、Goのガベージコレクタと連携して動作し、プログラムが必要とするメモリを効率的に管理します。
    • mheap: Goランタイムのグローバルなヒープ構造体で、大きなメモリチャンクの管理や、アロケータのキャッシュ(mcache)へのメモリ提供など、ヒープ全体の管理を行います。
    • mcache: 各M(OSスレッド)に紐付けられたローカルなメモリキャッシュです。小さなオブジェクトの割り当てを高速化するために使用されます。mcachemheapからメモリを受け取ります。
    • FixAlloc: Goランタイム内部で使われる、固定サイズのオブジェクトを効率的に割り当てるためのアロケータです。例えば、mcacheのようなランタイム内部のデータ構造の割り当てに使われます。
  3. パニック (Panic): Goにおけるパニックは、プログラムの回復不可能なエラーを示すメカニズムです。パニックが発生すると、通常の実行フローは中断され、遅延関数(defer)が実行された後、スタックがアンワインドされます。最終的に、パニックメッセージとスタックトレースが出力され、プログラムは終了します(recoverされない限り)。
  4. 起動シーケンス: Goプログラムが起動する際、ランタイムは様々な初期化ステップを実行します。これには、メモリ管理システムのセットアップ、スケジューラの初期化、メインゴルーチンの起動などが含まれます。これらのステップは特定の順序で実行され、一部のコンポーネントは他のコンポーネントが完全に初期化される前に必要とされることがあります。

技術的詳細

このコミットは、主に以下の2つのファイルに修正を加えています。

  1. src/pkg/runtime/mfixalloc.c:

    • runtime·FixAlloc_Alloc関数は、FixAlloc構造体から固定サイズのメモリブロックを割り当てるための関数です。
    • この関数に、f->size == 0という条件チェックが追加されました。FixAlloc構造体のsizeフィールドは、割り当てるブロックのサイズを示します。この値が0であるということは、FixAllocが適切に初期化されていない(FixAlloc_Initが呼び出されていない)ことを意味します。
    • もしf->sizeが0であれば、runtime·printfでエラーメッセージ「runtime: use of FixAlloc_Alloc before FixAlloc_Init」を出力し、runtime·throwを呼び出してランタイム内部エラーを発生させます。
    • この変更は、FixAllocアロケータが不正な状態で使用されることを早期に検出し、デバッグを容易にするための防御的なプログラミングです。
  2. src/pkg/runtime/panic.c:

    • runtime·startpanic関数は、パニック処理の開始点となる関数です。
    • この関数に、mallocヒープが初期化されているかどうかをチェックする新しい条件が追加されました。具体的には、runtime·mheap == 0 || runtime·mheap->cachealloc.size == 0という条件です。
      • runtime·mheap == 0: グローバルなヒープ構造体mheapがまだ割り当てられていない(ポインタがNULL)状態。
      • runtime·mheap->cachealloc.size == 0: mheap内のcacheallocmcacheなどの内部構造体を割り当てるためのFixAllocインスタンス)がまだ初期化されていない状態。
    • これらの条件のいずれかが真である場合(コメントでは「very early」と表現されている)、それはmallocヒープがまだ完全に準備できていない非常に早い段階でパニックが発生したことを意味します。
    • この場合、以下の処理が行われます。
      • runtime·printfでエラーメッセージ「runtime: panic before malloc heap initialized」を出力します。
      • m->mallocing = 1を設定します。これは、現在のM(OSスレッド)がパニック処理中であり、これ以上mallocを試みてはならないことをランタイムの他の部分に伝えるフラグです。これにより、パニック処理中にさらにメモリ割り当てを試みて、デバッグを妨げる二次的なクラッシュが発生するのを防ぎます。
    • 既存のelse if(m->mcache == nil)の条件は、シグナルハンドラやthrowから呼び出された場合など、mcacheが利用できない状況を処理するためのもので、これは引き続き維持されます。
    • また、このファイルに#include "malloc.h"が追加され、malloc関連の定義が利用可能になりました。

これらの変更により、Goランタイムは、メモリ管理システムが完全に機能する前の非常に早い段階で発生するパニックに対しても、より適切に対応できるようになりました。これにより、デバッグ時に意味のあるエラーメッセージとスタックトレースが得られる可能性が高まります。

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

src/pkg/runtime/mfixalloc.c

--- a/src/pkg/runtime/mfixalloc.c
+++ b/src/pkg/runtime/mfixalloc.c
@@ -30,6 +30,11 @@ void*
 runtime·FixAlloc_Alloc(FixAlloc *f)
 {
 	void *v;
+	
+	if(f->size == 0) {
+		runtime·printf("runtime: use of FixAlloc_Alloc before FixAlloc_Init\\n");
+		runtime·throw("runtime: internal error");
+	}
 
 	if(f->list) {
 		v = f->list;

src/pkg/runtime/panic.c

--- a/src/pkg/runtime/panic.c
+++ b/src/pkg/runtime/panic.c
@@ -5,6 +5,7 @@
 #include "runtime.h"
 #include "arch_GOARCH.h"
 #include "stack.h"
+#include "malloc.h"
 
 // Code related to defer, panic and recover.
 
@@ -383,7 +384,10 @@ nomatch:
 void
 runtime·startpanic(void)
 {
-\tif(m->mcache == nil)  // can happen if called from signal handler or throw
+\tif(runtime·mheap == 0 || runtime·mheap->cachealloc.size == 0) { // very early
+\t\truntime·printf("runtime: panic before malloc heap initialized\\n");
+\t\tm->mallocing = 1; // tell rest of panic not to try to malloc
+\t} else if(m->mcache == nil) // can happen if called from signal handler or throw
 \t\tm->mcache = runtime·allocmcache();
 \tif(m->dying) {
 \t\truntime·printf("panic during panic\\n");

コアとなるコードの解説

src/pkg/runtime/mfixalloc.c の変更

runtime·FixAlloc_Alloc関数は、Goランタイム内部で頻繁に利用される固定サイズアロケータの割り当て関数です。追加されたif(f->size == 0)チェックは、このアロケータが使用される前に適切に初期化されていることを保証します。FixAlloc構造体は、FixAlloc_Init関数によって初期化され、その際にsizeフィールドに割り当てるオブジェクトのサイズが設定されます。もしsizeが0のままFixAlloc_Allocが呼び出された場合、それはプログラミングエラーであり、ランタイムの整合性が損なわれている可能性が高いです。このチェックにより、そのような不正な状態を早期に検出し、明確なエラーメッセージとともにプログラムを停止させることで、デバッグの労力を削減します。

src/pkg/runtime/panic.c の変更

runtime·startpanic関数は、Goプログラムでパニックが発生した際に最初に呼び出される重要な関数です。この関数の変更は、Goランタイムの起動シーケンスにおけるメモリ管理の初期化段階に焦点を当てています。

  • #include "malloc.h": runtime·mheapm->mallocingといったメモリ管理関連のシンボルを使用するために、このヘッダファイルが追加されました。
  • if(runtime·mheap == 0 || runtime·mheap->cachealloc.size == 0): この新しい条件分岐が、このコミットの核心です。
    • runtime·mheap == 0: これは、Goランタイムのグローバルなヒープ構造体であるmheapがまだ割り当てられていない状態を指します。mheapはランタイムのメモリ管理の根幹をなすものであり、これが存在しないということは、メモリシステムが非常に初期の段階にあることを意味します。
    • runtime·mheap->cachealloc.size == 0: mheap自体は存在しても、その内部で使われるcacheallocmcacheなどのランタイム内部構造体を割り当てるためのFixAllocインスタンス)がまだ初期化されていない状態を指します。これもまた、メモリ管理システムが完全に機能する準備ができていないことを示します。
    • これらの条件のいずれかが真である場合、runtime·printfで「runtime: panic before malloc heap initialized」というメッセージが出力されます。これは、デバッグ時に非常に役立つ情報であり、パニックがメモリ初期化の非常に早い段階で発生したことを明確に示します。
    • m->mallocing = 1: 最も重要な変更の一つです。mは現在のOSスレッド(M)を表す構造体です。mallocingフラグを1に設定することで、このスレッドが現在パニック処理中であり、これ以上メモリ割り当てを試みてはならないことをランタイムに伝えます。これにより、パニック処理中にmallocが呼び出され、それが失敗してさらなるクラッシュを引き起こすという悪循環を防ぎます。パニック処理自体がメモリを必要とすることがあるため、このフラグは、メモリが利用できない状況下でのパニック処理の安全性を確保するために不可欠です。
  • else if(m->mcache == nil): 既存の条件は、シグナルハンドラやthrow(Goの内部的な例外のようなもの)からパニックが呼び出された場合に、現在のスレッドのmcachenilである可能性がある状況を処理します。この場合、runtime·allocmcache()を呼び出して新しいmcacheを割り当てます。この部分は、新しい早期パニック処理のロジックの後に実行されるように変更されました。

これらの変更により、Goランタイムは、メモリ管理システムが完全に機能する前の非常に早い段階で発生するパニックに対しても、より適切に対応できるようになりました。これにより、デバッグ時に意味のあるエラーメッセージとスタックトレースが得られる可能性が高まります。

関連リンク

参考にした情報源リンク

  • Goのコミット履歴: https://github.com/golang/go/commits/master
  • Goのコードレビューシステム (Gerrit): https://go-review.googlesource.com/ (コミットメッセージに記載されているhttps://golang.org/cl/7522046はこのGerritの変更リストへのリンクです)
  • Goのランタイムに関する技術記事や書籍(一般的な情報源)
  • C言語のmallocに関するドキュメント(一般的な情報源)
  • Goのパニックとリカバリに関する公式ドキュメントやチュートリアル(一般的な情報源)