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

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

このコミットは、GoランタイムにおけるDarwin (macOS) および FreeBSD システムコール mmap の戻り値のチェックに関する修正です。具体的には、mmap がエラーを返す際の挙動の違いと、それに対応するGoランタイムのメモリ管理関数 SysReserve および SysMap の修正に焦点を当てています。

コミット

commit 3b9702c9c8a7e69e5af9b6dafd2932228d6b3210
Author: Joel Sing <jsing@google.com>
Date:   Sat Mar 23 02:17:01 2013 +1100

    runtime: correct return value checks for mmap on darwin/freebsd

    On Darwin and FreeBSD, the mmap syscall return value is returned
    unmodified. This means that the return value will either be a
    valid address or a positive error number.

    Also check return value from mmap in SysReserve - the callers of
    SysReserve expect nil to be returned if the allocation failed.

    R=golang-dev, rsc
    CC=golang-dev
    https://golang.org/cl/7871043

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

https://github.com/golang/go/commit/3b9702c9c8a7e69e5af9b6dafd2932228d6b3210

元コミット内容

GoランタイムがDarwinおよびFreeBSD上で mmap システムコールを使用する際の戻り値のチェックを修正します。これらのOSでは、mmap はエラー時に負の値ではなく、正のエラー番号を直接返します。また、SysReserve 関数内で mmap の戻り値を適切にチェックし、メモリ確保が失敗した場合には nil を返すようにします。これは SysReserve の呼び出し元が期待する動作です。

変更の背景

Goランタイムは、プログラムのメモリを管理するためにオペレーティングシステムが提供するメモリマッピング機能(主に mmap システムコール)を利用しています。mmap は、ファイルや匿名メモリ領域をプロセスのアドレス空間にマッピングするために使用されます。

一般的なUnix系システム(Linuxなど)では、mmap システムコールが失敗した場合、エラーを示すために MAP_FAILED という特殊な値(通常は (void*)-1)を返します。そして、実際のエラーコードは errno グローバル変数に設定されます。

しかし、Darwin (macOS) や FreeBSD といったBSD系のシステムでは、mmap の挙動が異なります。これらのシステムでは、mmap が失敗した場合、MAP_FAILED のような特殊な値を返すのではなく、直接エラーコード(正の整数)をポインタ型にキャストして返します。例えば、メモリ不足のエラー ENOMEM (通常は12) が発生した場合、mmap(void*)12 のような値を返します。

Goランタイムの既存の実装では、このBSD系の mmap の挙動が考慮されておらず、mmap の戻り値が (void*)-ENOMEM と比較されていました。このため、mmap が実際にエラーを返しても、Goランタイムがそれを正しく認識できず、予期せぬ動作やクラッシュを引き起こす可能性がありました。

また、SysReserve 関数は、Goランタイムが将来使用する可能性のあるアドレス空間を予約するために mmapPROT_NONE (アクセス不可) で呼び出します。この関数は、予約が成功した場合は予約されたアドレスを、失敗した場合は nil を返すことが期待されています。しかし、mmap の戻り値が適切にチェックされていなかったため、SysReservenil 以外の不正な値を返す可能性がありました。

このコミットは、これらの問題を修正し、GoランタイムがDarwinおよびFreeBSD上でより堅牢にメモリを管理できるようにすることを目的としています。

前提知識の解説

mmap システムコール

mmap は、Unix系オペレーティングシステムで利用可能なシステムコールで、プロセスのアドレス空間にメモリ領域をマッピングするために使用されます。主な用途は以下の通りです。

  • ファイルマッピング: ファイルの内容を直接メモリにマッピングし、ファイルI/Oを効率化します。
  • 匿名メモリマッピング: ファイルに関連付けられていないメモリ領域を確保します。これは、ヒープメモリやスタック領域の確保、共有メモリの実装などに利用されます。Goランタイムのメモリ管理では、主に匿名メモリマッピングが使用されます。

