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

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

このコミットは、Go言語のランタイムにおけるLinux上でのmmapシステムコールがMAP_LOCKEDフラグ付きで失敗した場合のハンドリングを改善するものです。具体的には、mmapEAGAINエラーを返した場合に、より適切なエラーメッセージを出力して終了するように変更されています。これにより、以前はnilポインタ参照でクラッシュしていた状況が改善されます。

コミット

commit e786829e8320497a062be1b3f78646bcf9375abc
Author: Sébastien Paolacci <sebastien.paolacci@gmail.com>
Date:   Sat Dec 29 14:34:06 2012 -0500

    runtime: handle locked mmap failure on Linux
    
    Used to then die on a nil pointer situation. Most Linux standard setups are rather
    restrictive regarding the default amount of lockable memory.
    
    R=minux.ma, rsc
    CC=golang-dev
    https://golang.org/cl/6997049

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

https://github.com/golang/go/commit/e786829e8320497a062be1b3f78646bcf9375abc

元コミット内容

runtime: handle locked mmap failure on Linux

Used to then die on a nil pointer situation. Most Linux standard setups are rather
restrictive regarding the default amount of lockable memory.

R=minux.ma, rsc
CC=golang-dev
https://golang.org/cl/6997049

変更の背景

この変更の背景には、GoランタイムがLinuxシステム上でメモリを確保する際にmmapシステムコールを使用し、特定の状況下でMAP_LOCKEDフラグを付けてメモリをロックしようとすることがあります。しかし、多くのLinuxシステムでは、ユーザーがロックできるメモリ量にデフォルトで制限(ulimit -lで確認できるmemlockリソース制限)が設けられています。

この制限を超えてメモリをロックしようとすると、mmapEAGAINエラーを返します。以前のGoランタイムの実装では、このEAGAINエラーが適切に処理されておらず、mmapの戻り値がnilとして扱われ、その後のnilポインタ参照によってランタイムがクラッシュするという問題がありました。

このコミットは、このクラッシュを防ぎ、ユーザーに対してより分かりやすいエラーメッセージを提供することで、問題の原因(ロック可能なメモリの不足)を特定しやすくすることを目的としています。

前提知識の解説

mmapシステムコール

mmapは、Unix系OSでファイルやデバイスをプロセスのアドレス空間にマップするために使用されるシステムコールです。これにより、ファイルの内容をメモリのように直接アクセスできるようになります。メモリの確保にも使用され、特に匿名メモリ領域(ファイルに関連付けられていないメモリ)を確保する際にも利用されます。

MAP_LOCKEDフラグ

mmapシステムコールに渡されるフラグの一つで、マップされたメモリ領域を物理メモリにロック(常駐)させることをOSに要求します。これにより、そのメモリ領域がスワップアウトされるのを防ぎます。リアルタイムアプリケーションや、メモリの内容がディスクに書き出されることによるレイテンシを避けたい場合に利用されます。ただし、この操作は特権を必要とする場合があり、またシステム全体のリソースに影響を与えるため、通常はユーザーがロックできるメモリ量に制限が設けられています。

EAGAINエラー

EAGAINは、システムコールが一時的に失敗したことを示すエラーコードです。これは、操作を再試行すれば成功する可能性があることを示唆しますが、この文脈では、リソースが一時的に利用できない(この場合はロック可能なメモリが不足している)ために操作が完了できなかったことを意味します。mmapMAP_LOCKED付きで呼ばれた際にEAGAINを返すのは、プロセスがロックしようとしたメモリ量が、システムが許可する最大ロック可能メモリ量(ulimit -lで設定されるmemlock)を超過した場合によく発生します。

ENOMEMエラー

ENOMEMは、システムが要求されたメモリを割り当てることができなかったことを示すエラーコードです。これは通常、システムに利用可能な物理メモリやスワップ領域が不足している場合に発生します。

ulimit -l

ulimitコマンドは、ユーザーがプロセスに対して設定できるリソース制限を表示または設定するために使用されます。-lオプションは、プロセスがロックできるメモリの最大量(memlock)をバイト単位で表示または設定します。この値が小さいと、MAP_LOCKEDフラグを使用するmmap呼び出しがEAGAINエラーで失敗する可能性が高まります。

SELinux execmem

SELinux (Security-Enhanced Linux) は、Linuxカーネルのセキュリティモジュールで、強制アクセス制御 (MAC) を提供します。execmemは、メモリ領域が実行可能かつ書き込み可能であるという権限を制御するSELinuxのブール値です。一部のアプリケーション(特にJITコンパイラを使用するものなど)は、実行時にメモリを書き込み、それを実行可能にする必要があります。SELinuxがexecmemを無効にしている場合、このような操作は拒否され、mmapが失敗する可能性があります。このコミットのコードでは、mmapが失敗した場合にSELinuxのexecmem設定を確認するメッセージも含まれています。

