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

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

このコミットは、Goランタイムにおけるメモリ割り当ての堅牢性を向上させるための変更を含んでいます。具体的には、src/pkg/runtime/malloc.gocsrc/pkg/runtime/mgc0.csrc/pkg/runtime/mheap.csrc/pkg/runtime/mprof.goc の4つのファイルが変更されています。これらのファイルは、Goランタイムのメモリ管理、特にシステムからのメモリ確保 (runtime·SysAlloc) とガベージコレクションに関連する部分を扱っています。

コミット

  • コミットハッシュ: 94955f9b4076aafd54fc756b9d11065e2bba5b05
  • 作者: Jan Ziak 0xe2.0x9a.0x9b@gmail.com
  • コミット日時: Fri Mar 1 00:21:08 2013 -0500

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

https://github.com/golang/go/commit/94955f9b4076aafd54fc756b9d11065e2bba5b05

元コミット内容

runtime: check the value returned by runtime·SysAlloc

R=golang-dev, rsc
CC=golang-dev, minux.ma
https://golang.org/cl/7424047

変更の背景

このコミットが行われた2013年当時、Go言語はまだ比較的新しい言語であり、ランタイムの安定性と堅牢性は継続的に改善されていました。この変更の背景には、runtime·SysAlloc 関数がメモリ確保に失敗した場合(例えば、システムに利用可能なメモリが不足している場合)に nil を返す可能性があるにもかかわらず、その戻り値が適切にチェックされていなかったという問題がありました。

runtime·SysAlloc は、Goランタイムがオペレーティングシステムから直接メモリを要求するために使用する低レベルの関数です。この関数が nil を返した場合、それはメモリ確保の失敗を意味します。しかし、既存のコードではこの失敗が考慮されておらず、nil ポインタがそのまま使用されることで、ランタイムのクラッシュや未定義の動作を引き起こす可能性がありました。

このコミットは、このようなメモリ確保の失敗ケースを明示的に検出し、runtime·throw("runtime: cannot allocate memory") を呼び出すことで、より安全かつ予測可能な方法でランタイムを終了させることを目的としています。これにより、メモリ不足のような深刻な状況下でも、Goプログラムがより適切に振る舞うようになります。

前提知識の解説

Goランタイム

Goランタイムは、Goプログラムの実行を管理する非常に重要なコンポーネントです。これには、ガベージコレクタ、スケジューラ(ゴルーチンの管理)、メモリ割り当て、スタック管理、システムコールインターフェースなどが含まれます。Goプログラムは、コンパイル時にランタイムとリンクされ、ランタイムの機能を利用して実行されます。

runtime·SysAlloc

runtime·SysAlloc は、Goランタイムがオペレーティングシステムから直接メモリを要求するための内部関数です。これは、Goのヒープやその他のランタイム内部構造のために、OSから生のメモリページを取得するために使用されます。この関数は、Goのメモリ管理の最下層に位置し、通常、Goプログラムのユーザーコードから直接呼び出されることはありません。

nil ポインタ

nil は、Goにおけるゼロ値の一つで、ポインタ型の場合、有効なメモリアドレスを指していないことを示します。メモリ割り当て関数が nil を返す場合、それは要求されたメモリを確保できなかったことを意味します。nil ポインタを逆参照しようとすると、プログラムはパニック(Goにおけるランタイムエラー)を引き起こし、通常はクラッシュします。

runtime·throw

runtime·throw は、Goランタイム内部で使用される関数で、回復不可能な致命的なエラーが発生した場合に呼び出されます。これは、Goのユーザーコードで発生する通常のパニックとは異なり、ランタイム自体が深刻な問題に直面した際に、プログラムを即座に終了させるために使用されます。runtime·throw が呼び出されると、Goプログラムはスタックトレースを出力して終了します。このコミットでは、メモリ確保の失敗という致命的な状況を runtime·throw で処理することで、問題の早期発見と診断を可能にしています。

ガベージコレクション (GC)

Goは自動メモリ管理(ガベージコレクション)を採用しています。ガベージコレクタは、プログラムがもはや使用しないメモリを自動的に解放し、再利用可能にします。このコミットで変更されている mgc0.cmheap.c は、Goのガベージコレクタやヒープ管理の内部実装に関連するファイルです。runtime·SysAlloc は、GCが新しいメモリ領域をOSから取得する際にも使用されます。

技術的詳細

このコミットの技術的詳細は、Goランタイムのメモリ管理におけるエラーハンドリングの改善に集約されます。以前のバージョンでは、runtime·SysAlloc がメモリ確保に失敗して nil を返した場合、その nil ポインタが後続の処理でそのまま使用され、結果としてセグメンテーション違反などのクラッシュを引き起こす可能性がありました。これは、メモリ不足のようなシステムレベルの問題が発生した際に、Goプログラムが不安定になる原因となり得ました。

このコミットでは、runtime·SysAlloc の呼び出し直後に、その戻り値が nil であるかどうかをチェックするコードが追加されています。もし nil であった場合、すなわちメモリ確保に失敗した場合、runtime·throw("runtime: cannot allocate memory") が呼び出されます。

