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

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

このコミットは、GoランタイムにおけるLinux環境での一度限りのメモリリークを修正するものです。具体的には、src/pkg/runtime/mem_linux.c ファイル内のメモリ予約処理 (runtime·SysReserve) における mmap_fixed の誤用が原因で発生していた問題に対処しています。

コミット

commit fd41926347c6f1cf1cc06079050912b9c48a1f09
Author: Dmitriy Vyukov <dvyukov@google.com>
Date:   Mon Jun 10 22:59:39 2013 +0400

    runtime: fix one-time memory leak on linux
    Update #5641.
    
    R=golang-dev, bradfitz
    CC=golang-dev
    https://golang.org/cl/10144043
---
 src/pkg/runtime/mem_linux.c | 9 ++++++---\n 1 file changed, 6 insertions(+), 3 deletions(-)

diff --git a/src/pkg/runtime/mem_linux.c b/src/pkg/runtime/mem_linux.c
index 1bae755faf..bacd568d9e 100644
--- a/src/pkg/runtime/mem_linux.c
+++ b/src/pkg/runtime/mem_linux.c
@@ -95,14 +95,17 @@ runtime·SysReserve(void *v, uintptr n)
 	// Only user-mode Linux (UML) rejects these requests.
 	if(sizeof(void*) == 8 && (uintptr)v >= 0xffffffffU) {
 		p = mmap_fixed(v, 64<<10, PROT_NONE, MAP_ANON|MAP_PRIVATE, -1, 0);\n-\t\tif (p != v)\n+\t\tif (p != v) {\n+\t\t\tif(p >= (void*)4096)\n+\t\t\t\truntime·munmap(p, 64<<10);\n \t\t\treturn nil;\n+\t\t}\n \t\truntime·munmap(p, 64<<10);\n \t\treturn v;\n \t}\n-\t\n+\n \tp = runtime·mmap(v, n, PROT_NONE, MAP_ANON|MAP_PRIVATE, -1, 0);\n-\tif((uintptr)p < 4096 || -(uintptr)p < 4096)\n+\tif((uintptr)p < 4096)\n \t\treturn nil;\n \treturn p;\n }\n```

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

[https://github.com/golang/go/commit/fd41926347c6f1cf1cc06079050912b9c48a1f09](https://github.com/golang/go/commit/fd41926347c6f1cf1cc06079050912b9c48a1f09)

## 元コミット内容

runtime: fix one-time memory leak on linux Update #5641.

R=golang-dev, bradfitz CC=golang-dev https://golang.org/cl/10144043


## 変更の背景

このコミットは、GoランタイムがLinux上でメモリを予約する際に発生していた、一度限りのメモリリークを修正するために導入されました。コミットメッセージにある「Update #5641」は、この問題がGoのイシュートラッカーで追跡されていたことを示唆しています。

Goランタイムは、プログラムの実行に必要なメモリをOSから取得し、管理します。このプロセスには、`mmap`システムコールが頻繁に利用されます。特に、特定のメモリアドレスにメモリをマップしようとする場合、`MAP_FIXED`フラグが使用されることがあります。しかし、`MAP_FIXED`は非常に強力なフラグであり、その使用には細心の注意が必要です。指定されたアドレスが既にマップされている場合、既存のマッピングを上書きしてしまう可能性があります。また、指定されたアドレスにマップできなかった場合、`mmap`はエラーを返すか、異なるアドレスを返すことがあります。

このコミットが修正しようとしている問題は、`mmap_fixed`が要求されたアドレスにメモリをマップできなかった場合に、`mmap_fixed`が返した別の有効なアドレスのメモリが解放されずにリークしてしまうというものでした。これは、Goランタイムが特定のメモリ領域を予約しようとした際に、その予約が失敗した場合のクリーンアップ処理が不完全であったために発生しました。

## 前提知識の解説

### メモリリーク (Memory Leak)
メモリリークとは、プログラムが確保したメモリ領域を、不要になった後も解放せずに保持し続けてしまう現象です。これにより、利用可能なメモリが徐々に減少し、最終的にはシステムのパフォーマンス低下やクラッシュを引き起こす可能性があります。Go言語はガベージコレクションを備えていますが、OSレベルのメモリ管理(`mmap`など)の誤用によってもメモリリークは発生し得ます。

### `mmap` システムコール
`mmap` (memory map) は、Unix系OSで利用されるシステムコールで、ファイルやデバイス、または匿名メモリ領域をプロセスのアドレス空間にマッピングするために使用されます。これにより、ファイルの内容をメモリとして直接アクセスしたり、プロセス間でメモリを共有したりすることが可能になります。

`mmap`の主な引数:
*   `addr`: マッピングを開始するアドレスのヒント。OSは可能な限りこのアドレスに近い場所を試みます。`NULL`を指定するとOSが適切なアドレスを選択します。
*   `length`: マッピングするバイト数。
*   `prot`: メモリ領域の保護(読み取り、書き込み、実行など)。
    *   `PROT_NONE`: アクセス不可。メモリを予約するが、アクセスは許可しない場合などに使用されます。
*   `flags`: マッピングの動作を制御するフラグ。
    *   `MAP_ANON`: ファイルではなく、匿名メモリ領域をマッピングします。
    *   `MAP_PRIVATE`: マッピングがプライベートであり、他のプロセスと共有されないことを示します。
    *   `MAP_FIXED`: `addr`で指定されたアドレスに厳密にマッピングすることを要求します。このフラグを使用すると、`addr`が`NULL`でない限り、OSは指定されたアドレスにマッピングを試みます。指定されたアドレスが利用できない場合、`mmap`は失敗します。**このフラグは非常に強力であり、誤用すると既存のマッピングを上書きしたり、予期せぬメモリリークを引き起こしたりする可能性があります。**
*   `fd`: マッピングするファイルのファイルディスクリプタ。匿名マッピングの場合は`-1`。
*   `offset`: ファイル内のオフセット。

### `munmap` システムコール
`munmap`は、`mmap`によってマッピングされたメモリ領域をアンマッピング(解放)するために使用されるシステムコールです。これにより、プロセスのアドレス空間から指定されたメモリ領域が削除され、OSにメモリが返却されます。

### `runtime·SysReserve` 関数
Goランタイム内部の関数で、OSからメモリを予約するために使用されます。Goのガベージコレクタやメモリ管理システムが利用する、より低レベルなメモリ確保のメカニズムの一部です。

## 技術的詳細

このコミットが修正する問題は、`runtime·SysReserve`関数内で発生していました。この関数は、特定の条件下(特に64ビットシステムで、非常に高いアドレスにメモリを予約しようとする場合)で、`mmap_fixed`を使用してメモリを予約しようとします。

元のコードでは、`mmap_fixed(v, 64<<10, PROT_NONE, MAP_ANON|MAP_PRIVATE, -1, 0)`が呼び出され、`v`という特定のアドレスに`64KB`のメモリを`PROT_NONE`(アクセス不可)で匿名かつプライベートにマッピングしようとしていました。

問題は、`mmap_fixed`が`v`にマッピングできなかった場合です。この場合、`mmap_fixed`は`v`とは異なるアドレス`p`を返す可能性があります(またはエラーを示す`MAP_FAILED`を返す)。元のコードでは、`p != v`の場合に`return nil`としていましたが、この`p`が有効なアドレスであった場合、そのメモリ領域は`munmap`されずにリークしていました。つまり、`mmap_fixed`が意図しないアドレスにメモリをマップしてしまったが、そのメモリを解放する処理が欠けていたのです。

また、もう一つの変更点として、`mmap`の戻り値`p`が不正なアドレス(4096未満)であるかどうかのチェックがありました。元のコードでは`if((uintptr)p < 4096 || -(uintptr)p < 4096)`という条件でしたが、これは符号なし整数と符号付き整数の比較が混在しており、特に負の値のチェックが意図しない挙動を引き起こす可能性がありました。Linuxの`mmap`は成功した場合、常にページ境界にアラインされた非負のアドレスを返します。したがって、`p < 4096`(通常、NULLポインタや非常に低いアドレスは不正と見なされる)のチェックで十分です。

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

```diff
--- a/src/pkg/runtime/mem_linux.c
+++ b/src/pkg/runtime/mem_linux.c
@@ -95,14 +95,17 @@ runtime·SysReserve(void *v, uintptr n)
 	// Only user-mode Linux (UML) rejects these requests.
 	if(sizeof(void*) == 8 && (uintptr)v >= 0xffffffffU) {
 		p = mmap_fixed(v, 64<<10, PROT_NONE, MAP_ANON|MAP_PRIVATE, -1, 0);\n-\t\tif (p != v)\n+\t\t\tif (p != v) {\n+\t\t\t\tif(p >= (void*)4096)\n+\t\t\t\t\truntime·munmap(p, 64<<10);\n \t\t\treturn nil;\n+\t\t}\n \t\truntime·munmap(p, 64<<10);\
 \t\treturn v;\
 \t}\