mmap の基本的なシグネチャは以下のようになります(C言語の例):

void *mmap(void *addr, size_t len, int prot, int flags, int fd, off_t offset);
  • addr: マッピングを配置する推奨アドレス。NULL の場合、カーネルが適切なアドレスを選択します。
  • len: マッピングするバイト数。
  • prot: メモリ領域の保護(読み取り、書き込み、実行など)。例: PROT_READ, PROT_WRITE, PROT_NONE
  • flags: マッピングの動作を制御するフラグ。例: MAP_ANON (匿名マッピング), MAP_PRIVATE (プライベートコピー), MAP_FIXED (指定されたアドレスに厳密にマッピング)。
  • fd: マッピングするファイルのファイルディスクリプタ(匿名マッピングの場合は -1)。
  • offset: ファイル内のオフセット。

戻り値の挙動:

  • 成功時: マッピングされたメモリ領域の開始アドレスを返します。
  • 失敗時:
    • Linuxなど: MAP_FAILED (通常 (void*)-1) を返し、errno にエラーコードを設定します。
    • Darwin/FreeBSD: エラーコード(正の整数)を直接ポインタ型にキャストして返します。例えば、ENOMEM (12) の場合、(void*)12 を返します。

PROT_NONE

mmapprot 引数に指定できるフラグの一つで、マッピングされたメモリ領域へのアクセスを一切許可しないことを意味します。これは、アドレス空間を予約するだけで、実際にメモリを使用する準備ができていない場合によく使用されます。後で mprotect などを使って保護レベルを変更できます。

MAP_ANONMAP_PRIVATE

  • MAP_ANON: ファイルに関連付けられていない匿名メモリ領域を作成します。これは、ヒープやスタックなどのプログラムの動的メモリ割り当てに使用されます。
  • MAP_PRIVATE: マッピングがプライベートであることを示します。つまり、マッピングされたメモリへの変更は、そのプロセスからのみ見え、基になるファイルや他のプロセスには影響しません。

Goランタイムのメモリ管理関数

Goランタイムは、OSのメモリ管理機能を抽象化し、Goプログラムが効率的にメモリを使用できるように独自のメモリ管理層を持っています。このコミットで関連する関数は以下の通りです。

  • runtime·SysReserve(void *v, uintptr n):
    • 指定されたアドレス v から n バイトのアドレス空間を予約します。
    • この関数は、実際にメモリをコミット(物理メモリを割り当てる)するのではなく、単にアドレス空間を確保するだけです。
    • 予約が成功した場合は予約されたアドレスを、失敗した場合は nil を返すことが期待されます。
    • 内部的には mmapPROT_NONE フラグで呼び出します。
  • runtime·SysMap(void *v, uintptr n):
    • SysReserve で予約されたアドレス空間 v から n バイトを実際に使用可能にします(コミットします)。
    • この関数は、メモリ領域を読み書き可能にするために mmapPROT_READ|PROT_WRITE フラグで呼び出します。
    • 通常、MAP_FIXED フラグを使用して、指定されたアドレスに厳密にマッピングしようとします。

nil ポインタ

GoランタイムのCコードでは、nil はC言語の NULL ポインタに相当します。メモリ割り当て関数が失敗した場合に nil を返すのは一般的な慣習です。

技術的詳細

このコミットの核心は、mmap システムコールの戻り値の解釈に関するOS間の差異を正しく処理することです。

Darwin/FreeBSDにおける mmap の戻り値

DarwinとFreeBSDでは、mmap がエラーを返した場合、その戻り値は負の MAP_FAILED (通常 (void*)-1) ではなく、正のエラー番号を直接ポインタ型にキャストした値となります。例えば、メモリ不足を示す ENOMEM (エラーコード12) が発生した場合、mmap(void*)12 を返します。

