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

[インデックス 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に相当するVirtualAllocVirtualProtectといった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の引数は、mmapflags引数に何も指定しないことを意味し、デフォルトの挙動に依存していました。これにより、メモリ領域はアクセス不可になりますが、仮想アドレス空間は解放されず、プロセスに紐付けられたままでした。

変更後:

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(例えばVirtualFreeMEM_DECOMMITフラグの組み合わせ)を利用して、指定されたメモリ領域を「未使用」としてマークし、物理メモリのコミットを解除します。これにより、Unix系OSでのmmapの変更と同様に、仮想アドレス空間を占有しつつも物理メモリの消費を抑え、システムリソースの効率的な利用を促進します。コメントにあるように、「メモリをアクセス不可にし、その再利用を防ぐ」という目的は維持しつつ、より効率的な方法で実現しています。

関連リンク

参考にした情報源リンク