[インデックス 14731] ファイルの概要
このコミットは、Goランタイムにおけるmadvise
システムコールの挙動を修正するものです。具体的には、madvise
が失敗した場合にランタイムがクラッシュするのではなく、その失敗を無視するように変更されています。これは、メモリの解放がOSによって拒否される可能性があるシナリオ(例えば、mlockall(MCL_FUTURE)
が実行されている場合)に対応するための重要な変更です。
コミット
commit 295a4d8e6433e8a8b6df25375fb780b0f75ff4e6
Author: Russ Cox <rsc@golang.org>
Date: Sat Dec 22 15:06:28 2012 -0500
runtime: ignore failure from madvise
When we release memory to the OS, if the OS doesn't want us
to release it (for example, because the program executed
mlockall(MCL_FUTURE)), madvise will fail. Ignore the failure
instead of crashing.
Fixes #3435.
R=ken2
CC=golang-dev
https://golang.org/cl/6998052
GitHub上でのコミットページへのリンク
https://github.com/golang/go/commit/295a4d8e6433e8a8b6df25375fb780b0f75ff4e6
元コミット内容
GoランタイムがOSにメモリを解放しようとした際、OSがその解放を拒否した場合(例: プログラムがmlockall(MCL_FUTURE)
を実行しているため)、madvise
システムコールが失敗することがあります。このコミットは、madvise
の失敗を無視し、クラッシュしないように修正します。
関連するIssue: #3435
変更の背景
Goランタイムは、ガベージコレクションなどによって不要になったメモリをOSに返却する際に、madvise
システムコールを使用します。しかし、特定の状況下では、このmadvise
コールが失敗することがありました。特に、プログラムがmlockall(MCL_FUTURE)
を呼び出して、将来的に確保されるすべてのメモリページを物理メモリにロックするようOSに要求している場合、Goランタイムがメモリを解放しようとしても、OSはそれを拒否します。
この拒否が発生すると、以前のGoランタイムではmadvise
の失敗が致命的なエラーと見なされ、プログラムがクラッシュしていました。これは、mlockall
のような特定のシステム設定やセキュリティ要件を持つ環境でGoプログラムが安定して動作しない原因となっていました。
このコミットの目的は、このようなmadvise
の失敗が必ずしもプログラムの実行にとって致命的ではないことを認識し、ランタイムがクラッシュする代わりにその失敗を許容するように変更することです。これにより、Goプログラムの堅牢性と互換性が向上します。
前提知識の解説
madvise
システムコール
madvise
は、Unix系OS(Linux, macOS, FreeBSDなど)で利用可能なシステムコールで、プロセスが仮想メモリの特定の範囲についてOSに「アドバイス」を与えるために使用されます。このアドバイスは、OSがメモリの利用方法を最適化するのに役立ちますが、OSは必ずしもそのアドバイスに従う義務はありません。
一般的なmadvise
の用途には以下のようなものがあります。
MADV_DONTNEED
: 指定されたメモリ範囲の内容はもう必要ないため、OSはいつでもそのページを解放してよい。GoランタイムがメモリをOSに返却する際にこれを使用します。MADV_WILLNEED
: 指定されたメモリ範囲の内容はすぐに必要になるため、OSは事前にページをプリフェッチしてよい。MADV_FREE
: 指定されたメモリ範囲は解放されるが、後で再利用される可能性があるため、OSはページをスワップアウトしてもよい。
madvise
はあくまで「アドバイス」であり、OSがその要求を拒否する可能性も存在します。
mlockall
システムコールと MCL_FUTURE
mlockall
は、プロセスのアドレス空間全体、または将来的に確保されるメモリページを物理メモリにロックするシステムコールです。これにより、これらのメモリページがスワップアウトされるのを防ぎます。これは、リアルタイムシステムやセキュリティが非常に重要なアプリケーション(例: 暗号化キーをメモリに保持するアプリケーション)で、メモリの内容がディスクに漏洩するのを防ぐために使用されることがあります。
mlockall
には以下のフラグがあります。
MCL_CURRENT
: 現在プロセスが使用しているすべてのメモリページをロックします。MCL_FUTURE
: 将来的にプロセスが確保するすべてのメモリページをロックします。
このコミットの文脈では、mlockall(MCL_FUTURE)
が重要です。このフラグが設定されていると、Goランタイムがmadvise(MADV_DONTNEED)
を使ってメモリをOSに返却しようとしても、OSは「このメモリはロックされているため解放できない」と判断し、madvise
コールが失敗する原因となります。
アセンブリ言語とシステムコール
Goランタイムは、OSとの直接的なやり取りのために、アセンブリ言語で書かれたコードを使用することがあります。これは、パフォーマンスの最適化や、特定のOS機能(システムコール)への低レベルなアクセスが必要な場合に用いられます。
このコミットで変更されているファイルは、src/pkg/runtime/sys_darwin_386.s
のように、OSとアーキテクチャの組み合わせごとに存在するアセンブリファイルです。これらのファイルには、madvise
のようなシステムコールを呼び出すためのアセンブリ命令が含まれています。
システムコールが成功したかどうかは、通常、レジスタの値(例: AX
やR0
)や特定のフラグ(例: キャリーフラグ)をチェックすることで判断されます。失敗した場合、エラーコードが返されることが一般的です。
技術的詳細
このコミットの技術的な核心は、Goランタイムがmadvise
システムコールを呼び出した後のエラーハンドリングの変更にあります。
以前のバージョンでは、madvise
システムコールがエラーを返した場合、Goランタイムは意図的にクラッシュするようなアセンブリ命令を実行していました。これは、メモリ管理における予期せぬ失敗を早期に検出し、開発者に問題があることを知らせるための防御的なプログラミング手法と考えられます。
しかし、mlockall(MCL_FUTURE)
のような正当な理由でmadvise
が失敗するケースが判明したため、この「クラッシュ」という挙動は過剰であると判断されました。mlockall
が有効な環境では、メモリの解放ができないのはOSの設計上の意図であり、Goランタイムがそのメモリを解放できないからといってクラッシュする必要はありません。単にOSがメモリを解放してくれないだけであり、プログラムの実行自体には影響がない場合が多いからです。
このコミットでは、各OS/アーキテクチャのアセンブリファイルにおいて、madvise
システムコール呼び出し後のエラーチェックとクラッシュロジックが削除されています。代わりに、システムコールが失敗した場合でも、単にRET
(リターン)命令を実行して呼び出し元に戻るようになっています。これにより、madvise
の失敗は無視され、プログラムの実行が継続されます。
具体的には、以下のようなアセンブリ命令が削除されています(アーキテクチャによって命令は異なりますが、概念は同じです):
-
x86/x64 (Darwin, FreeBSD, NetBSD, OpenBSD):
JAE 2(PC)
/JCC 2(PC)
: システムコールが成功した場合(キャリーフラグがクリアされている場合)にジャンプする命令。これが削除されることで、成功/失敗に関わらず次の命令に進むようになります。MOVL $0xf1, 0xf1 // crash
: 意図的に不正なメモリアドレスに書き込もうとすることでクラッシュを引き起こす命令。
-
ARM (FreeBSD, Linux):
MOVW.CS $0, R9 // crash on syscall failure
/MOVW.HI $0, R9 // crash on syscall failure
: システムコールが失敗した場合(キャリーフラグがセットされている場合)にクラッシュを引き起こす命令。
これらの変更により、Goランタイムはmadvise
の失敗をより寛容に扱い、特定の環境下での安定性が向上しました。
コアとなるコードの変更箇所
このコミットでは、Goランタイムの複数のアセンブリファイルが変更されています。これらは、異なるOS(Darwin, FreeBSD, Linux, NetBSD, OpenBSD)とCPUアーキテクチャ(386, amd64, arm)の組み合わせに対応するものです。
変更されたファイルは以下の通りです。
src/pkg/runtime/sys_darwin_386.s
src/pkg/runtime/sys_darwin_amd64.s
src/pkg/runtime/sys_freebsd_386.s
src/pkg/runtime/sys_freebsd_amd64.s
src/pkg/runtime/sys_freebsd_arm.s
src/pkg/runtime/sys_linux_386.s
src/pkg/runtime/sys_linux_amd64.s
src/pkg/runtime/sys_linux_arm.s
src/pkg/runtime/sys_netbsd_386.s
src/pkg/runtime/sys_netbsd_amd64.s
src/pkg/runtime/sys_openbsd_amd64.s
各ファイルにおける変更のパターンは非常に似ており、madvise
システムコールを呼び出した直後のエラーチェックと、それに続くクラッシュロジックが削除されています。
例: src/pkg/runtime/sys_darwin_386.s
の変更
--- a/src/pkg/runtime/sys_darwin_386.s
+++ b/src/pkg/runtime/sys_darwin_386.s
@@ -47,8 +47,7 @@ TEXT runtime·mmap(SB),7,$0
TEXT runtime·madvise(SB),7,$0
MOVL $75, AX
INT $0x80
-- JAE 2(PC)
-- MOVL $0xf1, 0xf1 // crash
-+ // ignore failure - maybe pages are locked
RET
TEXT runtime·munmap(SB),7,$0
コアとなるコードの解説
上記の差分は、runtime·madvise
というGoランタイムの関数(アセンブリで実装されている)における変更を示しています。
MOVL $75, AX
: システムコール番号75(Darwin/x86におけるmadvise
の番号)をAX
レジスタにロードします。INT $0x80
: ソフトウェア割り込み0x80
を発生させ、システムコールを実行します。この命令が実行されると、OSカーネルに制御が渡り、madvise
システムコールが処理されます。JAE 2(PC)
: これは削除された行です。JAE
は "Jump if Above or Equal" の略で、キャリーフラグがクリアされている(つまり、システムコールが成功した)場合に、現在のプログラムカウンタ(PC)から2バイト先にジャンプするという意味です。この命令が存在すると、システムコールが成功した場合はクラッシュロジックをスキップし、失敗した場合は次のクラッシュロジックに進んでいました。MOVL $0xf1, 0xf1 // crash
: これも削除された行です。0xf1
という値を0xf1
というメモリアドレスに書き込もうとする命令です。0xf1
は通常、有効なメモリアドレスではないため、この操作はセグメンテーション違反を引き起こし、プログラムをクラッシュさせます。これは、madvise
が失敗した場合に意図的にクラッシュさせるためのコードでした。// ignore failure - maybe pages are locked
: 新しく追加されたコメントです。このコメントは、madvise
の失敗を無視する意図と、その理由(ページがロックされている可能性があるため)を明確に示しています。RET
: 関数からリターンします。システムコールが成功したか失敗したかに関わらず、この命令が実行され、呼び出し元に制御が戻ります。
この変更により、madvise
システムコールがエラーを返しても、Goランタイムはクラッシュすることなく、その失敗を透過的に処理するようになりました。これは、mlockall(MCL_FUTURE)
のような、メモリが意図的にロックされている環境でのGoプログラムの安定性を大幅に向上させます。
関連リンク
- Go Issue #3435: https://github.com/golang/go/issues/3435
- Go CL 6998052: https://golang.org/cl/6998052 (Goのコードレビューシステムにおける変更セットのリンク)
参考にした情報源リンク
madvise(2)
man page (Linux): https://man7.org/linux/man-pages/man2/madvise.2.htmlmlockall(2)
man page (Linux): https://man7.org/linux/man-pages/man2/mlockall.2.html- Go Assembly Language (Goの公式ドキュメント): https://go.dev/doc/asm
- System call (Wikipedia): https://ja.wikipedia.org/wiki/%E3%82%B7%E3%82%B9%E3%83%86%E3%83%A0%E3%82%B3%E3%83%BC%E3%83%AB
- x86 assembly language (Wikipedia): https://ja.wikipedia.org/wiki/X86%E3%82%A2%E3%82%BB%E3%83%B3%E3%83%96%E3%83%A9%E8%A8%80%E8%AA%9E
- ARM architecture (Wikipedia): https://ja.wikipedia.org/wiki/ARM%E3%82%A2%E3%83%BC%E3%82%AD%E3%83%86%E3%82%AF%E3%83%81%E3%83%A3