[インデックス 19662] ファイルの概要
このコミットは、Goランタイムのruntime·usleep関数とruntime·osyield関数が、cgoコールバックコンテキストから安全に呼び出せるようにするための変更です。具体的には、これらの関数がGoランタイムのM (Machine) スレッドに紐付けられていない状況(cgoコールバック時など)でも、libcの対応する関数を呼び出せるように、アセンブリラッパーを導入しています。
コミット
commit df75f082d3f18f859ddbd3d002c711a4ec507948
Author: Aram Hăvărneanu <aram@mgk.ro>
Date: Thu Jul 3 11:36:05 2014 +1000
runtime: make runtime·usleep and runtime·osyield callable from cgo callback
runtime·usleep and runtime·osyield fall back to calling an
assembly wrapper for the libc functions in the absence of a m,
so they can be called in cgo callback context.
LGTM=rsc
R=minux.ma, rsc
CC=dave, golang-codereviews
https://golang.org/cl/102620044
GitHub上でのコミットページへのリンク
https://github.com/golang/go/commit/df75f082d3f18f859ddbd3d002c711a4ec507948
元コミット内容
runtime: make runtime·usleep and runtime·osyield callable from cgo callback
runtime·usleep and runtime·osyield fall back to calling an
assembly wrapper for the libc functions in the absence of a m,
so they can be called in cgo callback context.
変更の背景
GoプログラムがC言語のコードと連携する際に使用されるcgoは、GoとCの間で関数を呼び出し合うことを可能にします。CコードからGo関数を呼び出す「cgoコールバック」のシナリオにおいて、Goランタイムの特定の関数、特にruntime·usleep(指定された時間だけスレッドを一時停止する)やruntime·osyield(現在のスレッドの実行を一時停止し、他のスレッドにCPUを譲る)が問題を引き起こす可能性がありました。
これらのGoランタイム関数は、通常、Goランタイムが管理するM (Machine) と呼ばれるOSスレッドのコンテキストで実行されることを前提としています。MはGoのスケジューラによって管理され、Goルーチン (G) の実行を担当します。しかし、cgoコールバックの際には、Goランタイムが管理するMが利用できない、あるいはまだ完全に初期化されていないOSスレッドのコンテキストでGo関数が実行されることがあります。このような状況でruntime·usleepやruntime·osyieldが直接システムコールを呼び出そうとすると、Mが存在しないためにランタイムの内部状態が不正になったり、クラッシュしたりする可能性がありました。
このコミットの目的は、cgoコールバックのような、GoランタイムのMが利用できないコンテキストでも、これらの関数が安全に、かつ期待通りに動作するようにすることです。具体的には、Mが存在しない場合には、直接libcの対応する関数(usleepやsched_yield)を呼び出すためのアセンブリラッパーを経由するように変更することで、この問題を解決しています。
前提知識の解説
Goのランタイムとスケジューラ (GMPモデル)
Goのランタイムは、Goルーチン (Goroutine: G)、論理プロセッサ (Processor: P)、OSスレッド (Machine: M) の3つの主要な要素からなるGMPモデルで並行性を管理します。
- G (Goroutine): Goにおける軽量な実行単位です。数千から数百万のGoルーチンを同時に実行できます。GoルーチンはGoランタイムによってスケジューリングされます。
- P (Processor): Goルーチンを実行するための論理的なプロセッサです。GoルーチンはP上で実行されます。Pの数は通常、CPUのコア数に設定され、Goルーチンが並行して実行できる数を制限します。
- M (Machine): OSスレッドを表します。MはPを保持し、P上でGoルーチンを実行します。Mはシステムコールを実行したり、Goルーチンをブロックしたりする際に使用されます。Goランタイムは、必要に応じてMを生成・破棄し、GoルーチンをMにディスパッチします。
通常、Goルーチンがシステムコールを呼び出す場合、そのGoルーチンはM上で実行されており、MはGoランタイムによって管理されています。しかし、cgoコールバックのような特殊なケースでは、Goランタイムが管理していないOSスレッドからGo関数が呼び出されることがあり、そのOSスレッドにはGoランタイムのMが関連付けられていない場合があります。
cgoとcgoコールバック
cgoは、GoプログラムからC言語の関数を呼び出したり、C言語の関数からGo関数を呼び出したりするためのGoの機能です。これにより、既存のCライブラリをGoから利用したり、Goの機能をCから利用したりできます。
cgoコールバックとは、C言語のコードがGo言語で実装された関数を呼び出すシナリオを指します。例えば、Cライブラリがコールバック関数を登録するAPIを提供しており、そのコールバック関数をGoで実装してCライブラリに渡すようなケースです。このとき、CライブラリがGoのコールバック関数を呼び出すOSスレッドは、Goランタイムが管理するMではない可能性があります。
システムコール usleep と sched_yield
usleep(microseconds): 指定されたマイクロ秒数だけ現在のスレッドの実行を一時停止するPOSIX標準のシステムコールです。sched_yield(): 現在実行中のスレッドがCPUを他のスレッドに譲ることをOSに要求するシステムコールです。これにより、他の準備ができたスレッドが実行される機会を得られます。
これらのシステムコールは、Goランタイムのruntime·usleepやruntime·osyieldの基盤となる機能です。
NOSPLIT ディレクティブ
Goの関数は、通常、必要に応じてスタックを動的に拡張します。これは、Goルーチンのスタックが比較的小さく開始され、必要に応じて自動的に成長するためです。しかし、一部の低レベルなランタイム関数やアセンブリ関数では、スタックの拡張処理(プロローグでのスタックチェックと拡張)が不要、あるいは望ましくない場合があります。
NOSPLITディレクティブは、Goコンパイラに対して、その関数がスタックを拡張する必要がないことを指示します。これにより、関数のプロローグからスタックチェックのコードが省略され、オーバーヘッドが削減されます。このコミットでは、アセンブリラッパー関数にNOSPLITが適用されており、これはこれらの関数が非常に低レベルであり、スタックの動的な管理を必要としないことを示唆しています。また、cgoコールバックのような特殊なコンテキストで呼ばれる可能性があるため、スタックの状況が通常のGoルーチンとは異なる場合にも安全に動作させる意図があります。
技術的詳細
このコミットの核心は、runtime·usleepとruntime·osyieldが、GoランタイムのM (Machine) が存在しないコンテキスト(特にcgoコールバック)でも安全に動作するようにすることです。
元の実装では、これらの関数は直接runtime·sysvicall6を呼び出し、これは最終的にlibcのusleepやsched_yieldを呼び出していました。runtime·sysvicall6はGoランタイムのMのコンテキストで動作することを前提としており、Mが存在しない場合に問題が発生します。
変更後の動作は以下のようになります。
-
runtime·usleepの変更:runtime·usleepは、直接libc·usleepを呼び出す代わりに、新しく導入されたアセンブリ関数runtime·usleep1を呼び出すように変更されました。runtime·usleep1はNOSPLITディレクティブを持ち、スタック拡張を行いません。runtime·usleep1の内部では、現在のスレッドがGoランタイムによって管理されているMに紐付けられたGoルーチン (g) のコンテキストで実行されているかどうかをチェックします。- もしGoランタイムのMに紐付けられた
gが存在し、かつ現在のGoルーチンがm->g0(システムコールなどを実行するための特別なGoルーチン)ではない場合、スタックスイッチが行われます。これは、runtime·usleepがGoルーチンのスタック上で呼び出された場合でも、実際のusleepシステムコールはm->g0のスタック上で実行されるようにするためです。これにより、システムコール中のスタックの安全性と、Goランタイムの整合性が保たれます。 - もしGoランタイムのMに紐付けられた
gが存在しない場合(cgoコールバックなど)、または既にm->g0上で実行されている場合は、スタックスイッチは行われず、直接runtime·usleep2が呼び出されます。
- もしGoランタイムのMに紐付けられた
runtime·usleep2は、OSスタック上で実行され、最終的にlibc·usleepを呼び出します。
-
runtime·osyieldの変更:runtime·osyieldは、まずg && g->m != nilというチェックを追加しました。- もし
gが存在し、かつgが紐付けられているmが存在する場合(通常のGoルーチンコンテキスト)、これまで通りruntime·sysvicall6を介してlibc·sched_yieldを呼び出します。 - もし
gが存在しない、またはgが紐付けられているmが存在しない場合(cgoコールバックなど)、新しく導入されたアセンブリ関数runtime·osyield1を呼び出します。 runtime·osyield1はNOSPLITディレクティブを持ち、OSスタック上で直接libc·sched_yieldを呼び出します。
この変更により、runtime·usleepとruntime·osyieldは、GoランタイムのMが利用できない状況でも、libcの対応する関数を直接呼び出すことで、安全かつ期待通りに動作するようになりました。特に、runtime·usleepにおけるスタックスイッチのロジックは、Goランタイムの内部的な整合性を保ちつつ、システムコールを安全に実行するための重要なメカニズムです。
コアとなるコードの変更箇所
src/pkg/runtime/os_solaris.c
--- a/src/pkg/runtime/os_solaris.c
+++ b/src/pkg/runtime/os_solaris.c
@@ -568,10 +568,13 @@ runtime·sysconf(int32 name)
return runtime·sysvicall6(libc·sysconf, 1, (uintptr)name);
}
+extern void runtime·usleep1(uint32);
+
+#pragma textflag NOSPLIT
void
-runtime·usleep(uint32 us)
+runtime·usleep(uint32 µs)
{
-\truntime·sysvicall6(libc·usleep, 1, (uintptr)us);\n+\truntime·usleep1(µs);
}
int32
@@ -580,8 +583,17 @@ runtime·write(uintptr fd, void* buf, int32 nbyte)
return runtime·sysvicall6(libc·write, 3, (uintptr)fd, (uintptr)buf, (uintptr)nbyte);
}
+extern void runtime·osyield1(void);
+
+#pragma textflag NOSPLIT
void
runtime·osyield(void)
{
-\truntime·sysvicall6(libc·sched_yield, 0);\n+\t// Check the validity of m because we might be called in cgo callback
+\t// path early enough where there isn't a m available yet.
+\tif(g && g->m != nil) {
+\t\truntime·sysvicall6(libc·sched_yield, 0);
+\t\treturn;
+\t}
+\truntime·osyield1();
}
src/pkg/runtime/sys_solaris_amd64.s
--- a/src/pkg/runtime/sys_solaris_amd64.s
+++ b/src/pkg/runtime/sys_solaris_amd64.s
@@ -270,3 +270,55 @@ exit:
ADDQ $184, SP
RET
+\n+// Called from runtime·usleep (Go). Can be called on Go stack, on OS stack,
+// can also be called in cgo callback path without a g->m.
+TEXT runtime·usleep1(SB),NOSPLIT,$0
+\tMOVL\tus+0(FP), DI
+\tMOVQ\t$runtime·usleep2(SB), AX // to hide from 6l
+\n+\t// Execute call on m->g0.
+\tget_tls(R15)
+\tCMPQ\tR15, $0
+\tJE\tusleep1_noswitch
+\n+\tMOVQ\tg(R15), R13
+\tCMPQ\tR13, $0
+\tJE\tusleep1_noswitch
+\tMOVQ\tg_m(R13), R13
+\tCMPQ\tR13, $0
+\tJE\tusleep1_noswitch
+\t// TODO(aram): do something about the cpu profiler here.
+\n+\tMOVQ\tm_g0(R13), R14
+\tCMPQ\tg(R15), R14
+\tJNE\tusleep1_switch
+\t// executing on m->g0 already
+\tCALL\tAX
+\tRET
+\n+usleep1_switch:
+\t// Switch to m->g0 stack and back.
+\tMOVQ\t(g_sched+gobuf_sp)(R14), R14
+\tMOVQ\tSP, -8(R14)
+\tLEAQ\t-8(R14), SP
+\tCALL\tAX
+\tMOVQ\t0(SP), SP
+\tRET
+\n+usleep1_noswitch:
+\t// Not a Go-managed thread. Do not switch stack.
+\tCALL\tAX
+\tRET
+\n+// Runs on OS stack. duration (in µs units) is in DI.
+TEXT runtime·usleep2(SB),NOSPLIT,$0
+\tMOVQ\tlibc·usleep(SB), AX
+\tCALL\tAX
+\tRET
+\n+// Runs on OS stack, called from runtime·osyield.
+TEXT runtime·osyield1(SB),NOSPLIT,$0
+\tMOVQ\tlibc·sched_yield(SB), AX
+\tCALL\tAX
+\tRET
コアとなるコードの解説
src/pkg/runtime/os_solaris.c の変更点
runtime·usleepの変更:runtime·usleep(uint32 us)の引数名がusからµsに変更されました。これはマイクロ秒を表す慣例的な記号です。- 以前は
runtime·sysvicall6(libc·usleep, 1, (uintptr)us);を直接呼び出していましたが、これがruntime·usleep1(µs);に変更されました。これにより、GoのCコードから直接libcを呼び出すのではなく、アセンブリラッパーを介するようになりました。 extern void runtime·usleep1(uint32);と#pragma textflag NOSPLITが追加され、runtime·usleep1が外部のアセンブリ関数であり、スタック拡張を行わないことが宣言されています。
runtime·osyieldの変更:extern void runtime·osyield1(void);と#pragma textflag NOSPLITが追加され、runtime·osyield1が外部のアセンブリ関数であり、スタック拡張を行わないことが宣言されています。runtime·osyieldの内部に条件分岐が追加されました。if(g && g->m != nil): これは、現在のGoルーチンgが存在し、かつそのGoルーチンがGoランタイムのM (OSスレッド) に紐付けられているかどうかをチェックしています。これは、通常のGoルーチンコンテキストで実行されていることを意味します。- この条件が真の場合、
runtime·sysvicall6(libc·sched_yield, 0);が実行されます。これは、これまで通りGoランタイムの管理下でシステムコールを呼び出すパスです。 - この条件が偽の場合(つまり、
gが存在しないか、gがMに紐付けられていない場合、例えばcgoコールバックコンテキスト)、runtime·osyield1();が呼び出されます。これにより、アセンブリラッパーを介して直接libcのsched_yieldが呼び出されます。
src/pkg/runtime/sys_solaris_amd64.s の変更点
このファイルには、新しいアセンブリ関数が追加されています。
-
TEXT runtime·usleep1(SB),NOSPLIT,$0:runtime·usleepから呼び出されるアセンブリ関数です。NOSPLITによりスタック拡張は行われません。- 引数
µs(マイクロ秒) はDIレジスタにロードされます。 runtime·usleep2のアドレスをAXレジスタにロードします。これは、runtime·usleep1が最終的に呼び出す関数です。- スタックスイッチのロジック:
get_tls(R15): スレッドローカルストレージ (TLS) から現在のGoルーチンgのポインタを取得します。CMPQ R15, $0/JE usleep1_noswitch:gが存在しない場合(TLSが設定されていない、つまりGoランタイムが管理していないスレッドの場合)、usleep1_noswitchにジャンプします。MOVQ g(R15), R13/CMPQ R13, $0/JE usleep1_noswitch:gが存在しない場合、usleep1_noswitchにジャンプします。MOVQ g_m(R13), R13/CMPQ R13, $0/JE usleep1_noswitch:gに紐付けられたMが存在しない場合、usleep1_noswitchにジャンプします。MOVQ m_g0(R13), R14: Mに紐付けられた特別なGoルーチンg0のポインタをR14にロードします。g0はシステムコールなどを実行するためのスタックを持ちます。CMPQ g(R15), R14/JNE usleep1_switch: 現在のGoルーチンがg0ではない場合、usleep1_switchにジャンプします。これは、Goルーチンのスタックからg0のスタックに切り替える必要があることを意味します。CALL AX/RET: 既にg0上で実行されている場合、直接runtime·usleep2を呼び出し、戻ります。
usleep1_switch:MOVQ (g_sched+gobuf_sp)(R14), R14:g0のスタックポインタをR14にロードします。MOVQ SP, -8(R14)/LEAQ -8(R14), SP: 現在のスタックポインタをg0のスタックに保存し、スタックポインタをg0のスタックに切り替えます。CALL AX:runtime·usleep2を呼び出します。これにより、usleepシステムコールはg0のスタック上で実行されます。MOVQ 0(SP), SP/RET:g0のスタックから元のGoルーチンのスタックに戻り、関数から戻ります。
usleep1_noswitch:CALL AX/RET: Goランタイムが管理していないスレッドの場合(gやmが存在しない場合)、スタックスイッチは行われず、直接runtime·usleep2を呼び出し、戻ります。
-
TEXT runtime·usleep2(SB),NOSPLIT,$0:runtime·usleep1から呼び出されるアセンブリ関数です。OSスタック上で実行されます。MOVQ libc·usleep(SB), AX:libc·usleepのアドレスをAXレジスタにロードします。CALL AX/RET:libc·usleepを呼び出し、戻ります。
-
TEXT runtime·osyield1(SB),NOSPLIT,$0:runtime·osyieldから呼び出されるアセンブリ関数です。OSスタック上で実行されます。MOVQ libc·sched_yield(SB), AX:libc·sched_yieldのアドレスをAXレジスタにロードします。CALL AX/RET:libc·sched_yieldを呼び出し、戻ります。
これらのアセンブリ関数は、Goランタイムの内部的な詳細を抽象化し、GoランタイムのMが存在しないコンテキストでも、低レベルなシステムコールを安全に実行するためのブリッジとして機能します。特にruntime·usleep1におけるスタックスイッチのロジックは、GoのGMPモデルとシステムコール実行の間の複雑な相互作用を管理するための重要な部分です。
関連リンク
- Go CL 102620044: https://golang.org/cl/102620044
参考にした情報源リンク
- GoのGMPスケジューラに関するドキュメントや記事 (例: Goの公式ドキュメント、ブログ記事など)
- cgoに関するGoの公式ドキュメント
- POSIX
usleepおよびsched_yieldシステムコールのマニュアルページ - Goのアセンブリ言語に関するドキュメント (例:
go doc cmd/goのgo help asmなど) - Goランタイムのソースコード (特に
src/runtimeディレクトリ)