[インデックス 16773] ファイルの概要
このコミットは、GoランタイムにおけるWindows環境でのosyield
およびusleep
関数の実装を改善するものです。具体的には、これらの関数がOSのスタックに切り替えて実行されるように変更することで、潜在的な問題を解決し、より堅牢な動作を実現しています。
コミット
commit 45cff65502ace2783f05cf27383d807f07627cf2
Author: Alex Brainman <alex.brainman@gmail.com>
Date: Tue Jul 16 12:36:05 2013 +1000
runtime: switch to os stack in windows osyield and usleep
Fixes #5831
R=golang-dev, rsc
CC=golang-dev
https://golang.org/cl/11266043
GitHub上でのコミットページへのリンク
https://github.com/golang/go/commit/45cff65502ace2783f05cf27383d807f07627cf2
元コミット内容
runtime: switch to os stack in windows osyield and usleep
Fixes #5831
変更の背景
このコミットは、GoランタイムがWindows上でosyield
(CPUを他のスレッドに譲渡する)やusleep
(指定された時間だけスリープする)といった操作を行う際に発生していた問題を解決するために導入されました。Goのランタイムは、軽量なゴルーチン(goroutine)を効率的にスケジューリングするために、独自のスタック管理メカニズムを持っています。しかし、OSレベルのシステムコール、特にスレッドのスケジューリングや待機に関連するものは、OSが管理するスタック(OSスタック)上で実行されることを期待する場合があります。
元の実装では、これらの関数がGoのゴルーチンのスタック上で直接システムコールを呼び出していた可能性があります。これにより、特にWINEのような互換性レイヤー上でGoプログラムを実行する場合や、特定のOSの挙動において、スタックの整合性やシステムコールの正しい実行に問題が生じる可能性がありました。Issue #5831は、この問題が実際に発生していたことを示唆しています。
Goのランタイムは、ゴルーチンがシステムコールを実行する際に、必要に応じてOSスタックに切り替えるメカニズムを持っています。このコミットは、osyield
とusleep
についてもこのメカニズムを適用し、より安全で堅牢な実行を保証することを目的としています。
前提知識の解説
Goランタイムとゴルーチン
Go言語は、並行処理のプリミティブとして「ゴルーチン(goroutine)」を提供します。ゴルーチンはOSのスレッドよりもはるかに軽量であり、数百万個のゴルーチンを同時に実行することも可能です。Goランタイムは、これらのゴルーチンをOSスレッドにマッピングし、スケジューリングを行います。
Goのスタック管理
Goのゴルーチンは、OSスレッドとは独立した独自のスタックを持っています。このスタックは、必要に応じて動的にサイズが変更されます(スタックの伸縮)。これにより、メモリ効率が向上し、多数のゴルーチンを効率的に管理できます。
OSスタックとシステムコール
OSのシステムコールは、通常、OSが管理するスタック(OSスタック)上で実行されることを前提としています。Goのランタイムは、ゴルーチンがブロッキングシステムコール(例: ファイルI/O、ネットワーク通信、スリープなど)を実行する際に、現在のゴルーチンのスタックからOSスタックに切り替えることで、OSの期待する環境を提供し、他のゴルーチンの実行をブロックしないようにします。この切り替えは、Goのスケジューラ(M:NスケジューリングモデルのM)によって管理されます。
osyield
とusleep
osyield
: CPUの実行権を他のスレッドに譲渡する関数です。これにより、他の準備ができたスレッドが実行される機会を得られます。Windowsでは、NtYieldExecution
やNtWaitForSingleObject
(タイムアウト0で)などがこれに相当する機能を提供します。usleep
: 指定されたマイクロ秒(us)だけ現在のスレッドをスリープさせる関数です。Windowsでは、NtWaitForSingleObject
に負のタイムアウト値を指定することで、相対的なスリープ時間を実現できます。
アセンブリ言語とスタック操作
Goのランタイムは、パフォーマンスが重要な部分やOSとのインタフェース部分でアセンブリ言語を使用しています。スタックの切り替えやレジスタの操作は、アセンブリ言語で直接記述されることが一般的です。
SP
(Stack Pointer): 現在のスタックの最上位(または最下位、アーキテクチャによる)を指すレジスタ。BP
(Base Pointer): スタックフレームのベースを指すレジスタ。get_tls(CX)
/get_tls(R15)
: スレッドローカルストレージ(TLS)から現在のゴルーチンやM(OSスレッド)の情報を取得するマクロまたは命令。m(CX)
/m(R15)
: TLSから現在のM(OSスレッド)の構造体へのポインタを取得。m_g0(BP)
/m_g0(R14)
: M構造体からg0
(OSスタックで実行される特別なゴルーチン)へのポインタを取得。g(CX)
/g(R15)
: TLSから現在のゴルーチンへのポインタを取得。g_sched+gobuf_sp
: ゴルーチンのスケジューリング情報(gobuf
)からスタックポインタ(sp
)のオフセット。
技術的詳細
このコミットの主要な変更点は、Windowsにおけるruntime·osyield
とruntime·usleep
の実装が、直接NtWaitForSingleObject
を呼び出すのではなく、runtime·usleep1
という新しい関数を介して、OSスタックに切り替えてからシステムコールを実行するように変更されたことです。
変更前
変更前は、runtime·osyield
とruntime·usleep
は、Goのゴルーチンスタック上で直接NtWaitForSingleObject
システムコールを呼び出していました。これは、Goのスタック管理とOSのシステムコール実行の期待との間に不整合を生じさせる可能性がありました。特に、NtWaitForSingleObject
のような関数は、OSが管理するスタック上で実行されることを前提としているため、Goの伸縮するスタック上で直接呼び出すと、予期せぬ問題(例: WINE環境でのクラッシュ)が発生する可能性がありました。
変更後
-
runtime·usleep1
の導入:runtime·osyield
とruntime·usleep
は、直接システムコールを呼び出す代わりに、runtime·usleep1
という新しいアセンブリ関数を呼び出すようになりました。runtime·usleep1
は、引数としてスリープ時間(100ns単位)を受け取ります。- この関数は、まず現在のスレッドがGoによって管理されているかどうか(TLSが設定されているか)をチェックします。
- もしGoによって管理されていないスレッド(例: OSが直接起動したスレッド)であれば、スタック切り替えを行わずに直接システムコールを呼び出します。
- Goによって管理されているスレッドの場合、現在のゴルーチンが既に
m->g0
(OSスタックで実行される特別なゴルーチン)上で実行されているかをチェックします。もしそうであれば、スタック切り替えは不要です。 - それ以外の場合(通常のゴルーチンスタック上で実行されている場合)、
runtime·usleep1
は現在のスタックポインタを保存し、m->g0
のスタックに切り替えます。 - スタック切り替え後、
runtime·usleep2
という別の新しいアセンブリ関数を呼び出します。 runtime·usleep2
の実行が完了すると、元のゴルーチンスタックに戻ります。
-
runtime·usleep2
の導入:runtime·usleep2
は、OSスタック上で実行されることを想定しています。- この関数は、
NtWaitForSingleObject
システムコールを呼び出し、実際のスリープ処理を行います。 - 引数として受け取ったスリープ時間(100ns単位)を負の値に変換し、
NtWaitForSingleObject
のタイムアウト引数として渡します。Windows APIでは、負のタイムアウト値は相対的な時間を意味します。
この変更により、osyield
とusleep
がOSスタック上で安全に実行されるようになり、GoのランタイムとOSの間のインタフェースがより堅牢になりました。特に、WINEのような環境での互換性問題が解決されることが期待されます。
コアとなるコードの変更箇所
src/pkg/runtime/os_windows.c
runtime·osyield
とruntime·usleep
のC言語実装が変更され、それぞれruntime·usleep1(1)
とruntime·usleep1(10*us)
を呼び出すようになりました。runtime·usleep1
のプロトタイプ宣言が追加されました。
// 変更前:
// TEXT runtime·osyield(SB),7,$20
// ... 直接 NtWaitForSingleObject を呼び出し ...
// TEXT runtime·usleep(SB),7,$20
// ... 直接 NtWaitForSingleObject を呼び出し ...
// 変更後:
extern void runtime·usleep1(uint32);
#pragma textflag 7
void
runtime·osyield(void)
{
runtime·usleep1(1);
}
#pragma textflag 7
void
runtime·usleep(uint32 us)
{
// Have 1us units; want 100ns units.
runtime·usleep1(10*us);
}
src/pkg/runtime/sys_windows_386.s
(32-bit x86 アセンブリ)
runtime·osyield
とruntime·usleep
のアセンブリ実装が削除され、代わりにruntime·usleep1
とruntime·usleep2
が追加されました。runtime·usleep1
は、スタック切り替えロジックを含み、runtime·usleep2
を呼び出します。runtime·usleep2
は、OSスタック上でNtWaitForSingleObject
を呼び出します。
// 変更前:
// TEXT runtime·osyield(SB),7,$20
// ... NtWaitForSingleObject を直接呼び出し ...
// TEXT runtime·usleep(SB),7,$20
// ... NtWaitForSingleObject を直接呼び出し ...
// 変更後:
// Sleep duration is in 100ns units.
TEXT runtime·usleep1(SB),7,$0
MOVL duration+0(FP), BX
MOVL $runtime·usleep2(SB), AX // to hide from 8l
// Execute call on m->g0 stack, in case we are not actually
// calling a system call wrapper, like when running under WINE.
get_tls(CX)
CMPL CX, $0
JNE 3(PC)
// Not a Go-managed thread. Do not switch stack.
CALL AX
RET
MOVL m(CX), BP
MOVL m_g0(BP), SI
CMPL g(CX), SI
JNE 3(PC)
// executing on m->g0 already
CALL AX
RET
// Switch to m->g0 stack and back.
MOVL (g_sched+gobuf_sp)(SI), SI
MOVL SP, -4(SI)
LEAL -4(SI), SP
CALL AX
MOVL 0(SP), SP
RET
// Runs on OS stack. duration (in 100ns units) is in BX.
TEXT runtime·usleep2(SB),7,$20
// Want negative 100ns units.
NEGL BX
MOVL $-1, hi-4(SP)
MOVL BX, lo-8(SP)
LEAL lo-8(SP), BX
MOVL BX, ptime-12(SP)
MOVL $0, alertable-16(SP)
MOVL $-1, handle-20(SP)
MOVL SP, BP
MOVL runtime·NtWaitForSingleObject(SB), AX
CALL AX
MOVL BP, SP
RET
src/pkg/runtime/sys_windows_amd64.s
(64-bit x86 アセンブリ)
sys_windows_386.s
と同様に、runtime·osyield
とruntime·usleep
のアセンブリ実装が削除され、代わりにruntime·usleep1
とruntime·usleep2
が追加されました。- 64-bitアーキテクチャに合わせたレジスタ(R15, R14など)とスタック操作(MOVQ, NEGQなど)が使用されています。
// 変更前:
// TEXT runtime·osyield(SB),7,$8
// ... NtWaitForSingleObject を直接呼び出し ...
// TEXT runtime·usleep(SB),7,$8
// ... NtWaitForSingleObject を直接呼び出し ...
// 変更後:
// Sleep duration is in 100ns units.
TEXT runtime·usleep1(SB),7,$0
MOVL duration+0(FP), BX
MOVQ $runtime·usleep2(SB), AX // to hide from 6l
// Execute call on m->g0 stack, in case we are not actually
// calling a system call wrapper, like when running under WINE.
get_tls(R15)
CMPQ R15, $0
JNE 3(PC)
// Not a Go-managed thread. Do not switch stack.
CALL AX
RET
MOVQ m(R15), R14
MOVQ m_g0(R14), R14
CMPQ g(R15), R14
JNE 3(PC)
// executing on m->g0 already
CALL AX
RET
// Switch to m->g0 stack and back.
MOVQ (g_sched+gobuf_sp)(R14), R14
MOVQ SP, -8(R14)
LEAQ -8(R14), SP
CALL AX
MOVQ 0(SP), SP
RET
// Runs on OS stack. duration (in 100ns units) is in BX.
TEXT runtime·usleep2(SB),7,$8
// Want negative 100ns units.
NEGQ BX
MOVQ SP, R8 // ptime
MOVQ BX, (R8)
MOVQ $-1, CX // handle
MOVQ $0, DX // alertable
MOVQ runtime·NtWaitForSingleObject(SB), AX
CALL AX
RET
コアとなるコードの解説
このコミットの核心は、GoのランタイムがWindowsのシステムコールを呼び出す際のスタック管理の改善です。
-
runtime·osyield
とruntime·usleep
のC言語ラッパー:- これらの関数は、Goのコードから呼び出されるエントリポイントです。
- 以前は直接アセンブリコードにジャンプしてシステムコールを呼び出していましたが、変更後は
runtime·usleep1
という共通のアセンブリ関数を呼び出すようになりました。 runtime·usleep
はマイクロ秒単位の引数を100ナノ秒単位に変換してruntime·usleep1
に渡します。これはNtWaitForSingleObject
が100ナノ秒単位のタイムアウトを期待するためです。
-
runtime·usleep1
(アセンブリ):- この関数は、GoのスケジューラとOSの間の重要なブリッジです。
- Go管理スレッドのチェック:
get_tls(CX)
(32-bit) またはget_tls(R15)
(64-bit) を使用して、現在のスレッドがGoランタイムによって管理されているかどうかを判断します。Go管理外のスレッドであれば、スタック切り替えは不要なため、直接runtime·usleep2
(またはその実体)を呼び出します。 g0
スタックのチェック: Go管理スレッドの場合、現在のゴルーチンが既にm->g0
(OSスタックで実行される特別なゴルーチン)上で実行されているかをチェックします。もしそうであれば、既にOSスタック上にいるため、スタック切り替えは不要です。- スタック切り替え: 上記のいずれでもない場合(つまり、通常のゴルーチンスタック上で実行されている場合)、現在のスタックポインタ(
SP
)を保存し、m->g0
のスタックポインタに切り替えます。これにより、runtime·usleep2
がOSスタック上で実行されることが保証されます。 runtime·usleep2
の呼び出し: スタック切り替え後、runtime·usleep2
を呼び出します。- スタックの復元:
runtime·usleep2
の実行が完了すると、保存しておいたスタックポインタを復元し、元のゴルーチンスタックに戻ります。
-
runtime·usleep2
(アセンブリ):- この関数は、OSスタック上で実行されます。
- 引数として受け取ったスリープ時間(100ns単位)を負の値に変換します。これは、Windowsの
NtWaitForSingleObject
システムコールが相対的なタイムアウトを負の値で表現するためです。 NtWaitForSingleObject
システムコールを呼び出し、指定された時間だけスリープします。このシステムコールは、カーネルモードで実行され、OSのスケジューラに制御を渡します。
この一連の処理により、Goのランタイムは、Windowsのシステムコールが期待するOSスタック環境を提供しつつ、Goの軽量なゴルーチンモデルを維持することができます。これにより、安定性と互換性が向上します。
関連リンク
- Go Issue #5831: https://github.com/golang/go/issues/5831
- Go CL 11266043: https://golang.org/cl/11266043
参考にした情報源リンク
- Go言語の公式ドキュメント (Goランタイム、スケジューラ、スタック管理に関する情報)
- Windows APIドキュメント (NtWaitForSingleObjectに関する情報)
- Goのソースコード (特に
src/pkg/runtime
ディレクトリ内のファイル) - GoのIssueトラッカー (Issue #5831の詳細)
- Goのコードレビューシステム (CL 11266043の詳細)
- WINEプロジェクトのドキュメント (Windows API互換性レイヤーに関する情報)
- x86/x64アセンブリ言語の資料 (スタック操作、レジスタ使用に関する情報)
- GoのM, P, Gモデルに関する解説記事
- Goのシステムコールとスタック切り替えに関する技術ブログや論文