この変更の意義は以下の点にあります。

  1. 堅牢性の向上: メモリ不足という予期せぬ状況下でも、プログラムが未定義の動作に陥るのではなく、明確なエラーメッセージとともに終了するようになります。これにより、デバッグが容易になり、システムの安定性が向上します。
  2. 早期エラー検出: nil ポインタがプログラムの奥深くまで伝播し、後になってクラッシュするのではなく、メモリ確保の失敗という根本原因が発生した時点で即座にエラーが報告されます。
  3. 診断の容易さ: runtime: cannot allocate memory という具体的なエラーメッセージは、問題がメモリ不足に起因することを明確に示し、システム管理者が問題を診断する上で非常に役立ちます。

この修正は、Goランタイムの様々な箇所、特にメモリを直接OSから取得する可能性のある低レベルな関数呼び出しの周辺に適用されています。これには、型情報のフラッシュ、ガベージコレクタのワークバッファ管理、ヒープのスパン管理、プロファイリングのためのメモリプール割り当てなどが含まれます。これらの箇所はすべて、Goプログラムの基本的な動作に不可欠な部分であり、ここでのメモリ確保の失敗はシステム全体の安定性に直接影響します。

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

このコミットでは、以下の4つのファイルにコードが追加されています。変更はすべて runtime·SysAlloc の呼び出し直後に、その戻り値が nil かどうかをチェックし、nil の場合は runtime·throw を呼び出すというパターンに従っています。

src/pkg/runtime/malloc.goc

