[インデックス 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ランタイムの概念に関する知識が必要です。
- Goランタイム (Go Runtime): Goプログラムは、OS上で直接実行されるのではなく、Goランタイムと呼ばれる軽量な実行環境上で動作します。ランタイムは、ガベージコレクション、スケジューラ(ゴルーチンの管理)、メモリ管理、パニック処理など、Go言語のコア機能を提供します。
malloc
とメモリ管理:malloc
はC言語の標準ライブラリ関数で、動的にメモリを割り当てるために使用されます。Goランタイムも内部的にはC言語で書かれた部分が多く、独自のメモリ管理システム(ヒープアロケータ)を持っています。これは、Goのガベージコレクタと連携して動作し、プログラムが必要とするメモリを効率的に管理します。mheap
: Goランタイムのグローバルなヒープ構造体で、大きなメモリチャンクの管理や、アロケータのキャッシュ(mcache
)へのメモリ提供など、ヒープ全体の管理を行います。mcache
: 各M(OSスレッド)に紐付けられたローカルなメモリキャッシュです。小さなオブジェクトの割り当てを高速化するために使用されます。mcache
はmheap
からメモリを受け取ります。FixAlloc
: Goランタイム内部で使われる、固定サイズのオブジェクトを効率的に割り当てるためのアロケータです。例えば、mcache
のようなランタイム内部のデータ構造の割り当てに使われます。
- パニック (Panic): Goにおけるパニックは、プログラムの回復不可能なエラーを示すメカニズムです。パニックが発生すると、通常の実行フローは中断され、遅延関数(
defer
)が実行された後、スタックがアンワインドされます。最終的に、パニックメッセージとスタックトレースが出力され、プログラムは終了します(recover
されない限り)。 - 起動シーケンス: Goプログラムが起動する際、ランタイムは様々な初期化ステップを実行します。これには、メモリ管理システムのセットアップ、スケジューラの初期化、メインゴルーチンの起動などが含まれます。これらのステップは特定の順序で実行され、一部のコンポーネントは他のコンポーネントが完全に初期化される前に必要とされることがあります。
技術的詳細
このコミットは、主に以下の2つのファイルに修正を加えています。
-
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
アロケータが不正な状態で使用されることを早期に検出し、デバッグを容易にするための防御的なプログラミングです。
-
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
内のcachealloc
(mcache
などの内部構造体を割り当てるための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·mheap
やm->mallocing
といったメモリ管理関連のシンボルを使用するために、このヘッダファイルが追加されました。if(runtime·mheap == 0 || runtime·mheap->cachealloc.size == 0)
: この新しい条件分岐が、このコミットの核心です。runtime·mheap == 0
: これは、Goランタイムのグローバルなヒープ構造体であるmheap
がまだ割り当てられていない状態を指します。mheap
はランタイムのメモリ管理の根幹をなすものであり、これが存在しないということは、メモリシステムが非常に初期の段階にあることを意味します。runtime·mheap->cachealloc.size == 0
:mheap
自体は存在しても、その内部で使われるcachealloc
(mcache
などのランタイム内部構造体を割り当てるための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の内部的な例外のようなもの)からパニックが呼び出された場合に、現在のスレッドのmcache
がnil
である可能性がある状況を処理します。この場合、runtime·allocmcache()
を呼び出して新しいmcache
を割り当てます。この部分は、新しい早期パニック処理のロジックの後に実行されるように変更されました。
これらの変更により、Goランタイムは、メモリ管理システムが完全に機能する前の非常に早い段階で発生するパニックに対しても、より適切に対応できるようになりました。これにより、デバッグ時に意味のあるエラーメッセージとスタックトレースが得られる可能性が高まります。
関連リンク
- Go言語の公式ドキュメント: https://golang.org/doc/
- Goランタイムのソースコード: https://github.com/golang/go/tree/master/src/runtime
- 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のパニックとリカバリに関する公式ドキュメントやチュートリアル(一般的な情報源)