従来のGoランタイムのコードでは、mmap の戻り値を (void*)-ENOMEM と比較していました。これはLinuxなどのシステムでは正しいかもしれませんが、Darwin/FreeBSDでは (void*)12(void*)-12 は異なる値であるため、エラーが正しく検出されませんでした。

SysReserve の修正

SysReserve 関数は、アドレス空間の予約に失敗した場合に nil を返す必要があります。従来のコードでは、mmap の戻り値をそのまま返していました。

// Before
void*
runtime·SysReserve(void *v, uintptr n)
{
	return runtime·mmap(v, n, PROT_NONE, MAP_ANON|MAP_PRIVATE, -1, 0);
}

この修正では、mmap の戻り値 p を受け取り、p(void*)4096 (ページサイズ) 未満である場合に nil を返すように変更されています。これは、有効なメモリ開始アドレスは通常、ページ境界にアラインされ、非常に小さい正の整数(エラーコード)とは区別できるという仮定に基づいています。4096 は一般的なメモリページサイズであり、これより小さい値は有効なアドレスではなく、エラーコードである可能性が高いと判断されます。

// After
void*
runtime·SysReserve(void *v, uintptr n)
{
	void *p;

	p = runtime·mmap(v, n, PROT_NONE, MAP_ANON|MAP_PRIVATE, -1, 0);
	if(p < (void*)4096) // ページサイズ未満であればエラーと判断
		return nil;
	return p;
}

SysMap の修正

SysMap 関数では、mmap がメモリ不足エラー ENOMEM を返した場合のチェックが修正されています。

// Before
if(p == (void*)-ENOMEM)

// After
if(p == (void*)ENOMEM)

これにより、Darwin/FreeBSDで mmap(void*)ENOMEM を返した場合に、Goランタイムが正しく「out of memory」エラーをスローできるようになります。

ENOMEM の定義

FreeBSDの src/pkg/runtime/mem_freebsd.c では、ENOMEM がローカルで定義されていました。

// Before
enum
{
	ENOMEM = 12,
};

この定義は、SysReserve の修正に伴い、SysAlloc の直後に移動され、より適切なスコープに配置されています。

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

src/pkg/runtime/mem_darwin.c

--- a/src/pkg/runtime/mem_darwin.c
+++ b/src/pkg/runtime/mem_darwin.c
@@ -37,7 +37,12 @@ runtime·SysFree(void *v, uintptr n)
 void*
 runtime·SysReserve(void *v, uintptr n)
 {
-	return runtime·mmap(v, n, PROT_NONE, MAP_ANON|MAP_PRIVATE, -1, 0);
+	void *p;
+
+	p = runtime·mmap(v, n, PROT_NONE, MAP_ANON|MAP_PRIVATE, -1, 0);
+	if(p < (void*)4096)
+		return nil;
+	return p;
 }
 
 enum
@@ -52,7 +57,7 @@ runtime·SysMap(void *v, uintptr n)
 	
 	mstats.sys += n;
 	p = runtime·mmap(v, n, PROT_READ|PROT_WRITE, MAP_ANON|MAP_FIXED|MAP_PRIVATE, -1, 0);
-	if(p == (void*)-ENOMEM)
+	if(p == (void*)ENOMEM)
 		runtime·throw("runtime: out of memory");
 	if(p != v)
 		runtime·throw("runtime: cannot map pages in arena address space");

src/pkg/runtime/mem_freebsd.c

--- a/src/pkg/runtime/mem_freebsd.c
+++ b/src/pkg/runtime/mem_freebsd.c
@@ -8,6 +8,11 @@
 #include "os_GOOS.h"\n #include "malloc.h"\n \n+enum\n+{\n+\tENOMEM = 12,\n+};\n+\n void*\n runtime·SysAlloc(uintptr n)\n {\
