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

[インデックス 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·usleepruntime·osyieldが直接システムコールを呼び出そうとすると、Mが存在しないためにランタイムの内部状態が不正になったり、クラッシュしたりする可能性がありました。

このコミットの目的は、cgoコールバックのような、GoランタイムのMが利用できないコンテキストでも、これらの関数が安全に、かつ期待通りに動作するようにすることです。具体的には、Mが存在しない場合には、直接libcの対応する関数(usleepsched_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ではない可能性があります。

システムコール usleepsched_yield

  • usleep(microseconds): 指定されたマイクロ秒数だけ現在のスレッドの実行を一時停止するPOSIX標準のシステムコールです。
  • sched_yield(): 現在実行中のスレッドがCPUを他のスレッドに譲ることをOSに要求するシステムコールです。これにより、他の準備ができたスレッドが実行される機会を得られます。

これらのシステムコールは、Goランタイムのruntime·usleepruntime·osyieldの基盤となる機能です。

NOSPLIT ディレクティブ

Goの関数は、通常、必要に応じてスタックを動的に拡張します。これは、Goルーチンのスタックが比較的小さく開始され、必要に応じて自動的に成長するためです。しかし、一部の低レベルなランタイム関数やアセンブリ関数では、スタックの拡張処理(プロローグでのスタックチェックと拡張)が不要、あるいは望ましくない場合があります。

NOSPLITディレクティブは、Goコンパイラに対して、その関数がスタックを拡張する必要がないことを指示します。これにより、関数のプロローグからスタックチェックのコードが省略され、オーバーヘッドが削減されます。このコミットでは、アセンブリラッパー関数にNOSPLITが適用されており、これはこれらの関数が非常に低レベルであり、スタックの動的な管理を必要としないことを示唆しています。また、cgoコールバックのような特殊なコンテキストで呼ばれる可能性があるため、スタックの状況が通常のGoルーチンとは異なる場合にも安全に動作させる意図があります。

技術的詳細

このコミットの核心は、runtime·usleepruntime·osyieldが、GoランタイムのM (Machine) が存在しないコンテキスト(特にcgoコールバック)でも安全に動作するようにすることです。

元の実装では、これらの関数は直接runtime·sysvicall6を呼び出し、これは最終的にlibcのusleepsched_yieldを呼び出していました。runtime·sysvicall6はGoランタイムのMのコンテキストで動作することを前提としており、Mが存在しない場合に問題が発生します。

変更後の動作は以下のようになります。

  1. runtime·usleepの変更:

    • runtime·usleepは、直接libc·usleepを呼び出す代わりに、新しく導入されたアセンブリ関数runtime·usleep1を呼び出すように変更されました。
    • runtime·usleep1NOSPLITディレクティブを持ち、スタック拡張を行いません。
    • 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が呼び出されます。
    • runtime·usleep2は、OSスタック上で実行され、最終的にlibc·usleepを呼び出します。
  2. runtime·osyieldの変更:

    • runtime·osyieldは、まずg && g->m != nilというチェックを追加しました。
    • もしgが存在し、かつgが紐付けられているmが存在する場合(通常のGoルーチンコンテキスト)、これまで通りruntime·sysvicall6を介してlibc·sched_yieldを呼び出します。
    • もしgが存在しない、またはgが紐付けられているmが存在しない場合(cgoコールバックなど)、新しく導入されたアセンブリ関数runtime·osyield1を呼び出します。
    • runtime·osyield1NOSPLITディレクティブを持ち、OSスタック上で直接libc·sched_yieldを呼び出します。

この変更により、runtime·usleepruntime·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ランタイムが管理していないスレッドの場合(gm が存在しない場合)、スタックスイッチは行われず、直接 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のGMPスケジューラに関するドキュメントや記事 (例: Goの公式ドキュメント、ブログ記事など)
  • cgoに関するGoの公式ドキュメント
  • POSIX usleep および sched_yield システムコールのマニュアルページ
  • Goのアセンブリ言語に関するドキュメント (例: go doc cmd/gogo help asmなど)
  • Goランタイムのソースコード (特にsrc/runtimeディレクトリ)