[インデックス 12205] ファイルの概要
このコミットは、Goランタイムのメモリ管理に関する重要な改善を含んでいます。特に、仮想アドレス空間の制限に適合するようにアリーナ(ヒープ領域)のサイズを調整する機能が導入されました。これにより、ulimit -v
などの仮想メモリ制限が厳しく設定されているシステムや、32ビットアーキテクチャのシステムでGoプログラムが正常に動作しない問題が解決されます。
コミット
commit 102274a30e5d2df4d13d5fad50c484f78904236a
Author: Russ Cox <rsc@golang.org>
Date: Fri Feb 24 15:28:51 2012 -0500
runtime: size arena to fit in virtual address space limit
For Brad.
Now FreeBSD/386 binaries run on nearlyfreespeech.net.
Fixes #2302.
R=golang-dev, r
CC=golang-dev
https://golang.org/cl/5700060
GitHub上でのコミットページへのリンク
https://github.com/golang/go/commit/102274a30e5d2df4d13d5fad50c484f78904236a
元コミット内容
Goランタイムが、仮想アドレス空間の制限に合わせてアリーナのサイズを調整するように変更されました。この変更により、Brad氏のために、nearlyfreespeech.net上でFreeBSD/386バイナリが実行できるようになりました。これはIssue #2302を修正するものです。
変更の背景
Goのランタイムは、プログラムのヒープとして使用するために、起動時に大きな連続した仮想メモリ領域(アリーナ)を予約しようとします。64ビットシステムでは16GB、32ビットシステムでは512MBといった大きなサイズが初期設定されていました。
しかし、一部のシステムでは、プロセスが利用できる仮想メモリの総量に制限が設けられています。これは通常、ulimit -v
コマンドなどで設定されるリソース制限(RLIMIT_AS
)によって制御されます。このような環境下で、Goランタイムがデフォルトで予約しようとするアリーナのサイズが、システムのリソース制限を超過してしまうと、メモリ予約に失敗し、Goプログラムが起動できないという問題が発生していました。
特に、32ビットシステムではアドレス空間自体が4GBに制限されており、その中で512MBもの連続した領域を確保することは、他のメモリ使用量(コード、データ、スタックなど)によっては困難な場合がありました。コミットメッセージにある「FreeBSD/386 binaries run on nearlyfreespeech.net」という記述は、この問題がFreeBSDの32ビット環境で顕著に発生していたことを示唆しています。
このコミットは、Goプログラムがより多様な環境、特に仮想メモリ制限が厳しい環境や32ビットシステムで安定して動作できるようにするために、ランタイムがシステムのリソース制限を考慮してアリーナのサイズを動的に調整するメカニズムを導入しました。
前提知識の解説
仮想アドレス空間 (Virtual Address Space)
仮想アドレス空間とは、各プロセスが利用できるメモリのアドレス範囲を抽象化したものです。オペレーティングシステム(OS)は、物理メモリとストレージ(スワップ領域)を組み合わせて、各プロセスに独立した仮想アドレス空間を提供します。これにより、プロセスは他のプロセスのメモリに干渉することなく、連続した大きなメモリ領域を利用できるかのように振る舞います。
リソース制限 (Resource Limits)
Unix系OSでは、ulimit
コマンドやsetrlimit
/getrlimit
システムコールを通じて、プロセスが利用できるシステムリソースに制限を設けることができます。これには、CPU時間、ファイルサイズ、オープンできるファイルディスクリプタの数、そして仮想メモリのサイズなどが含まれます。
RLIMIT_AS
(Address Space Limit): プロセスが利用できる仮想メモリの最大サイズをバイト単位で指定するリソース制限です。この制限を超えて仮想メモリを確保しようとすると、通常は失敗します。getrlimit
システムコール: プロセスに設定されている特定のリソースの現在の制限値(rlim_cur
)と最大制限値(rlim_max
)を取得するために使用されるシステムコールです。
Goランタイムのメモリ管理 (Go Runtime Memory Management)
Goのランタイムは、独自のメモリ管理システムを持っています。これは、OSから大きなメモリブロックをまとめて取得し(アリーナ)、その内部でGoのガベージコレクタが管理するヒープ領域を構築します。このアリーナは、Goプログラムがオブジェクトを割り当てるための連続した空間を提供します。初期のアリーナサイズは、Goプログラムが効率的にメモリを割り当てられるように、比較的大きく設定されています。
32ビットと64ビットアーキテクチャのメモリ制限
- 32ビットシステム: 仮想アドレス空間は最大で2^32バイト、つまり4GBに制限されます。この4GBは、カーネルとユーザープロセスで共有されるため、ユーザープロセスが利用できるのは通常2GBまたは3GB程度です。この限られた空間内で、Goランタイムが512MBもの連続したアリーナを確保しようとすると、他のメモリ使用量との競合により失敗する可能性が高まります。
- 64ビットシステム: 仮想アドレス空間は2^64バイトと非常に広大であり、事実上無制限に近いメモリを扱うことができます。しかし、
ulimit -v
のようなリソース制限が設定されている場合は、64ビットシステムでも仮想メモリの確保が問題となることがあります。
技術的詳細
このコミットの主要な目的は、Goランタイムがシステムのリソース制限、特に仮想アドレス空間の制限(RLIMIT_AS
)を認識し、それに基づいてヒープアリーナのサイズを動的に調整することです。
変更の核となるのは、以下のメカニズムです。
-
runtime·memlimit()
関数の導入:- この関数は、現在のプロセスの仮想アドレス空間の利用可能な上限を返します。
- FreeBSDおよびLinux環境では、
getrlimit(RLIMIT_AS, &rl)
システムコールを呼び出して、RLIMIT_AS
の現在の制限値を取得します。 - 取得した制限値から、バイナリのサイズとスレッドスタック用に確保される推定メモリ量(約64MB)を差し引くことで、ヒープアリーナとして利用可能な残りの仮想メモリ量を算出します。
- もし残りのメモリが16MB未満であれば、実質的に制限がないものとして
0
を返します。 - Darwin (macOS) やNetBSD、OpenBSD、Plan 9、Windowsなどの他のOSでは、
ulimit -v
が強制されない、または同様のメカニズムが存在しないため、この関数は単純に0
を返します(制限なしと見なす)。
-
malloc.goc
でのアリーナサイズ調整:runtime·mallocinit()
関数内で、runtime·memlimit()
を呼び出して利用可能なメモリ上限(limit
)を取得します。- 64ビットシステムの場合:
- 従来のコードでは、
sizeof(void*) == 8
(64ビット)の場合、アリーナサイズは16GBに固定されていました。 - 変更後、
limit == 0 || limit > (1<<30)
(制限がないか、制限が1GBより大きい場合)という条件が追加されました。これにより、もしlimit
が設定されており、それが1GB以下である場合は、従来の16GB固定のロジックがスキップされ、後述の32ビットシステム向けの調整ロジックが適用される可能性があります。これは、64ビットシステムでもulimit -v
が厳しく設定されている場合に、アリーナサイズを適切に調整するための重要な変更です。
- 従来のコードでは、
- 32ビットシステムの場合:
MaxArena32
(512MB)とbitmap_size
に基づいてアリーナサイズが計算されます。if(limit > 0 && arena_size+bitmap_size > limit)
という条件が追加されました。これは、limit
が設定されており、かつ計算されたアリーナとビットマップの合計サイズがlimit
を超過する場合に発動します。- この条件が真の場合、
bitmap_size
とarena_size
がlimit
に合わせて再計算されます。具体的には、bitmap_size = (limit / 9) & ~((1<<PageShift) - 1);
およびarena_size = bitmap_size * 8;
という計算が行われます。この計算は、利用可能な仮想アドレス空間の約1/9をビットマップに、残りをアリーナに割り当てることで、全体がlimit
内に収まるように調整します。PageShift
はメモリページのサイズに関連する定数で、アライメントを保証します。
-
OS固有のシステムコールラッパーの追加:
src/pkg/runtime/os_freebsd.h
とsrc/pkg/runtime/os_linux.h
に、RLIMIT_AS
の定義とRlimit
構造体の定義、そしてruntime·getrlimit
関数のプロトタイプが追加されました。src/pkg/runtime/sys_freebsd_386.s
、src/pkg/runtime/sys_freebsd_amd64.s
、src/pkg/runtime/sys_linux_386.s
、src/pkg/runtime/sys_linux_amd64.s
、src/pkg/runtime/sys_linux_arm.s
といったアセンブリファイルに、それぞれのOSとアーキテクチャに対応するgetrlimit
システムコールを呼び出すためのラッパー関数が追加されました。これにより、GoランタイムからOSのgetrlimit
機能を利用できるようになります。
これらの変更により、Goランタイムは起動時にシステムの仮想メモリ制限を動的に検出し、その制限内でヒープアリーナを適切にサイズ調整できるようになりました。これにより、ulimit -v
が設定された環境や、32ビットシステムでのメモリ予約失敗による起動不能問題が解消されます。
コアとなるコードの変更箇所
src/pkg/runtime/malloc.goc
--- a/src/pkg/runtime/malloc.goc
+++ b/src/pkg/runtime/malloc.goc
@@ -262,6 +262,7 @@ runtime·mallocinit(void)
uintptr arena_size, bitmap_size;
extern byte end[];
byte *want;
+ uintptr limit;
p = nil;
arena_size = 0;
@@ -274,10 +275,12 @@ runtime·mallocinit(void)
runtime·InitSizes();
+ limit = runtime·memlimit();
+
// Set up the allocation arena, a contiguous area of memory where
// allocated data will be found. The arena begins with a bitmap large
// enough to hold 4 bits per allocated word.
- if(sizeof(void*) == 8) {
+ if(sizeof(void*) == 8 && (limit == 0 || limit > (1<<30))) {
// On a 64-bit machine, allocate from a single contiguous reservation.
// 16 GB should be big enough for now.
//
@@ -326,6 +329,10 @@ runtime·mallocinit(void)
// of address space, which is probably too much in a 32-bit world.
bitmap_size = MaxArena32 / (sizeof(void*)*8/4);
arena_size = 512<<20;
+ if(limit > 0 && arena_size+bitmap_size > limit) {
+ bitmap_size = (limit / 9) & ~((1<<PageShift) - 1);
+ arena_size = bitmap_size * 8;
+ }
// SysReserve treats the address we ask for, end, as a hint,
// not as an absolute requirement. If we ask for the end
@@ -340,6 +347,8 @@ runtime·mallocinit(void)
p = runtime·SysReserve(want, bitmap_size + arena_size);
if(p == nil)
runtime·throw("runtime: cannot reserve arena virtual address space");
+ if((uintptr)p & (((uintptr)1<<PageShift)-1))
+ runtime·printf("runtime: SysReserve returned unaligned address %p; asked for %p", p, bitmap_size+arena_size);
}
if((uintptr)p & (((uintptr)1<<PageShift)-1))
runtime·throw("runtime: SysReserve returned unaligned address");
src/pkg/runtime/runtime.h
--- a/src/pkg/runtime/runtime.h
+++ b/src/pkg/runtime/runtime.h
@@ -729,3 +729,4 @@ bool runtime·showframe(Func*);
void runtime·ifaceE2I(struct InterfaceType*, Eface, Iface*);
+uintptr runtime·memlimit(void);
src/pkg/runtime/thread_freebsd.c
(および thread_linux.c
も同様)
--- a/src/pkg/runtime/thread_freebsd.c
+++ b/src/pkg/runtime/thread_freebsd.c
@@ -161,3 +161,31 @@ runtime·sigpanic(void)
}
runtime·panicstring(runtime·sigtab[g->sig].name);
}
+
+uintptr
+runtime·memlimit(void)
+{
+ Rlimit rl;
+ extern byte text[], end[];
+ uintptr used;
+
+ if(runtime·getrlimit(RLIMIT_AS, &rl) != 0)
+ return 0;
+ if(rl.rlim_cur >= 0x7fffffff)
+ return 0;
+
+ // Estimate our VM footprint excluding the heap.
+ // Not an exact science: use size of binary plus
+ // some room for thread stacks.
+ used = end - text + (64<<20);
+ if(used >= rl.rlim_cur)
+ return 0;
+
+ // If there's not at least 16 MB left, we're probably
+ // not going to be able to do much. Treat as no limit.
+ rl.rlim_cur -= used;
+ if(rl.rlim_cur < (16<<20))
+ return 0;
+
+ return rl.rlim_cur - used;
+}
コアとなるコードの解説
malloc.goc
の変更点
limit
変数が追加され、runtime·memlimit()
から取得した仮想アドレス空間の制限値が格納されます。- 64ビットシステムのアリーナサイズ決定ロジックに
limit == 0 || limit > (1<<30)
という条件が追加されました。これにより、もし仮想メモリ制限が1GB以下に設定されている場合、Goランタイムはデフォルトの16GBアリーナを予約しようとせず、より小さなアリーナサイズを検討するようになります。 - 32ビットシステムのアリーナサイズ決定ロジックに、
if(limit > 0 && arena_size+bitmap_size > limit)
という条件が追加されました。これは、システムに仮想メモリ制限があり、かつデフォルトのアリーナとビットマップの合計サイズがその制限を超える場合に発動します。- この条件が真の場合、
bitmap_size
とarena_size
がlimit
に基づいて再計算されます。bitmap_size = (limit / 9) & ~((1<<PageShift) - 1);
は、利用可能な制限の約1/9をビットマップに割り当て、ページ境界にアライメントします。arena_size = bitmap_size * 8;
は、ビットマップサイズに基づいてアリーナサイズを決定します。これにより、アリーナとビットマップの合計サイズが仮想メモリ制限内に収まるように調整されます。
- この条件が真の場合、
SysReserve
が返すアドレスがページ境界にアライメントされているかどうかのチェックが追加されました。
runtime.h
の変更点
uintptr runtime·memlimit(void);
という関数プロトタイプが追加されました。これは、GoランタイムがOSから仮想メモリ制限を取得するためのインターフェースを定義しています。
thread_freebsd.c
(および thread_linux.c
)の変更点
runtime·memlimit()
関数の実装が追加されました。runtime·getrlimit(RLIMIT_AS, &rl)
を呼び出し、RLIMIT_AS
(アドレス空間制限)の現在の値を取得します。rl.rlim_cur
が0x7fffffff
(32ビット符号付き整数の最大値)以上の場合、実質的に制限がないと見なし0
を返します。これは、getrlimit
が返す値が符号付き32ビット整数で表現される場合があるため、非常に大きな値(無制限に近い値)を適切に処理するためです。used = end - text + (64<<20);
で、バイナリのサイズとスレッドスタック用の推定メモリ量(64MB)を計算し、これを既に消費されているメモリ量と見なします。if(used >= rl.rlim_cur)
の場合、利用可能なメモリが既に消費されているメモリ以下であれば、0
を返します。rl.rlim_cur -= used;
で、総制限から消費済みメモリを差し引きます。if(rl.rlim_cur < (16<<20))
の場合、残りのメモリが16MB未満であれば、実質的にアリーナを確保するのに十分なスペースがないと判断し、0
を返します。- 最終的に、アリーナとして利用可能な残りの仮想メモリ量を返します。
これらの変更により、GoランタイムはOSの仮想メモリ制限を動的に検出し、その制限内でヒープアリーナを適切にサイズ調整できるようになり、特にリソース制限が厳しい環境や32ビットシステムでのGoプログラムの安定性が向上しました。
関連リンク
- Go Issue #2302: https://github.com/golang/go/issues/2302
- Go CL 5700060: https://golang.org/cl/5700060
参考にした情報源リンク
getrlimit(2)
man page (Linux): https://man7.org/linux/man-pages/man2/getrlimit.2.htmlulimit
command: https://man7.org/linux/man-pages/man1/ulimit.1p.html- Goのメモリ管理に関する一般的な情報 (例: Goのドキュメントやブログ記事)
- 32ビットと64ビットアーキテクチャのメモリモデルに関する情報