[インデックス 18979] ファイルの概要
このコミットは、GoランタイムがWindows環境でGetQueuedCompletionStatusEx
関数が存在しない場合にクラッシュする問題を修正します。具体的には、GetQueuedCompletionStatusEx
が利用できない古いWindowsバージョンや特定の環境において、Goランタイムが安全に動作するように、この関数の動的なロードと存在チェックを導入しています。
コミット
commit 277a7b22f1131ea2a2fb98be9d7378c0f4ab5834
Author: Alex Brainman <alex.brainman@gmail.com>
Date: Fri Mar 28 12:37:14 2014 +1100
runtime: do not crash when GetQueuedCompletionStatusEx is missing
Fixes #7635
LGTM=minux.ma
R=golang-codereviews, minux.ma
CC=golang-codereviews
https://golang.org/cl/80390043
GitHub上でのコミットページへのリンク
https://github.com/golang/go/commit/277a7b22f1131ea2a2fb98be9d7378c0f4ab5834
元コミット内容
runtime: do not crash when GetQueuedCompletionStatusEx is missing
Fixes #7635
LGTM=minux.ma
R=golang-codereviews, minux.ma
CC=golang-codereviews
https://golang.org/cl/80390043
変更の背景
この変更の背景には、GoランタイムがWindowsのI/O Completion Ports (IOCP) を利用する際に、特定のWindows API関数であるGetQueuedCompletionStatusEx
の可用性に関する問題がありました。
GetQueuedCompletionStatusEx
は、Windows VistaおよびWindows Server 2008以降で導入された関数であり、I/O完了ポートから複数の完了パケットを効率的に取得するために使用されます。これ以前のWindowsバージョン(例えばWindows XPやWindows Server 2003など)では、この関数は存在しませんでした。
GoランタイムがGetQueuedCompletionStatusEx
を静的にリンクまたは直接呼び出そうとした場合、この関数が存在しない環境では、プログラムが起動時または実行時にクラッシュする可能性がありました。これは、特に古いWindows環境でのGoアプリケーションの互換性を損なう重大な問題でした。
コミットメッセージにあるFixes #7635
は、この問題がGoのIssueトラッカーで報告され、修正が必要とされていたことを示しています。このコミットは、GetQueuedCompletionStatusEx
が利用できない場合でもGoランタイムがクラッシュしないように、そのロード方法を改善することを目的としています。
前提知識の解説
I/O Completion Ports (IOCP)
I/O Completion Ports (IOCP) は、Windowsオペレーティングシステムが提供する、高効率な非同期I/O処理のためのメカニズムです。特に、多数の同時接続を扱うサーバーアプリケーションなどでその真価を発揮します。
IOCPの基本的な考え方は以下の通りです。
- 完了ポートの作成:
CreateIoCompletionPort
関数を使用して、I/O完了ポートを作成します。 - デバイスの関連付け: ファイルハンドル、ソケット、名前付きパイプなどのI/Oデバイスを完了ポートに関連付けます。
- 非同期I/Oの開始:
ReadFile
,WriteFile
,WSARecv
,WSASend
などの非同期I/O操作を開始します。これらの操作はすぐに完了せず、バックグラウンドで実行されます。 - 完了通知の待機: I/O操作が完了すると、その結果は関連付けられた完了ポートにキューされます。アプリケーションは、
GetQueuedCompletionStatus
またはGetQueuedCompletionStatusEx
を呼び出して、完了したI/O操作の通知を待機します。 - スレッドプールの利用: 通常、複数のスレッドが同じ完了ポートから完了通知を待機します。IOCPは、利用可能なスレッドに完了通知を効率的にディスパッチし、スレッドの過剰な生成やコンテキストスイッチのオーバーヘッドを最小限に抑えることで、高いスケーラビリティを実現します。
GetQueuedCompletionStatusEx
GetQueuedCompletionStatusEx
は、Windows VistaおよびWindows Server 2008で導入された、GetQueuedCompletionStatus
の拡張版です。主な違いは、一度の呼び出しで複数のI/O完了パケットを取得できる点にあります。
GetQueuedCompletionStatus
: 1回の呼び出しで1つの完了パケットしか取得できません。GetQueuedCompletionStatusEx
: 1回の呼び出しで指定された数の完了パケットを配列として取得できます。これにより、システムコール数を減らし、特に高負荷な環境でのパフォーマンスを向上させることができます。
Goランタイムは、Windows上でのネットワークI/OやファイルI/Oにおいて、このIOCPメカニズムを内部的に利用しています。そのため、GetQueuedCompletionStatusEx
のような重要な関数が利用できない環境で問題が発生すると、ランタイムの安定性に直接影響します。
GoランタイムのWindowsにおけるDLL動的ロード (dynimport
と GetProcAddress
)
Windowsのプログラムは、通常、DLL (Dynamic Link Library) と呼ばれる共有ライブラリから関数を呼び出します。Goランタイムのような低レベルのシステムプログラミングでは、特定のDLL関数を動的にロードする必要がある場合があります。これは、主に以下の理由によります。
- 後方互換性: 特定の関数が古いWindowsバージョンには存在せず、新しいバージョンでのみ利用可能である場合、プログラムが古いOSでも動作するように、その関数を動的にロードし、存在しない場合は代替処理を行う必要があります。
- リソースの節約: プログラムの起動時にすべてのDLLをロードするのではなく、必要になったときにのみロードすることで、メモリ使用量を削減し、起動時間を短縮できます。
Goランタイムでは、dynimport
というディレクティブを使用して、外部DLL関数を宣言します。しかし、dynimport
はあくまで「この関数がこのDLLに存在する可能性がある」という宣言であり、実際にその関数が利用可能かどうかは実行時に確認する必要があります。
この確認のために使用されるのが、Windows APIのLoadLibraryA
とGetProcAddress
関数です。
LoadLibraryA
: 指定されたDLLをメモリにロードし、そのDLLのハンドルを返します。GetProcAddress
: ロードされたDLLのハンドルと関数名を指定して、その関数のエントリポイント(メモリ上のアドレス)を取得します。関数が存在しない場合、NULL
を返します。
このコミットでは、GetQueuedCompletionStatusEx
をdynimport
で静的に宣言するのではなく、LoadLibraryA
とGetProcAddress
を使って動的にロードすることで、関数が存在しない環境でもクラッシュしないように修正しています。
技術的詳細
このコミット以前のGoランタイムでは、GetQueuedCompletionStatusEx
関数をsrc/pkg/runtime/os_windows.c
内で#pragma dynimport runtime·GetQueuedCompletionStatusEx GetQueuedCompletionStatusEx "kernel32.dll"
のように宣言し、直接利用しようとしていました。このdynimport
は、コンパイル時にkernel32.dll
からGetQueuedCompletionStatusEx
をインポートしようとしますが、もし実行環境のkernel32.dll
にこの関数が存在しない場合(例: Windows XP)、ランタイムは未定義のシンボルを参照しようとしてクラッシュしていました。
この修正は、GetQueuedCompletionStatusEx
のロードを静的なインポートから動的なインポートに切り替えることで、この問題を解決します。具体的には、ランタイムの初期化フェーズ(runtime·osinit
関数内)で、kernel32.dll
を明示的にロードし、その中からGetQueuedCompletionStatusEx
のアドレスをGetProcAddress
を使って取得します。
もしGetProcAddress
がNULL
を返した場合、それはGetQueuedCompletionStatusEx
が現在のシステムで利用できないことを意味します。この場合、GoランタイムはGetQueuedCompletionStatusEx
を使用せず、代わりにGetQueuedCompletionStatus
のような代替手段(または、この関数が必須でない場合は単にその機能を使用しない)にフォールバックすることで、クラッシュを回避します。このコミット自体はフォールバックロジックを直接追加するものではなく、あくまでクラッシュを回避するための動的ロードメカニズムを導入しています。
このアプローチにより、GoアプリケーションはGetQueuedCompletionStatusEx
が存在しない古いWindowsバージョンでも起動し、動作できるようになります。
コアとなるコードの変更箇所
変更はsrc/pkg/runtime/os_windows.c
ファイルに集中しています。
--- a/src/pkg/runtime/os_windows.c
+++ b/src/pkg/runtime/os_windows.c
@@ -21,7 +21,6 @@
#pragma dynimport runtime·FreeEnvironmentStringsW FreeEnvironmentStringsW "kernel32.dll"
#pragma dynimport runtime·GetEnvironmentStringsW GetEnvironmentStringsW "kernel32.dll"
#pragma dynimport runtime·GetProcAddress GetProcAddress "kernel32.dll"
-#pragma dynimport runtime·GetQueuedCompletionStatusEx GetQueuedCompletionStatusEx "kernel32.dll"
#pragma dynimport runtime·GetStdHandle GetStdHandle "kernel32.dll"
#pragma dynimport runtime·GetSystemInfo GetSystemInfo "kernel32.dll"
#pragma dynimport runtime·GetSystemTimeAsFileTime GetSystemTimeAsFileTime "kernel32.dll"
@@ -54,7 +53,6 @@ extern void *runtime·ExitProcess;
extern void *runtime·FreeEnvironmentStringsW;
extern void *runtime·GetEnvironmentStringsW;
extern void *runtime·GetProcAddress;
-extern void *runtime·GetQueuedCompletionStatusEx;
extern void *runtime·GetStdHandle;
extern void *runtime·GetSystemInfo;
extern void *runtime·GetSystemTimeAsFileTime;
@@ -74,6 +72,8 @@ extern void *runtime·WaitForSingleObject;
extern void *runtime·WriteFile;
extern void *runtime·timeBeginPeriod;
+void *runtime·GetQueuedCompletionStatusEx;
+
extern uintptr runtime·externalthreadhandlerp;
void runtime·externalthreadhandler(void);
void runtime·sigtramp(void);
@@ -90,6 +90,8 @@ getproccount(void)
void
runtime·osinit(void)
{
+ void *kernel32;
+
runtime·externalthreadhandlerp = (uintptr)runtime·externalthreadhandler;
runtime·stdcall(runtime·AddVectoredExceptionHandler, 2, (uintptr)1, (uintptr)runtime·sigtramp);
@@ -102,6 +104,11 @@ runtime·osinit(void)
// equivalent threads that all do a mix of GUI, IO, computations, etc.
// In such context dynamic priority boosting does nothing but harm, so we turn it off.
runtime·stdcall(runtime·SetProcessPriorityBoost, 2, (uintptr)-1, (uintptr)1);\n+\n+\tkernel32 = runtime·stdcall(runtime·LoadLibraryA, 1, \"kernel32.dll\");\n+\tif(kernel32 != nil) {\n+\t\truntime·GetQueuedCompletionStatusEx = runtime·stdcall(runtime·GetProcAddress, 2, kernel32, \"GetQueuedCompletionStatusEx\");\n+\t}\n }
void
主な変更点は以下の通りです。
#pragma dynimport
の削除:GetQueuedCompletionStatusEx
に対する#pragma dynimport
行が削除されました。これにより、コンパイル時にこの関数を静的にインポートしようとする試みがなくなります。extern void *runtime·GetQueuedCompletionStatusEx;
の削除: 同様に、外部シンボルとしての宣言も削除されました。void *runtime·GetQueuedCompletionStatusEx;
の追加: グローバル変数としてruntime·GetQueuedCompletionStatusEx
が宣言されました。これは、動的に取得した関数のアドレスを格納するためのポインタです。runtime·osinit
関数内の動的ロードロジックの追加:kernel32 = runtime·stdcall(runtime·LoadLibraryA, 1, "kernel32.dll");
:kernel32.dll
を動的にロードします。if(kernel32 != nil) { ... }
:kernel32.dll
のロードが成功した場合にのみ、以下の処理を実行します。runtime·GetQueuedCompletionStatusEx = runtime·stdcall(runtime·GetProcAddress, 2, kernel32, "GetQueuedCompletionStatusEx");
:ロードしたkernel32.dll
からGetQueuedCompletionStatusEx
関数のアドレスを取得し、runtime·GetQueuedCompletionStatusEx
ポインタに格納します。
コアとなるコードの解説
このコミットの核心は、GetQueuedCompletionStatusEx
関数のロード方法を、コンパイル時の静的なインポートから実行時の動的なインポートへと変更した点にあります。
以前は、Goランタイムは#pragma dynimport
を使用して、GetQueuedCompletionStatusEx
がkernel32.dll
に存在することを前提としていました。しかし、この関数はWindows Vista以降で導入されたため、Windows XPなどの古いシステムでは利用できませんでした。その結果、古いシステムでGoプログラムを実行しようとすると、存在しない関数への参照によってクラッシュが発生していました。
新しいコードでは、runtime·osinit
(GoランタイムのWindows固有の初期化関数)内で、以下の手順を踏みます。
kernel32.dll
のロード:runtime·stdcall(runtime·LoadLibraryA, 1, "kernel32.dll");
を呼び出して、kernel32.dll
を明示的にメモリにロードします。LoadLibraryA
は、指定されたDLLのモジュールハンドルを返します。このハンドルは、そのDLL内の関数を検索するために使用されます。- 関数のアドレス取得:
kernel32
がnil
でない(つまり、kernel32.dll
のロードに成功した)場合、runtime·stdcall(runtime·GetProcAddress, 2, kernel32, "GetQueuedCompletionStatusEx");
を呼び出します。GetProcAddress
は、指定されたモジュールハンドルと関数名に対応する関数のエントリポイントのアドレスを返します。もし関数が見つからない場合、NULL
を返します。 - ポインタへの格納: 取得したアドレスは、グローバル変数
runtime·GetQueuedCompletionStatusEx
に格納されます。
この変更により、GoランタイムはGetQueuedCompletionStatusEx
がシステムに存在するかどうかを実行時に確認できるようになります。もし関数が存在しない場合、runtime·GetQueuedCompletionStatusEx
はNULL
のままとなり、ランタイムはクラッシュすることなく、この関数が利用できないことを認識できます。その後のGoランタイムのコードは、このポインタがNULL
であるかどうかをチェックし、適切な代替ロジック(例えば、GetQueuedCompletionStatus
を使用するなど)にフォールバックするか、あるいはGetQueuedCompletionStatusEx
の機能が必須でない場合はその機能を使用しないように設計されていると推測されます。
この修正は、GoランタイムのWindowsにおける堅牢性と互換性を大幅に向上させるものです。
関連リンク
- GitHubコミットページ: https://github.com/golang/go/commit/277a7b22f1131ea2a2fb98be9d7378c0f4ab5834
- Go Gerrit Change-ID: https://golang.org/cl/80390043
参考にした情報源リンク
- Web検索結果:
GetQueuedCompletionStatusEx
に関するMicrosoft Learnドキュメント(検索時に参照された可能性のある情報源) - Web検索結果: Go Gerrit Change-ID
golang.org/cl/80390043
に関する情報(検索時に参照された可能性のある情報源)