[インデックス 18860] ファイルの概要
このコミットは、Goランタイムにおけるメモリ管理、特にefence
(Electric Fence)の挙動を改善するものです。efence
は、メモリの不正アクセス(バッファオーバーラン、解放済みメモリへのアクセスなど)を検出するためのデバッグツールであり、Goランタイムが提供するメモリデバッグ機能の一部として利用されます。この変更は、解放されたメモリブロックを「未使用」としてマークすることで、特にamd64
アーキテクチャにおいて、プロセスが大量のヒープメモリ(128GB)を消費してもシステムがクラッシュしないようにすることを目的としています。
コミット
commit c115cda22c82e219654056f6864e9819b922febc
Author: Dmitriy Vyukov <dvyukov@google.com>
Date: Thu Mar 13 19:04:00 2014 +0400
runtime: improve efence
Mark free memory blocks as unused.
On amd64 it allows the process to eat all 128 GB of heap
without killing the machine.
LGTM=rsc
R=rsc
CC=golang-codereviews
https://golang.org/cl/74070043
GitHub上でのコミットページへのリンク
https://github.com/golang/go/commit/c115cda22c82e219654056f6864e9819b922febc
元コミット内容
runtime: improve efence
Mark free memory blocks as unused.
On amd64 it allows the process to eat all 128 GB of heap
without killing the machine.
変更の背景
このコミットの背景には、Goプログラムのデバッグ時におけるメモリ使用量の問題がありました。特にefence
(Electric Fence)というメモリデバッグツールを使用している場合、解放されたメモリ領域が適切に処理されないと、システムが大量の仮想メモリを消費し、結果としてシステム全体のパフォーマンス低下やクラッシュを引き起こす可能性がありました。
従来のefence
の実装では、解放されたメモリブロックを単にアクセス不可(PROT_NONE
)に設定するだけでした。しかし、これは仮想メモリ空間を占有し続けるため、特にamd64
のような64ビットシステムで大量のメモリを扱う場合に問題となります。Goのランタイムは、プログラムが要求するメモリをOSから確保し、不要になったメモリをOSに返却する(または再利用のために保持する)役割を担っています。efence
が有効な場合、解放されたメモリはOSに返却されず、アクセス不可の状態でプロセスに紐付けられたままになります。これにより、プロセスが使用可能な仮想アドレス空間を使い果たし、システムリソースを過剰に消費する事態が発生していました。
この問題は、特にメモリリークのデバッグや、メモリを大量に確保・解放するアプリケーションのテストにおいて顕著でした。開発者は、メモリ関連の問題を特定するためにefence
のようなツールを必要としますが、そのツール自体がシステムを不安定にするというジレンマに直面していました。このコミットは、この問題を解決し、efence
をより実用的なデバッグツールにするための改善です。
前提知識の解説
Goランタイムとメモリ管理
Go言語は、独自のランタイムシステムを持ち、メモリ管理(ガベージコレクションを含む)を自動的に行います。プログラムがメモリを要求すると、GoランタイムはOSからメモリを確保し、それをヒープとして管理します。不要になったメモリはガベージコレクタによって回収され、再利用可能な状態になります。
mmap
システムコール
mmap
(memory map)は、Unix系OSで利用されるシステムコールで、ファイルやデバイス、または匿名メモリ領域をプロセスの仮想アドレス空間にマッピングするために使用されます。メモリを確保する際や、メモリ領域の保護属性を変更する際に利用されます。
PROT_NONE
: メモリ領域へのアクセスを許可しない保護属性です。この属性が設定されたメモリ領域にアクセスしようとすると、セグメンテーション違反(segmentation fault)が発生します。efence
のようなツールでは、解放されたメモリ領域をこの属性に設定することで、解放済みメモリへのアクセスを検出します。MAP_ANON
: ファイルではなく、匿名メモリ領域をマッピングすることを示します。MAP_PRIVATE
: マッピングがプライベートであり、他のプロセスと共有されないことを示します。MAP_FIXED
: 指定されたアドレスにマッピングを配置しようとします。このフラグがない場合、OSは任意のアドレスにマッピングを配置できます。
efence
(Electric Fence)
efence
は、メモリの不正アクセスを検出するためのデバッグライブラリです。主にC/C++プログラムのデバッグで利用されますが、Goランタイムのデバッグビルドでも同様の目的で利用されます。efence
が有効な場合、malloc
で確保されたメモリブロックの直後や直前にアクセス不可のページを配置することで、バッファオーバーランやバッファアンダーランを検出します。また、free
されたメモリブロックをアクセス不可に設定することで、解放済みメモリへのアクセス(use-after-free)を検出します。
仮想メモリと物理メモリ
- 物理メモリ: 実際にコンピュータに搭載されているRAMのことです。
- 仮想メモリ: OSが提供する抽象化されたメモリ空間です。各プロセスは独自の仮想アドレス空間を持ち、物理メモリのどこにデータが配置されているかを意識する必要がありません。OSは、仮想アドレスと物理アドレスのマッピングを管理し、必要に応じてディスク上のスワップ領域を利用して物理メモリを補います。
PROT_NONE
で保護されたメモリ領域は、物理メモリを消費しない場合がありますが、仮想アドレス空間は占有し続けます。大量の仮想アドレス空間が占有されると、OSのページテーブル管理にオーバーヘッドが生じたり、プロセスが利用可能な仮想アドレス空間を使い果たしたりする可能性があります。
SysUnused
(Windows固有)
Windows環境では、Unix系OSのmmap
に相当するVirtualAlloc
やVirtualProtect
といったAPIが利用されます。SysUnused
は、GoランタイムがWindows上でメモリを未使用としてマークするための内部関数であり、通常はVirtualFree
関数とMEM_DECOMMIT
フラグを組み合わせて、物理メモリのコミットを解除し、仮想アドレス空間を解放せずにメモリを未使用状態にするために使用されます。
技術的詳細
このコミットの主要な変更点は、Goランタイムがefence
モードでメモリを解放する際の挙動です。以前は、解放されたメモリブロックに対してmmap
システムコールをPROT_NONE
フラグのみで呼び出していました。これは、そのメモリ領域へのアクセスを禁止し、不正アクセスを検出するためのものでした。しかし、この方法では仮想アドレス空間が解放されず、特に大量のメモリを確保・解放するアプリケーションにおいて、仮想メモリの枯渇やシステムリソースの過剰消費につながる可能性がありました。
新しい実装では、PROT_NONE
に加えて、MAP_ANON|MAP_PRIVATE|MAP_FIXED
フラグをmmap
システムコールに渡すように変更されています。
MAP_ANON
: このフラグは、ファイルではなく匿名メモリ領域をマッピングすることを示します。これにより、メモリ領域がファイルシステム上の何かに紐付けられることなく、純粋なメモリとして扱われます。MAP_PRIVATE
: このフラグは、マッピングがプライベートであり、他のプロセスと共有されないことを示します。これにより、このメモリ領域への変更が他のプロセスに影響を与えることはありません。MAP_FIXED
: このフラグは、mmap
が指定されたアドレス(v
)にメモリ領域を正確にマッピングしようとすることを意味します。これは、Goランタイムが特定の仮想アドレス範囲を管理している場合に重要です。
これらのフラグを組み合わせることで、mmap
は既存のメモリ領域を再マッピングし、その領域をアクセス不可(PROT_NONE
)にするだけでなく、その領域が匿名かつプライベートなメモリとして扱われることを保証します。これにより、OSは必要に応じてその仮想アドレス空間に対応する物理メモリを解放し、ページテーブルのエントリを最適化する機会を得ることができます。結果として、プロセスが大量の仮想メモリを占有し続けることを防ぎ、システムリソースの消費を抑えることができます。
Windows環境においては、mmap
の代わりにVirtualProtect
が使用されていましたが、このコミットではruntime·SysUnused
関数を呼び出すように変更されています。runtime·SysUnused
は、Windows固有のメモリ管理API(VirtualFree
with MEM_DECOMMIT
など)を利用して、メモリを未使用状態にマークし、物理メモリのコミットを解除することで、同様の効果を実現します。これにより、Windows上でもefence
使用時のメモリ消費が改善されます。
この変更により、efence
が有効な状態でGoプログラムを実行しても、解放されたメモリが仮想アドレス空間を過剰に占有することがなくなり、特にamd64
システムで128GBものヒープメモリを扱うような大規模なアプリケーションでも、システムが安定して動作するようになります。
コアとなるコードの変更箇所
このコミットは、Goランタイムの各OS固有のメモリ管理ファイルにおけるruntime·SysFault
関数の実装を変更しています。
具体的には、以下のファイルが変更されています。
src/pkg/runtime/mem_darwin.c
src/pkg/runtime/mem_dragonfly.c
src/pkg/runtime/mem_freebsd.c
src/pkg/runtime/mem_linux.c
src/pkg/runtime/mem_nacl.c
src/pkg/runtime/mem_netbsd.c
src/pkg/runtime/mem_openbsd.c
src/pkg/runtime/mem_solaris.c
src/pkg/runtime/mem_windows.c
各Unix系OSのファイルでは、runtime·SysFault
関数内のruntime·mmap
の呼び出しが変更されています。
--- a/src/pkg/runtime/mem_darwin.c
+++ b/src/pkg/runtime/mem_darwin.c
@@ -44,7 +44,7 @@ runtime·SysFree(void *v, uintptr n, uint64 *stat)
void
runtime·SysFault(void *v, uintptr n)
{
- runtime·mmap(v, n, PROT_NONE, 0, -1, 0);
+ runtime·mmap(v, n, PROT_NONE, MAP_ANON|MAP_PRIVATE|MAP_FIXED, -1, 0);
}
Windowsのファイルでは、runtime·SysFault
関数の実装がruntime·SysUnused
の呼び出しに置き換えられています。
--- a/src/pkg/runtime/mem_windows.c
+++ b/src/pkg/runtime/mem_windows.c
@@ -66,11 +66,8 @@ runtime·SysFree(void *v, uintptr n, uint64 *stat)
void
runtime·SysFault(void *v, uintptr n)
{
- uintptr r, old;
-
- r = (uintptr)runtime·stdcall(runtime·VirtualProtect, 4, v, n, (uintptr)PAGE_NOACCESS, &old);
- if(r == 0)
- runtime·throw("runtime: failed to protect pages");
+ // SysUnused makes the memory inaccessible and prevents its reuse
+ runtime·SysUnused(v, n);
}
コアとなるコードの解説
Unix系OS (mem_darwin.c
, mem_linux.c
など)
変更前:
runtime·mmap(v, n, PROT_NONE, 0, -1, 0);
この行は、アドレスv
からサイズn
のメモリ領域を、アクセス不可(PROT_NONE
)としてマッピングし直していました。0
の引数は、mmap
のflags
引数に何も指定しないことを意味し、デフォルトの挙動に依存していました。これにより、メモリ領域はアクセス不可になりますが、仮想アドレス空間は解放されず、プロセスに紐付けられたままでした。
変更後:
runtime·mmap(v, n, PROT_NONE, MAP_ANON|MAP_PRIVATE|MAP_FIXED, -1, 0);
変更後もPROT_NONE
は維持され、メモリ領域へのアクセス禁止は継続されます。しかし、flags
引数にMAP_ANON|MAP_PRIVATE|MAP_FIXED
が追加されました。
MAP_ANON
: このメモリ領域がファイルではなく、匿名メモリであることを明示します。MAP_PRIVATE
: このメモリ領域がプロセス固有であり、他のプロセスと共有されないことを明示します。MAP_FIXED
:mmap
が指定されたアドレスv
に正確にマッピングを配置しようとすることを明示します。
これらのフラグを組み合わせることで、OSは、このメモリ領域がもはや物理メモリを必要とせず、かつ他の用途に再利用可能であることをより明確に認識できるようになります。これにより、OSは物理メモリのコミットを解除し、ページテーブルのエントリを最適化するなど、より効率的なメモリ管理を行うことができます。結果として、プロセスが大量の仮想メモリを占有し続けることを防ぎ、システムリソースの消費を抑える効果があります。
Windows (mem_windows.c
)
変更前:
uintptr r, old;
r = (uintptr)runtime·stdcall(runtime·VirtualProtect, 4, v, n, (uintptr)PAGE_NOACCESS, &old);
if(r == 0)
runtime·throw("runtime: failed to protect pages");
Windowsでは、VirtualProtect
APIを使用してメモリ領域の保護属性を変更していました。PAGE_NOACCESS
は、Unix系OSのPROT_NONE
に相当し、メモリ領域へのアクセスを禁止します。しかし、これも仮想アドレス空間を解放するものではありませんでした。
変更後:
// SysUnused makes the memory inaccessible and prevents its reuse
runtime·SysUnused(v, n);
Windows版では、VirtualProtect
の直接呼び出しをruntime·SysUnused(v, n)
に置き換えています。runtime·SysUnused
は、Goランタイム内部で定義された関数で、Windowsのメモリ管理API(例えばVirtualFree
とMEM_DECOMMIT
フラグの組み合わせ)を利用して、指定されたメモリ領域を「未使用」としてマークし、物理メモリのコミットを解除します。これにより、Unix系OSでのmmap
の変更と同様に、仮想アドレス空間を占有しつつも物理メモリの消費を抑え、システムリソースの効率的な利用を促進します。コメントにあるように、「メモリをアクセス不可にし、その再利用を防ぐ」という目的は維持しつつ、より効率的な方法で実現しています。
関連リンク
- Go言語の公式ドキュメント: https://golang.org/doc/
- Goランタイムのソースコード: https://github.com/golang/go/tree/master/src/runtime
mmap
システムコールに関するmanページ (Linux):man mmap
VirtualProtect
関数 (Windows): https://learn.microsoft.com/en-us/windows/win32/api/memoryapi/nf-memoryapi-virtualprotect
参考にした情報源リンク
- Goのコミット履歴: https://github.com/golang/go/commits/master
- Go CL 74070043: https://golang.org/cl/74070043 (元のコミットに記載されているGo Code Reviewのリンク)
- Electric Fence (Wikipedia): https://en.wikipedia.org/wiki/Electric_Fence
mmap
に関する情報 (例: Linux man pages online): https://man7.org/linux/man-pages/man2/mmap.2.html- Windowsメモリ管理に関する情報 (例: Microsoft Learn): https://learn.microsoft.com/en-us/windows/win32/memory/memory-management
- Goのメモリ管理に関するブログ記事や解説(一般的な情報源)
- "Go's Memory Allocator" by Rick Hudson: https://go.dev/blog/go-memory-allocator (これは一般的なGoのメモリ管理に関する記事であり、直接このコミットに言及しているわけではありませんが、背景知識として有用です。)
- Goのガベージコレクションに関する情報(一般的な情報源)
- "Go's new GC: less latency, more throughput" by Rick Hudson: https://go.dev/blog/go15gc (これも一般的なGoのGCに関する記事であり、直接このコミットに言及しているわけではありませんが、背景知識として有用です。)
- Goのランタイムに関する書籍やオンラインリソース(一般的な情報源)