-\t\n+\n \tp = runtime·mmap(v, n, PROT_NONE, MAP_ANON|MAP_PRIVATE, -1, 0);\
-\tif((uintptr)p < 4096 || -(uintptr)p < 4096)\n+\tif((uintptr)p < 4096)\
 \t\treturn nil;\
 \treturn p;\
 }\

コアとなるコードの解説

このコミットには主に2つの重要な変更点があります。

  1. mmap_fixedによるメモリリークの修正:

    • 変更前:
      if (p != v)
          return nil;
      runtime·munmap(p, 64<<10);
      
      このコードでは、mmap_fixedが要求されたアドレスvとは異なるアドレスpを返した場合(つまりp != v)、すぐにnilを返して関数を終了していました。しかし、このpが有効なメモリ領域を指していた場合、そのメモリはmunmapされることなくリークしていました。その後のruntime·munmap(p, 64<<10);は、p == vの場合にのみ実行されるため、リークが発生していました。
    • 変更後:
      if (p != v) {
          if(p >= (void*)4096)
              runtime·munmap(p, 64<<10);
          return nil;
      }
      
      この修正では、p != vの場合に、まずpが有効なアドレス(4096以上、つまりNULLや非常に低い不正なアドレスではない)であるかをチェックします。もし有効であれば、そのメモリ領域をruntime·munmapで明示的に解放してからnilを返します。これにより、mmap_fixedが意図しないアドレスにメモリをマップしてしまった場合でも、そのメモリが適切に解放されるようになり、メモリリークが解消されます。
  2. mmap戻り値の不正アドレスチェックの簡素化:

    • 変更前:
      if((uintptr)p < 4096 || -(uintptr)p < 4096)
          return nil;
      
      この条件は、mmapが返したアドレスpが不正であるかをチェックしています。4096という値は、通常、NULLポインタや非常に低いアドレス(カーネルによって保護されている可能性のあるアドレス)を不正と見なすための閾値として使われます。しかし、-(uintptr)p < 4096という部分は、符号なし整数uintptrに負の演算子を適用しており、これは通常、非常に大きな正の数になります。この条件は、mmapがエラーを示すMAP_FAILED(通常は-1)を返した場合に、それがuintptrにキャストされると非常に大きな正の数になるため、それを検出するための試みだった可能性があります。しかし、これはGoのランタイムがmmapの戻り値をどのように扱うかによって、よりシンプルにできる可能性があります。
    • 変更後:
      if((uintptr)p < 4096)
          return nil;
      
      この修正では、-(uintptr)p < 4096の条件が削除され、p4096未満であるかどうかのチェックのみに簡素化されました。これは、Linuxのmmapが成功した場合、常にページ境界にアラインされた非負のアドレスを返すという事実に基づいています。したがって、MAP_FAILEDのようなエラー値は、uintptrにキャストされたとしても、通常は4096未満の小さな値にはならないため、この簡素化されたチェックで十分であると判断されたと考えられます。これにより、コードの可読性と正確性が向上しています。

これらの変更により、GoランタイムはLinux環境でより堅牢なメモリ管理を行うことができるようになりました。

関連リンク

  • Go CL 10144043: https://golang.org/cl/10144043
  • Go Issue #5641 (言及されているが、直接的なイシューページは特定できず): 関連する議論や修正は、Goのイシュートラッカーやメーリングリストで追跡されていた可能性があります。

参考にした情報源リンク