技術的詳細

Goランタイムは、メモリ管理のためにOSのmmapシステムコールを利用しています。特に、Goのガベージコレクタやスケジューラは、特定のメモリ領域を物理メモリに固定する必要がある場合があります。このためにMAP_LOCKEDフラグが使用されます。

変更前のコードでは、mmapが返すエラーコードを適切に区別していませんでした。mmapEAGAINを返した場合、これはnilポインタとして解釈され、その後のメモリ操作でクラッシュを引き起こしていました。

このコミットでは、src/pkg/runtime/mem_linux.cファイルに以下の変更が加えられています。

  1. EAGAINエラーコード(値は11)がenumに追加されました。これにより、コード内でEAGAINをシンボリックに参照できるようになります。
  2. runtime·SysAlloc関数内でmmapの戻り値をチェックするロジックが強化されました。
    • 既存のp == nilチェックに加えて、p == (void*)EAGAINという条件が追加されました。
    • この条件が真の場合、つまりmmapEAGAINを返した場合、"runtime: mmap: too much locked memory (check 'ulimit -l')."というエラーメッセージを出力し、runtime·exit(2)でプログラムを終了します。

これにより、MAP_LOCKEDによるmmap呼び出しがEAGAINで失敗した場合に、Goランタイムはクラッシュする代わりに、ユーザーに対して具体的な原因(ロック可能なメモリの不足)と解決策(ulimit -lの確認)を提示して終了するようになります。

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

変更はsrc/pkg/runtime/mem_linux.cファイルに集中しています。

--- a/src/pkg/runtime/mem_linux.c
+++ b/src/pkg/runtime/mem_linux.c
@@ -10,6 +10,7 @@
 
 enum
 {
+	EAGAIN = 11,
 	ENOMEM = 12,
 	_PAGE_SIZE = 4096,
 };
@@ -63,6 +64,10 @@ runtime·SysAlloc(uintptr n)
 		if(p == nil) {
 			// On some systems, mmap with PROT_NONE does not work.
 			// Try again with PROT_READ|PROT_WRITE.
 			// This does not affect overall security because
 			// the memory is not yet in use. It will be
 			// PROT_NONE-d before being given to user.
 			p = runtime·mmap(nil, n, PROT_READ|PROT_WRITE, MAP_ANON|MAP_PRIVATE, -1, 0);
 			if(p == nil) {
 				runtime·printf("runtime: mmap: cannot allocate memory\\n");
 				runtime·exit(2);
 			}
 			// Check for executable stack.
 			// Some systems, like OpenBSD, do not allow
 			// non-executable stack.
 			// If you're running SELinux, enable execmem for this process.
 			runtime·printf("if you're running SELinux, enable execmem for this process.\\n");
 			runtime·exit(2);
 		}
+		if(p == (void*)EAGAIN) {
+			runtime·printf("runtime: mmap: too much locked memory (check 'ulimit -l').\\n");
+			runtime·exit(2);
+		}
 		return nil;
 	}
 	return p;

コアとなるコードの解説

  1. EAGAIN = 11, の追加: enumブロックにEAGAINが追加され、その値がLinuxシステムにおけるEAGAINエラーコードの標準的な値である11に設定されています。これにより、マジックナンバーを避け、コードの可読性と保守性が向上します。

  2. if(p == (void*)EAGAIN) ブロックの追加: runtime·SysAlloc関数内で、mmapシステムコールがメモリを割り当てようとした後に、その戻り値pnilであるかどうかのチェックが行われています。このコミットでは、既存のif(p == nil)ブロックの直後に、if(p == (void*)EAGAIN)という新しい条件分岐が追加されました。

    • mmapシステムコールは、エラーが発生した場合に-1を返します。C言語では、ポインタ型にキャストされた-1は通常nilとは異なる値になります。しかし、Goのランタイムコードでは、mmapの戻り値をvoid*として受け取り、エラーコードを直接ポインタ値として解釈する場合があります。このため、EAGAIN(数値の11)がポインタにキャストされた値と比較されています。
    • この条件が真の場合、つまりmmapEAGAINエラーを示した場合、以下の処理が実行されます。
      • runtime·printf("runtime: mmap: too much locked memory (check 'ulimit -l').\\n");:ユーザーに対して、ロック可能なメモリが不足していること、そしてulimit -lコマンドで設定を確認するよう促すエラーメッセージが出力されます。
      • runtime·exit(2);:プログラムが終了コード2で終了します。これは、一般的なエラーを示す終了コードです。

この変更により、GoランタイムはMAP_LOCKED付きのmmap呼び出しがEAGAINで失敗した場合に、より堅牢かつ情報量の多い方法で対応できるようになりました。

関連リンク

参考にした情報源リンク