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

[インデックス 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つのバグを修正します。

  1. efenceモードの修正: efence (error checking fence)モードにおいて、メモリ解放時にヒープのメタデータが適切に更新されないため、後続のメモリ再利用時に予期せぬクラッシュが発生する可能性がありました。この修正では、SysFreeの代わりにSysFaultを使用することで、解放されたメモリが再利用されないようにし、efenceモードでのメモリ安全性を確保します。これにより、efenceモードではメモリがより早く枯渇する可能性がありますが、不明なクラッシュは回避されます。将来的にはSysFreeに戻すためにMHeap_DeleteSpanの実装が必要であることも示唆されています。
  2. メモリページアライメントの修正: GoランタイムのヒープPageSizeが8KBであるのに対し、多くのハードウェアのページサイズが4KBであるため、SysAllocSysReserveといったシステムコールが返すメモリのアドレスが、Goランタイムが期待する8KBアライメントになっていない場合がありました。この修正では、これらのシステムコールから取得したメモリを明示的に8KB境界にアライメントする処理を追加し、malloc.hにこの事実を明記することで、メモリ割り当ての整合性を保ちます。

このコミットは、GoのIssue #7448を修正すると記載されていますが、現在のGitHubリポジトリではこの番号のIssueは確認できませんでした。

変更の背景

このコミットは、Goランタイムのメモリ管理における安定性とデバッグ可能性の向上を目的としています。

  1. efenceモードの信頼性向上: efenceは、メモリの不正アクセスや解放済みメモリの使用といったバグを検出するためのデバッグモードです。しかし、既存の実装では、解放されたメモリがOSに返却され、その後再利用された際に、Goランタイムのヒープメタデータ(特にガベージコレクションのビットマップなど)との不整合が生じ、原因不明のクラッシュを引き起こす問題がありました。この問題は、デバッグモードであるにもかかわらず、デバッグを困難にするという矛盾を抱えていました。この修正は、efenceモードの信頼性を高め、より効果的なデバッグを可能にすることを目的としています。
  2. メモリ割り当てのアライメント問題の解決: 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の仮想アドレス空間を予約します。実際の物理メモリはまだ割り当てられません。addressnilの場合、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·SysAllocruntime·SysReserveといったOSレベルのメモリ割り当て関数は、OSのページサイズ(通常4KB)にアライメントされたアドレスを返します。このため、Goランタイムがこれらの関数から受け取ったメモリのアドレスが、Goの期待する8KB境界にアライメントされていない場合がありました。特に、mallocinit(メモリマネージャの初期化)やMHeap_SysAlloc(ヒープへのシステム割り当て)の際に、このアライメントの不整合が問題となる可能性がありました。

解決策: このコミットでは、runtime·mallocinitruntime·MHeap_SysAllocの複数の箇所で、SysReserveSysAllocから返されたポインタを明示的にGoランタイムのPageSize(8KB)にアライメントする処理を追加しています。具体的には、ROUND((uintptr)p, PageSize)のような操作を用いて、ポインタを次のPageSizeの倍数に切り上げています。また、malloc.hSysAllocSysReserveのドキュメントに、これらの関数がOSアライメントされたメモリを返すため、呼び出し元がGoランタイムのより大きなアライメント要件に合わせて再アライメントする必要があることを明記しています。これにより、Goランタイムのメモリ管理システムが常に期待するアライメントでメモリを操作できるようになり、潜在的なアライメント違反によるバグを防ぎます。

これらの修正は、Goランタイムの低レベルなメモリ管理の正確性と堅牢性を高めるものであり、Goプログラム全体の安定性に寄与します。

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

このコミットによる主要なコード変更は、以下の3つのファイルにわたっています。

  1. src/pkg/runtime/malloc.goc:

    • runtime·free関数内で、efenceモードの場合にruntime·SysFreeの呼び出しをruntime·SysFaultに変更。
    • runtime·mallocinit関数内で、SysReserveの呼び出し後に返されたポインタをPageSizeにアライメントする処理をより明確にし、アライメントチェックを追加。
    • runtime·MHeap_SysAlloc関数内で、SysReserveSysAllocから返されたポインタをPageSizeにアライメントする処理を追加し、アライメントチェックを追加。
  2. src/pkg/runtime/malloc.h:

    • SysAllocSysReserve関数のコメントに、これらの関数がOSアライメントされたメモリを返すため、呼び出し元がGoランタイムのより大きなアライメント要件に合わせて再アライメントする必要があることを追記。
  3. 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から返されたポインタpp += -(uintptr)p & (PageSize-1);という式で明示的にPageSizeにアライメントしています。これは、pPageSizeの次の倍数に切り上げる効果があります。さらに、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.
 //

SysAllocSysReserve関数のコメントに、これらの関数が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.gocruntime·free関数における変更と一貫しており、efenceモードでのメモリ安全性を確保するためのものです。

関連リンク

  • Goのメモリ管理に関する公式ドキュメントやブログ記事(当時のもの)
  • Goのガベージコレクションに関する情報
  • Goのefenceモードに関する情報

参考にした情報源リンク

  • golang/go GitHubリポジトリ
  • Go CL 71750048 (コミットメッセージに記載されているGoのコードレビューシステムへのリンク)
  • GoのIssue #7448 (ただし、現在のGitHubリポジトリでは見つかりませんでした。当時のGoのIssueトラッカーはGoogle Code上にあった可能性もあります。)
  • Goのメモリ管理に関する一般的な知識
  • OSのページングとメモリ管理に関する一般的な知識