--- a/src/pkg/runtime/malloc.goc
+++ b/src/pkg/runtime/malloc.goc
@@ -519,6 +519,8 @@ runtime·settype_flush(M *mp, bool sysalloc)\n \t\t\t\tdata3 = runtime·mallocgc(nbytes3, FlagNoPointers, 0, 1);\n \t\t\t} else {\n \t\t\t\tdata3 = runtime·SysAlloc(nbytes3);\n+\t\t\t\tif(data3 == nil)\n+\t\t\t\t\truntime·throw(\"runtime: cannot allocate memory\");\n \t\t\t\tif(0) runtime·printf(\"settype(0->3): SysAlloc(%x) --> %p\\n\", (uint32)nbytes3, data3);\n \t\t\t}\n \n@@ -555,6 +557,8 @@ runtime·settype_flush(M *mp, bool sysalloc)\n \t\t\t\t\tdata2 = runtime·mallocgc(nbytes2, FlagNoPointers, 0, 1);\n \t\t\t\t} else {\n \t\t\t\t\tdata2 = runtime·SysAlloc(nbytes2);\n+\t\t\t\t\tif(data2 == nil)\n+\t\t\t\t\t\truntime·throw(\"runtime: cannot allocate memory\");\n \t\t\t\t\tif(0) runtime·printf(\"settype.(3->2): SysAlloc(%x) --> %p\\n\", (uint32)nbytes2, data2);\n \t\t\t\t}\n \ndiff --git a/src/pkg/runtime/mgc0.c b/src/pkg/runtime/mgc0.c

src/pkg/runtime/mgc0.c

--- a/src/pkg/runtime/mgc0.c
+++ b/src/pkg/runtime/mgc0.c
@@ -585,6 +585,8 @@ scanblock(Workbuf *wbuf, Obj *wp, uintptr nobj, bool keepworking)\n \n \t\tif(bufferList == nil) {\n \t\t\tbufferList = runtime·SysAlloc(sizeof(*bufferList));\n+\t\t\tif(bufferList == nil)\n+\t\t\t\truntime·throw(\"runtime: cannot allocate memory\");\n \t\t\tbufferList->next = nil;\n \t\t}\n \t\tscanbuffers = bufferList;\n@@ -1147,6 +1149,8 @@ getempty(Workbuf *b)\n \t\tif(work.nchunk < sizeof *b) {\n \t\t\twork.nchunk = 1<<20;\n \t\t\twork.chunk = runtime·SysAlloc(work.nchunk);\n+\t\t\tif(work.chunk == nil)\n+\t\t\t\truntime·throw(\"runtime: cannot allocate memory\");\n \t\t}\n \t\tb = (Workbuf*)work.chunk;\n \t\twork.chunk += sizeof *b;\n@@ -1230,6 +1234,8 @@ addroot(Obj obj)\n \t\tif(cap < 2*work.rootcap)\n \t\t\tcap = 2*work.rootcap;\n \t\tnew = (Obj*)runtime·SysAlloc(cap*sizeof(Obj));\n+\t\tif(new == nil)\n+\t\t\truntime·throw(\"runtime: cannot allocate memory\");\n \t\tif(work.roots != nil) {\n \t\t\truntime·memmove(new, work.roots, work.rootcap*sizeof(Obj));\n \t\t\truntime·SysFree(work.roots, work.rootcap*sizeof(Obj));\n@@ -1381,6 +1387,8 @@ handlespecial(byte *p, uintptr size)\n \tif(finq == nil || finq->cnt == finq->cap) {\n \t\tif(finc == nil) {\n \t\t\tfinc = runtime·SysAlloc(PageSize);\n+\t\t\tif(finc == nil)\n+\t\t\t\truntime·throw(\"runtime: cannot allocate memory\");\n \t\t\tfinc->cap = (PageSize - sizeof(FinBlock)) / sizeof(Finalizer) + 1;\n \t\t\tfinc->alllink = allfin;\n \t\t\tallfin = finc;\ndiff --git a/src/pkg/runtime/mheap.c b/src/pkg/runtime/mheap.c

src/pkg/runtime/mheap.c

--- a/src/pkg/runtime/mheap.c
+++ b/src/pkg/runtime/mheap.c
@@ -37,6 +37,8 @@ RecordSpan(void *vh, byte *p)\n \t\tif(cap < h->nspancap*3/2)\n \t\t\tcap = h->nspancap*3/2;\n \t\tall = (MSpan**)runtime·SysAlloc(cap*sizeof(all[0]));\n+\t\tif(all == nil)\n+\t\t\truntime·throw(\"runtime: cannot allocate memory\");\n \t\tif(h->allspans) {\n \t\t\truntime·memmove(all, h->allspans, h->nspancap*sizeof(all[0]));\n \t\t\truntime·SysFree(h->allspans, h->nspancap*sizeof(all[0]));

src/pkg/runtime/mprof.goc

--- a/src/pkg/runtime/mprof.goc
+++ b/src/pkg/runtime/mprof.goc
@@ -40,6 +40,8 @@ allocate(uintptr size)\n \truntime·lock(&alloclock);\n \tif(size > poolfree) {\n \t\tpool = runtime·SysAlloc(Chunk);\n+\t\tif(pool == nil)\n+\t\t\truntime·throw(\"runtime: cannot allocate memory\");\n \t\tpoolfree = Chunk;\n \t}\n \tv = pool;\n@@ -100,6 +102,8 @@ stkbucket(int32 typ, uintptr *stk, int32 nstk, bool alloc)\n \n \tif(buckhash == nil) {\n \t\tbuckhash = runtime·SysAlloc(BuckHashSize*sizeof buckhash[0]);\n+\t\tif(buckhash == nil)\n+\t\t\truntime·throw(\"runtime: cannot allocate memory\");\n \t\tmstats.buckhash_sys += BuckHashSize*sizeof buckhash[0];\n \t}\n \n@@ -123,6 +127,8 @@ stkbucket(int32 typ, uintptr *stk, int32 nstk, bool alloc)\n \t\treturn nil;\n \n \tb = allocate(sizeof *b + nstk*sizeof stk[0]);\n+\tif(b == nil)\n+\t\truntime·throw(\"runtime: cannot allocate memory\");\n \tbucketmem += sizeof *b + nstk*sizeof stk[0];\n \truntime·memmove(b->stk, stk, nstk*sizeof stk[0]);\n \tb->typ = typ;

コアとなるコードの解説

各ファイルでの変更は、runtime·SysAlloc の呼び出し結果をチェックし、nil であれば runtime·throw を呼び出すという共通のパターンに従っています。

  • src/pkg/runtime/malloc.goc:

    • runtime·settype_flush 関数内で、runtime·SysAlloc を使用して data3 および data2 という変数のためのメモリを確保しています。これらの割り当てが失敗した場合に runtime: cannot allocate memory というエラーでランタイムを終了させるように変更されました。この関数は、Goの型システムが内部的に使用するメモリの管理に関連している可能性があります。
  • src/pkg/runtime/mgc0.c:

    • scanblock 関数内で、ガベージコレクタが使用する bufferList のためのメモリ確保。
    • getempty 関数内で、ガベージコレクタのワークバッファ (work.chunk) のためのメモリ確保。
    • addroot 関数内で、GCルート(ガベージコレクションの起点となるオブジェクト)を管理するための new 配列のメモリ確保。
    • handlespecial 関数内で、ファイナライザ(オブジェクトがガベージコレクションされる直前に実行される関数)を管理するための finc のメモリ確保。 これらの変更は、ガベージコレクタが内部的に必要とするメモリの確保が失敗した場合に、ランタイムがクラッシュするのを防ぎます。
  • src/pkg/runtime/mheap.c:

    • RecordSpan 関数内で、ヒープのスパン(メモリ領域の管理単位)を記録するための all 配列のメモリ確保。Goのヒープ管理において重要な部分であり、ここでのメモリ確保失敗はヒープの整合性に影響を与えます。
  • src/pkg/runtime/mprof.goc:

    • allocate 関数内で、プロファイリングデータのためのメモリプール (pool) の確保。
    • stkbucket 関数内で、スタックトレースのバケット (buckhash) および個々のスタックバケット (b) のためのメモリ確保。 これらの変更は、プロファイリング機能がメモリを確保する際に発生する可能性のある問題を対処し、プロファイリングがランタイムの安定性を損なわないようにします。

これらの変更は、Goランタイムの様々なサブシステムがシステムからメモリを要求する際に、その要求が失敗する可能性を考慮し、堅牢なエラーハンドリングを導入したものです。これにより、Goプログラムはより安定し、メモリ不足のような極端な状況下でも予測可能な振る舞いをするようになります。

関連リンク

参考にした情報源リンク