[インデックス 18788] ファイルの概要
このコミットは、Goランタイムのメモリ管理における2つの重要なバグ修正に焦点を当てています。具体的には、efence
モードでのメモリ再利用時の問題と、SysAlloc
およびSysReserve
が返すメモリのアライメントに関する問題に対処しています。これらの修正は、Goプログラムの安定性とメモリ安全性を向上させることを目的としています。
コミット
commit da1bea0ef0355482e78b8dc0f3cf2f992a8464d7
Author: Russ Cox <rsc@golang.org>
Date: Thu Mar 6 18:34:29 2014 -0500
runtime: fix malloc page alignment + efence
Two memory allocator bug fixes.
- efence is not maintaining the proper heap metadata
to make eventual memory reuse safe, so use SysFault.
- now that our heap PageSize is 8k but most hardware
uses 4k pages, SysAlloc and SysReserve results must be
explicitly aligned. Do that in a few more call sites and
document this fact in malloc.h.
Fixes #7448.
LGTM=iant
R=golang-codereviews, josharian, iant
CC=dvyukov, golang-codereviews
https://golang.org/cl/71750048
GitHub上でのコミットページへのリンク
https://github.com/golang/go/commit/da1bea0ef0355482e78b8dc0f3cf2f992a8464d7
元コミット内容
このコミットは、Goランタイムのメモリ管理における2つのバグを修正します。
- efenceモードの修正:
efence
(error checking fence)モードにおいて、メモリ解放時にヒープのメタデータが適切に更新されないため、後続のメモリ再利用時に予期せぬクラッシュが発生する可能性がありました。この修正では、SysFree
の代わりにSysFault
を使用することで、解放されたメモリが再利用されないようにし、efence
モードでのメモリ安全性を確保します。これにより、efence
モードではメモリがより早く枯渇する可能性がありますが、不明なクラッシュは回避されます。将来的にはSysFree
に戻すためにMHeap_DeleteSpan
の実装が必要であることも示唆されています。 - メモリページアライメントの修正: Goランタイムのヒープ
PageSize
が8KBであるのに対し、多くのハードウェアのページサイズが4KBであるため、SysAlloc
やSysReserve
といったシステムコールが返すメモリのアドレスが、Goランタイムが期待する8KBアライメントになっていない場合がありました。この修正では、これらのシステムコールから取得したメモリを明示的に8KB境界にアライメントする処理を追加し、malloc.h
にこの事実を明記することで、メモリ割り当ての整合性を保ちます。
このコミットは、GoのIssue #7448を修正すると記載されていますが、現在のGitHubリポジトリではこの番号のIssueは確認できませんでした。
変更の背景
このコミットは、Goランタイムのメモリ管理における安定性とデバッグ可能性の向上を目的としています。
- efenceモードの信頼性向上:
efence
は、メモリの不正アクセスや解放済みメモリの使用といったバグを検出するためのデバッグモードです。しかし、既存の実装では、解放されたメモリがOSに返却され、その後再利用された際に、Goランタイムのヒープメタデータ(特にガベージコレクションのビットマップなど)との不整合が生じ、原因不明のクラッシュを引き起こす問題がありました。この問題は、デバッグモードであるにもかかわらず、デバッグを困難にするという矛盾を抱えていました。この修正は、efence
モードの信頼性を高め、より効果的なデバッグを可能にすることを目的としています。 - メモリ割り当てのアライメント問題の解決: Goランタイムのメモリ管理は、特定のページサイズ(8KB)に基づいて設計されています。しかし、基盤となるOSやハードウェアのページサイズ(一般的に4KB)との間に差異がある場合、OSから直接取得したメモリのアドレスがGoランタイムの期待するアライメントを満たさないことがありました。これにより、メモリ管理システム内部でアライメント違反が発生し、潜在的なバグやパフォーマンスの問題につながる可能性がありました。この修正は、異なるページサイズ間の不整合を解消し、メモリ割り当ての堅牢性を確保することを目的としています。
これらの問題は、Goプログラムの実行時におけるメモリ関連のクラッシュやデバッグの困難さとして現れる可能性があり、Goランタイムの基盤的な安定性を確保するために重要な修正でした。
前提知識の解説
このコミットの変更を理解するためには、以下のGoランタイムのメモリ管理に関する概念とシステムコールについての知識が必要です。
1. Goランタイムのメモリ管理 (MHeap, MSpan, PageSize)
Goランタイムは独自のメモリマネージャを持っており、OSから直接メモリを要求し、それを管理してGoプログラムに割り当てます。
- MHeap: Goランタイムのグローバルなヒープを表す構造体です。メモリの割り当てと解放を管理します。
- MSpan: 連続したメモリページのブロックを表す構造体です。Goランタイムは、メモリをMSpan単位で管理し、オブジェクトのサイズに応じて異なるサイズのMSpanを使用します。
- PageSize: Goランタイムが内部的に使用するメモリページのサイズです。このコミットの時点では8KB(
1<<PageShift
で表現される)です。これはOSのページサイズ(通常4KB)とは異なる場合があります。
2. efenceモード
efence
(error checking fence)は、Goランタイムのデバッグモードの一つです。このモードを有効にすると、メモリの不正アクセス(例えば、解放済みメモリへの書き込みや読み込み)を検出するために、解放されたメモリページをOSから切り離したり、アクセス保護を設定したりします。これにより、メモリ関連のバグを早期に発見しやすくなります。
3. システムコールとメモリ管理
Goランタイムは、OSからメモリを要求するために以下のシステムコール(またはそれに相当する内部関数)を使用します。
SysAlloc(size, stats)
: OSから指定されたsize
のメモリを割り当てます。この関数が返すメモリはOSのページサイズにアライメントされています。SysReserve(address, size)
: 指定されたaddress
からsize
の仮想アドレス空間を予約します。実際の物理メモリはまだ割り当てられません。address
がnil
の場合、OSが適切なアドレスを選択します。SysFree(address, size, stats)
: 指定されたaddress
からsize
のメモリをOSに返却します。これにより、OSはそのメモリを他のプロセスに再割り当てしたり、物理メモリを解放したりできます。SysFault(address, size)
: 指定されたaddress
からsize
のメモリページにアクセス保護を設定し、アクセスするとセグメンテーションフォルト(クラッシュ)を引き起こすようにします。これは、解放済みメモリへのアクセスを検出するためにefence
モードでよく使用されます。SysFree
とは異なり、メモリをOSに返却するわけではありませんが、アクセスを禁止します。
4. メモリのアライメント
メモリのアライメントとは、メモリ上のデータが特定のアドレス境界に配置されることを指します。例えば、4バイトのアライメントが必要なデータは、アドレスが4の倍数である場所に配置されます。CPUは、アライメントされたデータにアクセスする方が効率的であり、場合によってはアライメントされていないデータへのアクセスを許可しないこともあります。Goランタイムのメモリ管理では、内部的なデータ構造やパフォーマンスのために、OSが提供するアライメントよりも厳密なアライメント(例: 8KBページアライメント)を要求する場合があります。
技術的詳細
このコミットは、Goランタイムのメモリ管理における2つの独立した、しかし重要な問題を解決しています。
1. efenceモードにおけるメモリ再利用の安全性確保
問題点: 従来のefence
モードでは、runtime·free
関数内でメモリを解放する際にruntime·SysFree
を使用していました。SysFree
はOSにメモリを返却するため、OSはそのメモリを後でGoランタイムに再割り当てする可能性があります。しかし、SysFree
が呼び出された際に、Goランタイムのヒープメタデータ(特にMSpan
構造体やガベージコレクションのビットマップ)が適切に更新されていませんでした。このため、同じ物理メモリが再利用された際に、古いメタデータが残っていることで、ガベージコレクタが混乱したり、不正なメモリアクセスが発生したりして、原因不明のクラッシュにつながっていました。
解決策: このコミットでは、efence
モードの場合にruntime·SysFree
の代わりにruntime·SysFault
を使用するように変更しました。SysFault
はメモリをOSに返却するのではなく、そのメモリ領域へのアクセスを禁止します。これにより、解放されたメモリがGoランタイムによって再利用されることがなくなり、メタデータの不整合によるクラッシュを防ぎます。この変更の副作用として、efence
モードではメモリがOSに返却されないため、プログラムがより早くメモリ不足に陥る可能性があります。コミットメッセージでは、将来的にSysFree
に戻すためには、ヒープからMSpan
を削除するMHeap_DeleteSpan
のようなメカニズムを実装する必要があることが示唆されています。
2. メモリ割り当てのページアライメントの強制
問題点: Goランタイムのヒープは8KBのPageSize
で動作するように設計されていますが、多くのOSやハードウェアは4KBのページサイズを使用しています。runtime·SysAlloc
やruntime·SysReserve
といったOSレベルのメモリ割り当て関数は、OSのページサイズ(通常4KB)にアライメントされたアドレスを返します。このため、Goランタイムがこれらの関数から受け取ったメモリのアドレスが、Goの期待する8KB境界にアライメントされていない場合がありました。特に、mallocinit
(メモリマネージャの初期化)やMHeap_SysAlloc
(ヒープへのシステム割り当て)の際に、このアライメントの不整合が問題となる可能性がありました。
解決策: このコミットでは、runtime·mallocinit
とruntime·MHeap_SysAlloc
の複数の箇所で、SysReserve
やSysAlloc
から返されたポインタを明示的にGoランタイムのPageSize
(8KB)にアライメントする処理を追加しています。具体的には、ROUND((uintptr)p, PageSize)
のような操作を用いて、ポインタを次のPageSize
の倍数に切り上げています。また、malloc.h
のSysAlloc
とSysReserve
のドキュメントに、これらの関数がOSアライメントされたメモリを返すため、呼び出し元がGoランタイムのより大きなアライメント要件に合わせて再アライメントする必要があることを明記しています。これにより、Goランタイムのメモリ管理システムが常に期待するアライメントでメモリを操作できるようになり、潜在的なアライメント違反によるバグを防ぎます。
これらの修正は、Goランタイムの低レベルなメモリ管理の正確性と堅牢性を高めるものであり、Goプログラム全体の安定性に寄与します。
コアとなるコードの変更箇所
このコミットによる主要なコード変更は、以下の3つのファイルにわたっています。
-
src/pkg/runtime/malloc.goc
:runtime·free
関数内で、efence
モードの場合にruntime·SysFree
の呼び出しをruntime·SysFault
に変更。runtime·mallocinit
関数内で、SysReserve
の呼び出し後に返されたポインタをPageSize
にアライメントする処理をより明確にし、アライメントチェックを追加。runtime·MHeap_SysAlloc
関数内で、SysReserve
やSysAlloc
から返されたポインタをPageSize
にアライメントする処理を追加し、アライメントチェックを追加。
-
src/pkg/runtime/malloc.h
:SysAlloc
とSysReserve
関数のコメントに、これらの関数がOSアライメントされたメモリを返すため、呼び出し元がGoランタイムのより大きなアライメント要件に合わせて再アライメントする必要があることを追記。
-
src/pkg/runtime/mgc0.c
:runtime·MSpan_Sweep
関数内で、efence
モードの場合にruntime·SysFree
の呼び出しをruntime·SysFault
に変更。これはmalloc.goc
の変更と連携しています。
コアとなるコードの解説
src/pkg/runtime/malloc.goc
runtime·free
関数内の変更
--- a/src/pkg/runtime/malloc.goc
+++ b/src/pkg/runtime/malloc.goc
@@ -310,8 +310,22 @@ runtime·free(void *v)
// they might coalesce v into other spans and change the bitmap further.
runtime·markfreed(v);
runtime·unmarkspan(v, 1<<PageShift);
+ // NOTE(rsc,dvyukov): The original implementation of efence
+ // in CL 22060046 used SysFree instead of SysFault, so that
+ // the operating system would eventually give the memory
+ // back to us again, so that an efence program could run
+ // longer without running out of memory. Unfortunately,
+ // calling SysFree here without any kind of adjustment of the
+ // heap data structures means that when the memory does
+ // come back to us, we have the wrong metadata for it, either in
+ // the MSpan structures or in the garbage collection bitmap.
+ // Using SysFault here means that the program will run out of
+ // memory fairly quickly in efence mode, but at least it won't
+ // have mysterious crashes due to confused memory reuse.
+ // It should be possible to switch back to SysFree if we also
+ // implement and then call some kind of MHeap_DeleteSpan.
if(runtime·debug.efence)
- runtime·SysFree((void*)(s->start<<PageShift), size, &mstats.heap_sys);
+ runtime·SysFault((void*)(s->start<<PageShift), size);
else
runtime·MHeap_Free(&runtime·mheap, s, 1);
c->local_nlargefree++;
この変更は、efence
デバッグモードが有効な場合に、解放されたメモリをOSに返却するSysFree
の代わりに、そのメモリ領域へのアクセスを禁止するSysFault
を呼び出すようにします。これにより、解放されたメモリがGoランタイムによって再利用されることを防ぎ、ヒープメタデータの不整合によるクラッシュを回避します。追加されたコメントは、この変更の理由と、将来的な改善の可能性(MHeap_DeleteSpan
の実装)を説明しています。
runtime·mallocinit
関数内の変更
--- a/src/pkg/runtime/malloc.goc
+++ b/src/pkg/runtime/malloc.goc
@@ -533,13 +551,16 @@ runtime·mallocinit(void)
// PageSize can be larger than OS definition of page size,
// so SysReserve can give us a PageSize-unaligned pointer.
// To overcome this we ask for PageSize more and round up the pointer.
- p = (byte*)ROUND((uintptr)p, PageSize);
+ p1 = (byte*)ROUND((uintptr)p, PageSize);
- runtime·mheap.spans = (MSpan**)p;
- runtime·mheap.bitmap = p + spans_size;
- runtime·mheap.arena_start = p + spans_size + bitmap_size;
+ runtime·mheap.spans = (MSpan**)p1;
+ runtime·mheap.bitmap = p1 + spans_size;
+ runtime·mheap.arena_start = p1 + spans_size + bitmap_size;
runtime·mheap.arena_used = runtime·mheap.arena_start;
- runtime·mheap.arena_end = runtime·mheap.arena_start + arena_size;
+ runtime·mheap.arena_end = p + p_size;
+
+ if(((uintptr)runtime·mheap.arena_start & (PageSize-1)) != 0)
+ runtime·throw("misrounded allocation in mallocinit");
// Initialize the rest of the allocator.
runtime·MHeap_Init(&runtime·mheap);
この部分では、SysReserve
によって予約された仮想アドレス空間の開始ポインタp
を、GoランタイムのPageSize
(8KB)にアライメントするためにROUND
マクロを使用しています。元のコードではp = (byte*)ROUND((uintptr)p, PageSize);
と直接p
を更新していましたが、新しいコードでは一時変数p1
を導入し、mheap
の各フィールド(spans
, bitmap
, arena_start
)をp1
に基づいて設定しています。また、arena_end
の計算もp + p_size
に変更され、最後にarena_start
が正しくアライメントされているかを確認するためのruntime·throw
によるチェックが追加されています。これにより、メモリマネージャの初期化段階でアライメントの不整合が発生しないようにしています。
runtime·MHeap_SysAlloc
関数内の変更
--- a/src/pkg/runtime/malloc.goc
+++ b/src/pkg/runtime/malloc.goc
@@ -578,6 +608,9 @@ runtime·MHeap_SysAlloc(MHeap *h, uintptr n)
// runtime·MHeap_MapSpans(h);
// if(raceenabled)
// runtime·racemapshadow(p, n);
+ //
+ // if(((uintptr)p & (PageSize-1)) != 0)
+ // runtime·throw("misrounded allocation in MHeap_SysAlloc");
// return p;
// }
//
@@ -588,27 +621,32 @@ runtime·MHeap_SysAlloc(MHeap *h, uintptr n)
// On 32-bit, once the reservation is gone we can
// try to get memory at a location chosen by the OS
// and hope that it is in the range we allocated bitmap for.
- p = runtime·SysAlloc(n, &mstats.heap_sys);
+ p_size = ROUND(n, PageSize) + PageSize;
+ p = runtime·SysAlloc(p_size, &mstats.heap_sys);
if(p == nil)
return nil;
- if(p < h->arena_start || p+n - h->arena_start >= MaxArena32) {
+ if(p < h->arena_start || p+p_size - h->arena_start >= MaxArena32) {
runtime·printf("runtime: memory allocated by OS (%p) not in usable range [%p,%p)\\n",
p, h->arena_start, h->arena_start+MaxArena32);
- runtime·SysFree(p, n, &mstats.heap_sys);
+ runtime·SysFree(p, p_size, &mstats.heap_sys);
return nil;
}
-
+
+ p_end = p + p_size;
+ p += -(uintptr)p & (PageSize-1);
if(p+n > h->arena_used) {
h->arena_used = p+n;
- if(h->arena_used > h->arena_end)
- h->arena_end = h->arena_used;
+ if(p_end > h->arena_end)
+ h->arena_end = p_end;
runtime·MHeap_MapBits(h);
runtime·MHeap_MapSpans(h);
if(raceenabled)
runtime·racemapshadow(p, n);
}
+ if(((uintptr)p & (PageSize-1)) != 0)
+ runtime·throw("misrounded allocation in MHeap_SysAlloc");
return p;
}
このMHeap_SysAlloc
関数は、ヒープがOSから直接メモリを要求する際に使用されます。変更点としては、SysAlloc
に渡すサイズをROUND(n, PageSize) + PageSize
として、Goのページサイズにアライメントされたメモリを確実に取得できるようにしています。また、SysAlloc
から返されたポインタp
をp += -(uintptr)p & (PageSize-1);
という式で明示的にPageSize
にアライメントしています。これは、p
をPageSize
の次の倍数に切り上げる効果があります。さらに、arena_end
の更新ロジックもp_end
を使用するように変更され、最後にアライメントが正しく行われたかを確認するruntime·throw
によるチェックが追加されています。
src/pkg/runtime/malloc.h
--- a/src/pkg/runtime/malloc.h
+++ b/src/pkg/runtime/malloc.h
@@ -158,6 +158,9 @@ struct MLink
// SysAlloc obtains a large chunk of zeroed memory from the
// operating system, typically on the order of a hundred kilobytes
// or a megabyte.
+// NOTE: SysAlloc returns OS-aligned memory, but the heap allocator
+// may use larger alignment, so the caller must be careful to realign the
+// memory obtained by SysAlloc.
//
// SysUnused notifies the operating system that the contents
// of the memory region are no longer needed and can be reused
@@ -173,6 +176,9 @@ struct MLink
// If the pointer passed to it is non-nil, the caller wants the
// reservation there, but SysReserve can still choose another
// location if that one is unavailable.
+// NOTE: SysReserve returns OS-aligned memory, but the heap allocator
+// may use larger alignment, so the caller must be careful to realign the
+// memory obtained by SysAlloc.
//
// SysMap maps previously reserved address space for use.
//
SysAlloc
とSysReserve
関数のコメントに、これらの関数がOSアライメントされたメモリを返すものの、Goランタイムのヒープアロケータがより大きなアライメントを使用する可能性があるため、呼び出し元が取得したメモリを再アライメントする必要があるという注意書きが追加されました。これは、コード変更の意図を明確にし、将来的な開発者が同様の問題に遭遇するのを防ぐための重要なドキュメントの更新です。
src/pkg/runtime/mgc0.c
--- a/src/pkg/runtime/mgc0.c
+++ b/src/pkg/runtime/mgc0.c
@@ -1817,8 +1817,9 @@ runtime·MSpan_Sweep(MSpan *s)
// important to set sweepgen before returning it to heap
runtime·atomicstore(&s->sweepgen, sweepgen);
sweepgenset = true;
+\t\t\t// See note about SysFault vs SysFree in malloc.goc.
if(runtime·debug.efence)
- runtime·SysFree(p, size, &mstats.gc_sys);
+ runtime·SysFault(p, size);
else
runtime·MHeap_Free(&runtime·mheap, s, 1);
c->local_nlargefree++;
この変更は、ガベージコレクションのスイープ処理中にMSpan
が解放される際に、efence
モードが有効であればSysFree
の代わりにSysFault
を呼び出すようにします。これはmalloc.goc
のruntime·free
関数における変更と一貫しており、efence
モードでのメモリ安全性を確保するためのものです。
関連リンク
- Goのメモリ管理に関する公式ドキュメントやブログ記事(当時のもの)
- Goのガベージコレクションに関する情報
- Goの
efence
モードに関する情報
参考にした情報源リンク
- golang/go GitHubリポジトリ
- Go CL 71750048 (コミットメッセージに記載されているGoのコードレビューシステムへのリンク)
- GoのIssue #7448 (ただし、現在のGitHubリポジトリでは見つかりませんでした。当時のGoのIssueトラッカーはGoogle Code上にあった可能性もあります。)
- Goのメモリ管理に関する一般的な知識
- OSのページングとメモリ管理に関する一般的な知識