@@ -36,20 +41,20 @@ runtime·SysFree(void *v, uintptr n)
 void*
 runtime·SysReserve(void *v, uintptr n)
 {
+\tvoid *p;\n+\n \t// On 64-bit, people with ulimit -v set complain if we reserve too\n \t// much address space.  Instead, assume that the reservation is okay\n \t// and check the assumption in SysMap.\n \tif(sizeof(void*) == 8)\n \t\treturn v;\n \t\n-\treturn runtime·mmap(v, n, PROT_NONE, MAP_ANON|MAP_PRIVATE, -1, 0);\n+\tp = runtime·mmap(v, n, PROT_NONE, MAP_ANON|MAP_PRIVATE, -1, 0);\n+\tif(p < (void*)4096)\n+\t\treturn nil;\n+\treturn p;\n }\n \n-enum\n-{\n-\tENOMEM = 12,\n-};\n-\n void
 runtime·SysMap(void *v, uintptr n)
 {
@@ -60,7 +65,7 @@ runtime·SysMap(void *v, uintptr n)
 	// On 64-bit, we don\'t actually have v reserved, so tread carefully.\n \tif(sizeof(void*) == 8) {\n \t\tp = runtime·mmap(v, n, PROT_READ|PROT_WRITE, MAP_ANON|MAP_PRIVATE, -1, 0);\n-\t\tif(p == (void*)-ENOMEM)\n+\t\tif(p == (void*)ENOMEM)\n \t\t\truntime·throw(\"runtime: out of memory\");\n \t\tif(p != v) {\n \t\t\truntime·printf(\"runtime: address space conflict: map(%p) = %p\\n\", v, p);\n@@ -70,7 +75,7 @@ runtime·SysMap(void *v, uintptr n)
 \t}\n \n \tp = runtime·mmap(v, n, PROT_READ|PROT_WRITE, MAP_ANON|MAP_FIXED|MAP_PRIVATE, -1, 0);\n-\tif(p == (void*)-ENOMEM)\n+\tif(p == (void*)ENOMEM)\n \t\truntime·throw(\"runtime: out of memory\");\n \tif(p != v)\n \t\truntime·throw(\"runtime: cannot map pages in arena address space\");\

コアとなるコードの解説

runtime·SysReserve の変更

  • 変更前: runtime·mmap の戻り値を直接返していました。
  • 変更後:
    1. runtime·mmap の戻り値を一時変数 p に格納します。
    2. if(p < (void*)4096) という条件を追加します。これは、mmap がエラーコード(正の小さい整数)を返した場合に true となります。有効なメモリ開始アドレスは通常、ページサイズ(4096バイト)以上の値になるため、このチェックでエラーと正常なアドレスを区別します。
    3. 条件が true の場合、nil を返します。これにより、SysReserve の呼び出し元はメモリ予約の失敗を正しく認識できます。
    4. 条件が false の場合、p (有効なアドレス) を返します。

runtime·SysMap の変更

  • 変更前: mmap の戻り値 p(void*)-ENOMEM と等しいかどうかをチェックしていました。
  • 変更後: mmap の戻り値 p(void*)ENOMEM と等しいかどうかをチェックするように変更されました。これにより、Darwin/FreeBSDで mmap が正のエラーコード ENOMEM を返した場合に、Goランタイムが適切に「out of memory」エラーをスローできるようになります。

ENOMEM の定義の移動 (mem_freebsd.c のみ)

  • mem_freebsd.c では、ENOMEMenum 定義が runtime·SysReserve 関数の直前から runtime·SysAlloc 関数の直後に移動されました。これは機能的な変更ではなく、コードの構造と可読性を改善するためのものです。

これらの変更により、GoランタイムはDarwinおよびFreeBSD環境下で mmap システムコールのエラーを正確に検出し、メモリ管理の堅牢性が向上しました。

関連リンク

参考にした情報源リンク

  • コミットメッセージと差分 (./commit_data/15893.txt の内容)
  • mmap システムコールの一般的なドキュメント (Linux, FreeBSD, macOS の man ページ)
  • Go言語のランタイムに関する一般的な知識とメモリ管理の概念