[インデックス 15530] ファイルの概要
このコミットは、Go言語のランタイムにおける新しいスケジューラがFreeBSDおよびWindows環境で正しく動作しない問題を修正するものです。具体的には、新しいOSスレッドが起動される際に、そのスレッドが実行すべき開始関数が正しく渡されない、または取得されないという問題に対処しています。この修正は、スレッドローカルストレージ(TLS)を介して開始関数を渡すメカニズムを導入し、各OSおよびアーキテクチャ固有のアセンブリコードとCコードを更新することで実現されています。
コミット
- コミットハッシュ:
c5f694a5c9d210b83b82f52931e1d46b3e25393d
- Author: Russ Cox rsc@golang.org
- Date: Fri Mar 1 08:30:11 2013 -0500
- コミットメッセージ:
runtime: fix new scheduler on freebsd, windows R=devon.odell CC=golang-dev https://golang.org/cl/7443046
GitHub上でのコミットページへのリンク
https://github.com/golang/go/commit/c5f694a5c9d210b83b82f52931e1d46b3e25393d
元コミット内容
runtime: fix new scheduler on freebsd, windows
R=devon.odell
CC=golang-dev
https://golang.org/cl/7443046
変更の背景
Go言語のランタイムは、ゴルーチン(Goroutine)と呼ばれる軽量な並行処理単位を効率的にスケジューリングするために、独自のスケジューラを持っています。このコミットが作成された2013年頃は、Goランタイムのスケジューラが大きく進化していた時期であり、特に「新しいスケジューラ」への移行が進められていました。
新しいスケジューラは、より効率的なゴルーチンの管理とOSスレッドへのマッピングを目指して設計されましたが、その導入に伴い、特定のOS(FreeBSDとWindows)で問題が発生しました。具体的には、GoランタイムがOSに対して新しいスレッド(M: Machine)の作成を要求する際に、その新しいスレッドが起動後に実行すべき初期関数(通常はruntime.mstart
)が正しく渡されず、結果としてスレッドが意図した処理を開始できない、あるいはクラッシュするという問題が発生していました。
この問題は、OSスレッドの起動メカニズムと、Goランタイムがスレッド間で情報を共有する方法(特にスレッドローカルストレージ、TLS)の間の不整合に起因していました。FreeBSDとWindowsの特定のアーキテクチャ(386, amd64, arm)において、新しいスレッドが起動時に必要なコンテキスト(この場合は開始関数へのポインタ)を適切に取得できないため、ランタイムの初期化が失敗し、Goプログラムが正常に動作しない状況が生じていました。このコミットは、このOSとアーキテクチャに特有の起動シーケンスにおける情報の受け渡しを修正することで、新しいスケジューラがこれらの環境でも安定して動作するようにすることを目的としています。
前提知識の解説
GoランタイムのM-P-Gモデル
Go言語の並行処理は、以下の3つの主要な抽象化によって管理されます。
- G (Goroutine): Go言語における軽量な実行単位です。数千から数百万のゴルーチンを同時に実行できます。Goの関数呼び出しの前に
go
キーワードを付けることで作成されます。 - M (Machine/OS Thread): オペレーティングシステムのスレッドです。Goランタイムは、ゴルーチンを実行するためにOSスレッドを使用します。MはOSスケジューラによって管理され、CPUコア上で実際にコードを実行します。
- P (Processor): 論理プロセッサです。MとGの間の仲介役として機能します。Pは、実行可能なゴルーチンのキューを保持し、Mにゴルーチンを供給します。Goランタイムは、利用可能なCPUコアの数に応じてPの数を調整します(通常は
GOMAXPROCS
環境変数で設定)。
M-P-Gモデルの基本的な流れは以下の通りです。
- Goランタイムは、Pの数だけM(OSスレッド)を作成します。
- 各MはPにアタッチされ、Pからゴルーチンを取得して実行します。
- ゴルーチンがシステムコールなどでブロックされると、MはPからデタッチされ、別のMがそのPにアタッチされて他のゴルーチンを実行できます。
- 新しいゴルーチンが作成されると、Pのキューに追加されます。
このコミットは、特に新しいM(OSスレッド)が起動される際の初期化プロセスに関わるものです。
スレッドローカルストレージ (TLS)
スレッドローカルストレージ(Thread Local Storage, TLS)は、マルチスレッド環境において、各スレッドがそれぞれ独立したデータを持つためのメカニズムです。通常、グローバル変数や静的変数はすべてのスレッドで共有されますが、TLSを使用すると、同じ変数名であっても各スレッドが独自のコピーを持つことができます。
Goランタイムでは、M(OSスレッド)に関連する特定の情報(例えば、現在のゴルーチンへのポインタ、MのIDなど)を効率的に管理するためにTLSを利用します。これにより、OSスレッドが切り替わっても、そのスレッド固有のコンテキストを迅速に参照できるようになります。
TLSの実装はOSやアーキテクチャによって異なります。例えば、x86アーキテクチャでは、特定のレジスタ(FSまたはGSセグメントレジスタ)がTLSベースアドレスを指すように設定され、オフセットを使ってTLSデータにアクセスします。
アセンブリ言語の基本
このコミットには、386 (x86), amd64 (x86-64), ARMアーキテクチャ向けのアセンブリコードが含まれています。Goランタイムの低レベルな部分は、OSとのインタラクションやパフォーマンスクリティカルな処理のためにアセンブリ言語で記述されています。
TEXT
: 関数の開始を宣言します。MOVL
,MOVQ
,MOVW
: データを移動する命令です。L
はLong (32ビット)、Q
はQuad (64ビット)、W
はWord (16ビット) を示します。CALL
: 関数を呼び出す命令です。SB
: シンボルベース(Symbol Base)を意味し、グローバルシンボルを参照する際に使用されます。get_tls(CX)
: TLSベースアドレスをレジスタCX
にロードするマクロまたは命令シーケンスです。m(CX)
:CX
レジスタが指すアドレスからのオフセットで、M構造体内のフィールドにアクセスします。
これらのアセンブリコードは、新しいOSスレッドが起動した際に、TLSから必要な情報を読み取り、適切な開始関数を呼び出す役割を担っています。
技術的詳細
このコミットの核心は、Goランタイムが新しいOSスレッドを起動する際に、そのスレッドが実行を開始すべき関数(runtime.mstart
など)を、OSスレッドのコンテキスト内で正しく取得できるようにすることです。
以前の実装では、runtime.newosproc
関数が新しいOSスレッドを作成する際に、開始関数へのポインタを直接渡すか、あるいは暗黙的にruntime.mstart
が呼び出されることを期待していました。しかし、FreeBSDやWindowsのような特定のOS環境では、スレッドの起動メカニズムが異なり、この直接的な方法では開始関数が正しく認識されない、または実行されない問題がありました。
この修正では、以下の技術的なアプローチが取られています。
-
TLS (Thread Local Storage) の活用:
runtime.h
のM
構造体において、tls
フィールドのサイズがuint32 tls[8]
からuint64 tls[4]
に変更されました。これは、64ビットアーキテクチャでのポインタ格納に対応するため、およびTLSの利用方法の変更を示唆しています。runtime.newosproc
関数(thread_freebsd.c
,thread_windows.c
)内で、新しいOSスレッドが起動する前に、実行すべき開始関数へのポインタ(fn
)をmp->tls[2]
に明示的に格納するように変更されました。mp
は新しく作成されるM(OSスレッド)の構造体へのポインタです。これにより、新しく起動するスレッドが自身のTLSからこの関数ポインタを取得できるようになります。
-
アセンブリコードの修正:
- FreeBSD (386, amd64, arm) および Windows (386, amd64) 向けの
sys_*.s
ファイル(スレッドの開始点となるアセンブリコード)が修正されました。 - これらのアセンブリコードでは、スレッドが起動した直後に、TLSから
mp->tls[2]
に格納された関数ポインタを読み出し、その関数をCALL
命令で呼び出すように変更されています。 - 例えば、
sys_freebsd_386.s
では、以前は直接CALL runtime·mstart(SB)
を呼び出していましたが、修正後はget_tls(CX)
でTLSベースアドレスを取得し、MOVQ 8(CX), AX
でTLSのオフセット8(mp->tls[2]
に相当)から関数ポインタをAX
レジスタにロードし、CALL AX
でその関数を間接的に呼び出すようになっています。
- FreeBSD (386, amd64, arm) および Windows (386, amd64) 向けの
-
USED
マクロの削除と検証の追加:thread_freebsd.c
とthread_windows.c
では、USED(fn)
やUSED(gp)
といったマクロが削除され、代わりにif(gp != mp->g0) runtime·throw("invalid newosproc gp");
のような明示的な検証が追加されています。これは、newosproc
が常にmp->g0
(Mのゼロゴルーチン)とmstart
関数で呼び出されるという前提が、TLSを介した関数ポインタの受け渡しによってより柔軟になったことを示唆しています。
これらの変更により、GoランタイムはOSスレッドの起動時に、実行すべき開始関数をTLSというOSスレッド固有の安全な場所に格納し、起動後のアセンブリコードでそれを正確に取得して実行できるようになりました。これにより、FreeBSDとWindows環境における新しいスケジューラの安定性が向上しました。
コアとなるコードの変更箇所
このコミットにおける主要なコード変更は、以下のファイルに集中しています。
-
src/pkg/runtime/runtime.h
:M
構造体内のtls
フィールドの型とサイズが変更されました。--- a/src/pkg/runtime/runtime.h +++ b/src/pkg/runtime/runtime.h @@ -265,7 +265,7 @@ struct M uintptr cret; // return value from C uint64 procid; // for debuggers, but offset not hard-coded G* gsignal; // signal-handling G - uint32 tls[8]; // thread-local storage (for 386 extern register) + uint64 tls[4]; // thread-local storage (for x86 extern register) G* curg; // current running goroutine P* p; // attached P for executing Go code (nil if not executing Go code) P* nextp;
uint32 tls[8]
(32ビット値が8つ、合計32バイト) からuint64 tls[4]
(64ビット値が4つ、合計32バイト) へと変更されています。これは、64ビットシステムでのポインタ格納に適応するため、およびTLSの利用方法の変更を反映しています。
-
src/pkg/runtime/sys_freebsd_386.s
:runtime·thr_start
関数内で、runtime·mstart
を直接呼び出す代わりに、TLSから開始関数を取得して呼び出すように変更されました。--- a/src/pkg/runtime/sys_freebsd_386.s +++ b/src/pkg/runtime/sys_freebsd_386.s @@ -38,7 +38,12 @@ TEXT runtime·thr_start(SB),7,$0 MOVL AX, m(CX) CALL runtime·stackcheck(SB) // smashes AX - CALL runtime·mstart(SB) + + // newosproc left the function we should call in mp->tls[2] for us. + get_tls(CX) + MOVQ 8(CX), AX + CALL AX + MOVL 0, AX // crash (not reached)
-
src/pkg/runtime/sys_freebsd_amd64.s
:runtime·thr_start
関数内で、同様にTLSから開始関数を取得して呼び出すように変更されました。--- a/src/pkg/runtime/sys_freebsd_amd64.s +++ b/src/pkg/runtime/sys_freebsd_amd64.s @@ -34,12 +34,18 @@ TEXT runtime·thr_start(SB),7,$0 // set up m, g get_tls(CX) + MOVQ 8(CX), AX MOVQ R13, m(CX) MOVQ m_g0(R13), DI MOVQ DI, g(CX) CALL runtime·stackcheck(SB) - CALL runtime·mstart(SB) + + // newosproc left the function we should call in mp->tls[2] for us. + get_tls(CX) + MOVQ 16(CX), AX + CALL AX + MOVQ 0, AX // crash (not reached)
MOVQ 8(CX), AX
とMOVQ 16(CX), AX
の違いは、TLSのオフセットがアーキテクチャやTLSのレイアウトによって異なるためです。
-
src/pkg/runtime/sys_freebsd_arm.s
:runtime·thr_start
関数内で、TLSから開始関数を取得して呼び出すように変更されました。--- a/src/pkg/runtime/sys_freebsd_arm.s +++ b/src/pkg/runtime/sys_freebsd_arm.s @@ -33,7 +33,11 @@ TEXT runtime·thr_start(SB),7,$0 // set up g MOVW m_g0(R9), R10 BL runtime·emptyfunc(SB) // fault if stack check is wrong - BL runtime·mstart(SB) + + // newosproc left the function we should call in mp->tls[2] for us. + MOVW (m_tls+8)(m), R0 + BL (R0) + MOVW $2, R9 // crash (not reached) MOVW R9, (R9)\n \tRET
-
src/pkg/runtime/sys_windows_386.s
:runtime·tstart
関数内で、TLSから開始関数を取得して呼び出すように変更されました。--- a/src/pkg/runtime/sys_windows_386.s +++ b/src/pkg/runtime/sys_windows_386.s @@ -260,7 +260,10 @@ TEXT runtime·tstart(SB),7,$0 CALL runtime·stackcheck(SB) // clobbers AX,CX - CALL runtime·mstart(SB) + // start function is in tls[2] + get_tls(CX) + MOVL 8(CX), AX + CALL AX RET
-
src/pkg/runtime/sys_windows_amd64.s
:runtime·tstart_stdcall
関数内で、TLSから開始関数を取得して呼び出すように変更されました。--- a/src/pkg/runtime/sys_windows_amd64.s +++ b/src/pkg/runtime/sys_windows_amd64.s @@ -329,7 +329,11 @@ TEXT runtime·tstart_stdcall(SB),7,$0 CLD CALL runtime·stackcheck(SB) // clobbers AX,CX - CALL runtime·mstart(SB) + + // start function is in tls[2] + get_tls(CX) + MOVQ 16(CX), AX + CALL AX XORL AX, AX // return 0 == success RET
-
src/pkg/runtime/thread_freebsd.c
:runtime·newosproc
関数内で、開始関数fn
をmp->tls[2]
に格納する行が追加されました。また、USED(fn)
とUSED(gp)
が削除され、gp
の検証が追加されました。--- a/src/pkg/runtime/thread_freebsd.c +++ b/src/pkg/runtime/thread_freebsd.c @@ -82,12 +82,13 @@ runtime·newosproc(M *mp, G *gp, void *stk, void (*fn)(void))\n ThrParam param;\n Sigset oset;\n \n - USED(fn); // thr_start assumes fn == mstart\n - USED(gp); // thr_start assumes gp == mp->g0\n + // thr_start assumes gp == mp->g0\n + if(gp != mp->g0)\n + runtime·throw("invalid newosproc gp");\n \n if(0){\n runtime·printf("newosproc stk=%p m=%p g=%p fn=%p id=%d/%d ostk=%p\\n",\n - stk, mp, gp, fn, mp->id, mp->tls[0], &mp);\n + stk, mp, gp, fn, mp->id, (int32)mp->tls[0], &mp);\n }\n \n runtime·sigprocmask(&sigset_all, &oset);\n @@ -103,6 +104,7 @@ runtime·newosproc(M *mp, G *gp, void *stk, void (*fn)(void))\n param.tls_size = sizeof mp->tls;\n \n mp->tls[0] = mp->id; // so 386 asm can find it\n + mp->tls[2] = (uintptr)fn;\n \n runtime·thr_new(¶m, sizeof param);\n runtime·sigprocmask(&oset, nil);\n ```
-
src/pkg/runtime/thread_windows.c
:runtime·newosproc
関数内で、開始関数fn
をmp->tls[2]
に格納する行が追加されました。また、USED(fn)
とUSED(gp)
が削除され、gp
の検証が追加されました。--- a/src/pkg/runtime/thread_windows.c +++ b/src/pkg/runtime/thread_windows.c @@ -192,8 +192,12 @@ runtime·newosproc(M *mp, G *gp, void *stk, void (*fn)(void))\n void *thandle;\n \n USED(stk);\n - USED(gp); // assuming gp = mp->g0\n - USED(fn); // assuming fn = mstart\n +\n + // assume gp == mp->g0\n + if(gp != mp->g0)\n + runtime·throw("invalid newosproc gp");\n +\n + mp->tls[2] = (uintptr)fn;\n \n thandle = runtime·stdcall(runtime·CreateThread, 6,\n \tnil, (uintptr)0x20000, runtime·tstart_stdcall, mp,\n ```
その他のthread_*.c
ファイル(darwin, linux, netbsd, openbsd, plan9)では、printf
のフォーマット文字列がmp->tls[0]
をint32
としてキャストするように変更されていますが、これはデバッグ出力の修正であり、本質的な機能変更ではありません。
コアとなるコードの解説
このコミットの核となる変更は、Goランタイムが新しいOSスレッドを起動する際に、そのスレッドが実行を開始すべき関数へのポインタを、スレッドローカルストレージ(TLS)を介して安全かつ確実に渡すメカニズムを確立した点にあります。
-
runtime.h
の変更:M
構造体のtls
フィールドがuint32 tls[8]
からuint64 tls[4]
に変更されたのは、主に64ビットアーキテクチャでポインタ(アドレス)を格納するためにuint64
を使用するためです。これにより、TLSに格納できるデータの種類とサイズがより柔軟になります。tls[2]
というインデックスが使用されていることから、TLSの特定のオフセットが開始関数ポインタの格納場所として予約されたことがわかります。
-
thread_freebsd.c
およびthread_windows.c
の変更:runtime·newosproc
関数は、GoランタイムがOSに対して新しいスレッドの作成を要求する際に呼び出されます。この関数内で、mp->tls[2] = (uintptr)fn;
という行が追加されました。- ここで、
fn
は新しく作成されるOSスレッドが起動後に実行すべき関数(通常はruntime.mstart
)へのポインタです。このポインタが、新しく作成されるM(OSスレッド)のTLS領域の特定のオフセット(インデックス2)に格納されます。 - これにより、OSスレッドが起動した際に、そのスレッド自身のTLS領域からこの関数ポインタを読み出すことが可能になります。
- また、
USED(fn)
やUSED(gp)
といったマクロが削除され、if(gp != mp->g0) runtime·throw("invalid newosproc gp");
のような明示的な検証が追加されたのは、TLSを介して関数ポインタを渡すことで、newosproc
が常に特定の関数(mstart
)とゴルーチン(mp->g0
)で呼び出されるという暗黙の前提が不要になったためです。これにより、コードの意図がより明確になり、将来的な変更にも対応しやすくなります。
-
sys_freebsd_*.s
およびsys_windows_*.s
のアセンブリコードの変更:- これらのファイルには、各OSおよびアーキテクチャにおける新しいOSスレッドの実際のエントリポイント(
runtime·thr_start
やruntime·tstart
)が記述されています。 - 以前は、これらのエントリポイントから直接
CALL runtime·mstart(SB)
のようにruntime.mstart
関数を呼び出していました。しかし、これはOSスレッドの起動メカニズムによっては、runtime.mstart
が期待通りに実行されない原因となっていました。 - 修正後、アセンブリコードは以下の手順を踏みます。
get_tls(CX)
: 現在のスレッドのTLSベースアドレスをCX
レジスタにロードします。MOVQ 8(CX), AX
(またはMOVQ 16(CX), AX
,MOVW (m_tls+8)(m), R0
):CX
レジスタが指すTLSベースアドレスから、特定のオフセット(mp->tls[2]
に相当する位置)にある64ビット値(関数ポインタ)をAX
レジスタ(またはR0
レジスタ)にロードします。このオフセットはアーキテクチャによって異なります。CALL AX
(またはBL (R0)
):AX
レジスタ(またはR0
レジスタ)に格納されたアドレス(つまり、runtime·newosproc
でTLSに格納された開始関数へのポインタ)を間接的に呼び出します。
- これらのファイルには、各OSおよびアーキテクチャにおける新しいOSスレッドの実際のエントリポイント(
この一連の変更により、Goランタイムは、新しいOSスレッドが起動する際に、そのスレッドが実行すべき初期関数をTLSというスレッド固有の安全な場所に格納し、起動後のアセンブリコードでその情報を正確に取得して実行できるようになりました。これにより、FreeBSDとWindows環境におけるGoランタイムの新しいスケジューラの安定性と信頼性が大幅に向上しました。
関連リンク
- Go CL 7443046: https://golang.org/cl/7443046
参考にした情報源リンク
- Go's Concurrency Model: Goroutines and the Go Scheduler: https://go.dev/doc/effective_go#concurrency
- Go Scheduler: M-P-G Model Explained: https://medium.com/a-journey-with-go/go-scheduler-m-p-g-model-explained-71fdd24a1c2f
- Thread-Local Storage (TLS) in C/C++: https://en.wikipedia.org/wiki/Thread-local_storage
- x86 Assembly Language Reference: https://www.felixcloutier.com/x86/
- ARM Assembly Language Reference: (General ARM assembly resources, e.g., ARM Architecture Reference Manual)
- Go runtime source code (relevant files):
src/runtime/runtime.h
(nowsrc/runtime/runtime2.go
or similar in newer Go versions)src/runtime/sys_freebsd_386.s
(and othersys_*.s
files)src/runtime/thread_freebsd.c
(and otherthread_*.c
files)```markdown
[インデックス 15530] ファイルの概要
このコミットは、Go言語のランタイムにおける新しいスケジューラがFreeBSDおよびWindows環境で正しく動作しない問題を修正するものです。具体的には、新しいOSスレッドが起動される際に、そのスレッドが実行すべき開始関数が正しく渡されない、または取得されないという問題に対処しています。この修正は、スレッドローカルストレージ(TLS)を介して開始関数を渡すメカニズムを導入し、各OSおよびアーキテクチャ固有のアセンブリコードとCコードを更新することで実現されています。
コミット
- コミットハッシュ:
c5f694a5c9d210b83b82f52931e1d46b3e25393d
- Author: Russ Cox rsc@golang.org
- Date: Fri Mar 1 08:30:11 2013 -0500
- コミットメッセージ:
runtime: fix new scheduler on freebsd, windows R=devon.odell CC=golang-dev https://golang.org/cl/7443046
GitHub上でのコミットページへのリンク
https://github.com/golang/go/commit/c5f694a5c9d210b83b82f52931e1d46b3e25393d
元コミット内容
runtime: fix new scheduler on freebsd, windows
R=devon.odell
CC=golang-dev
https://golang.org/cl/7443046
変更の背景
Go言語のランタイムは、ゴルーチン(Goroutine)と呼ばれる軽量な並行処理単位を効率的にスケジューリングするために、独自のスケジューラを持っています。このコミットが作成された2013年頃は、Goランタイムのスケジューラが大きく進化していた時期であり、特に「新しいスケジューラ」への移行が進められていました。
新しいスケジューラは、より効率的なゴルーチンの管理とOSスレッドへのマッピングを目指して設計されましたが、その導入に伴い、特定のOS(FreeBSDとWindows)で問題が発生しました。具体的には、GoランタイムがOSに対して新しいスレッド(M: Machine)の作成を要求する際に、その新しいスレッドが起動後に実行すべき初期関数(通常はruntime.mstart
)が正しく渡されない、あるいはクラッシュするという問題が発生していました。
この問題は、OSスレッドの起動メカニズムと、Goランタイムがスレッド間で情報を共有する方法(特にスレッドローカルストレージ、TLS)の間の不整合に起因していました。FreeBSDとWindowsの特定のアーキテクチャ(386, amd64, arm)において、新しいスレッドが起動時に必要なコンテキスト(この場合は開始関数へのポインタ)を適切に取得できないため、ランタイムの初期化が失敗し、Goプログラムが正常に動作しない状況が生じていました。このコミットは、このOSとアーキテクチャに特有の起動シーケンスにおける情報の受け渡しを修正することで、新しいスケジューラがこれらの環境でも安定して動作するようにすることを目的としています。
前提知識の解説
GoランタイムのM-P-Gモデル
Go言語の並行処理は、以下の3つの主要な抽象化によって管理されます。
- G (Goroutine): Go言語における軽量な実行単位です。数千から数百万のゴルーチンを同時に実行できます。Goの関数呼び出しの前に
go
キーワードを付けることで作成されます。 - M (Machine/OS Thread): オペレーティングシステムのスレッドです。Goランタイムは、ゴルーチンを実行するためにOSスレッドを使用します。MはOSスケジューラによって管理され、CPUコア上で実際にコードを実行します。
- P (Processor): 論理プロセッサです。MとGの間の仲介役として機能します。Pは、実行可能なゴルーチンのキューを保持し、Mにゴルーチンを供給します。Goランタイムは、利用可能なCPUコアの数に応じてPの数を調整します(通常は
GOMAXPROCS
環境変数で設定)。
M-P-Gモデルの基本的な流れは以下の通りです。
- Goランタイムは、Pの数だけM(OSスレッド)を作成します。
- 各MはPにアタッチされ、Pからゴルーチンを取得して実行します。
- ゴルーチンがシステムコールなどでブロックされると、MはPからデタッチされ、別のMがそのPにアタッチされて他のゴルーチンを実行できます。
- 新しいゴルーチンが作成されると、Pのキューに追加されます。
このコミットは、特に新しいM(OSスレッド)が起動される際の初期化プロセスに関わるものです。
スレッドローカルストレージ (TLS)
スレッドローカルストレージ(Thread Local Storage, TLS)は、マルチスレッド環境において、各スレッドがそれぞれ独立したデータを持つためのメカニズムです。通常、グローバル変数や静的変数はすべてのスレッドで共有されますが、TLSを使用すると、同じ変数名であっても各スレッドが独自のコピーを持つことができます。
Goランタイムでは、M(OSスレッド)に関連する特定の情報(例えば、現在のゴルーチンへのポインタ、MのIDなど)を効率的に管理するためにTLSを利用します。これにより、OSスレッドが切り替わっても、そのスレッド固有のコンテキストを迅速に参照できるようになります。
TLSの実装はOSやアーキテクチャによって異なります。例えば、x86アーキテクチャでは、特定のレジスタ(FSまたはGSセグメントレジスタ)がTLSベースアドレスを指すように設定され、オフセットを使ってTLSデータにアクセスします。
アセンブリ言語の基本
このコミットには、386 (x86), amd64 (x86-64), ARMアーキテクチャ向けのアセンブリコードが含まれています。Goランタイムの低レベルな部分は、OSとのインタラクションやパフォーマンスクリティカルな処理のためにアセンブリ言語で記述されています。
TEXT
: 関数の開始を宣言します。MOVL
,MOVQ
,MOVW
: データを移動する命令です。L
はLong (32ビット)、Q
はQuad (64ビット)、W
はWord (16ビット) を示します。CALL
: 関数を呼び出す命令です。SB
: シンボルベース(Symbol Base)を意味し、グローバルシンボルを参照する際に使用されます。get_tls(CX)
: TLSベースアドレスをレジスタCX
にロードするマクロまたは命令シーケンスです。m(CX)
:CX
レジスタが指すアドレスからのオフセットで、M構造体内のフィールドにアクセスします。
これらのアセンブリコードは、新しいOSスレッドが起動した際に、TLSから必要な情報を読み取り、適切な開始関数を呼び出す役割を担っています。
技術的詳細
このコミットの核心は、Goランタイムが新しいOSスレッドを起動する際に、そのスレッドが実行を開始すべき関数(runtime.mstart
など)を、OSスレッドのコンテキスト内で正しく取得できるようにすることです。
以前の実装では、runtime.newosproc
関数が新しいOSスレッドを作成する際に、開始関数へのポインタを直接渡すか、あるいは暗黙的にruntime.mstart
が呼び出されることを期待していました。しかし、FreeBSDやWindowsのような特定のOS環境では、スレッドの起動メカニズムが異なり、この直接的な方法では開始関数が正しく認識されない、または実行されない問題がありました。
この修正では、以下の技術的なアプローチが取られています。
-
TLS (Thread Local Storage) の活用:
runtime.h
のM
構造体において、tls
フィールドのサイズがuint32 tls[8]
からuint64 tls[4]
に変更されました。これは、64ビットアーキテクチャでのポインタ格納に対応するため、およびTLSの利用方法の変更を示唆しています。runtime.newosproc
関数(thread_freebsd.c
,thread_windows.c
)内で、新しいOSスレッドが起動する前に、実行すべき開始関数へのポインタ(fn
)をmp->tls[2]
に明示的に格納するように変更されました。mp
は新しく作成されるM(OSスレッド)の構造体へのポインタです。これにより、新しく起動するスレッドが自身のTLSからこの関数ポインタを取得できるようになります。
-
アセンブリコードの修正:
- FreeBSD (386, amd64, arm) および Windows (386, amd64) 向けの
sys_*.s
ファイル(スレッドの開始点となるアセンブリコード)が修正されました。 - これらのアセンブリコードでは、スレッドが起動した直後に、TLSから
mp->tls[2]
に格納された関数ポインタを読み出し、その関数をCALL
命令で呼び出すように変更されています。 - 例えば、
sys_freebsd_386.s
では、以前は直接CALL runtime·mstart(SB)
を呼び出していましたが、修正後はget_tls(CX)
でTLSベースアドレスを取得し、MOVQ 8(CX), AX
でTLSのオフセット8(mp->tls[2]
に相当)から関数ポインタをAX
レジスタにロードし、CALL AX
でその関数を間接的に呼び出すようになっています。
- FreeBSD (386, amd64, arm) および Windows (386, amd64) 向けの
-
USED
マクロの削除と検証の追加:thread_freebsd.c
とthread_windows.c
では、USED(fn)
やUSED(gp)
といったマクロが削除され、代わりにif(gp != mp->g0) runtime·throw("invalid newosproc gp");
のような明示的な検証が追加されています。これは、TLSを介した関数ポインタの受け渡しによって、newosproc
が常にmp->g0
(Mのゼロゴルーチン)とmstart
関数で呼び出されるという前提がより柔軟になったことを示唆しています。
これらの変更により、GoランタイムはOSスレッドの起動時に、実行すべき開始関数をTLSというOSスレッド固有の安全な場所に格納し、起動後のアセンブリコードでそれを正確に取得して実行できるようになりました。これにより、FreeBSDとWindows環境における新しいスケジューラの安定性が向上しました。
コアとなるコードの変更箇所
このコミットにおける主要なコード変更は、以下のファイルに集中しています。
-
src/pkg/runtime/runtime.h
:M
構造体内のtls
フィールドの型とサイズが変更されました。--- a/src/pkg/runtime/runtime.h +++ b/src/pkg/runtime/runtime.h @@ -265,7 +265,7 @@ struct M uintptr cret; // return value from C uint64 procid; // for debuggers, but offset not hard-coded G* gsignal; // signal-handling G - uint32 tls[8]; // thread-local storage (for 386 extern register) + uint64 tls[4]; // thread-local storage (for x86 extern register) G* curg; // current running goroutine P* p; // attached P for executing Go code (nil if not executing Go code) P* nextp;
uint32 tls[8]
(32ビット値が8つ、合計32バイト) からuint64 tls[4]
(64ビット値が4つ、合計32バイト) へと変更されています。これは、64ビットシステムでのポインタ格納に適応するため、およびTLSの利用方法の変更を反映しています。
-
src/pkg/runtime/sys_freebsd_386.s
:runtime·thr_start
関数内で、runtime·mstart
を直接呼び出す代わりに、TLSから開始関数を取得して呼び出すように変更されました。--- a/src/pkg/runtime/sys_freebsd_386.s +++ b/src/pkg/runtime/sys_freebsd_386.s @@ -38,7 +38,12 @@ TEXT runtime·thr_start(SB),7,$0 MOVL AX, m(CX) CALL runtime·stackcheck(SB) // smashes AX - CALL runtime·mstart(SB) + + // newosproc left the function we should call in mp->tls[2] for us. + get_tls(CX) + MOVQ 8(CX), AX + CALL AX + MOVL 0, AX // crash (not reached)
-
src/pkg/runtime/sys_freebsd_amd64.s
:runtime·thr_start
関数内で、同様にTLSから開始関数を取得して呼び出すように変更されました。--- a/src/pkg/runtime/sys_freebsd_amd64.s +++ b/src/pkg/runtime/sys_freebsd_amd64.s @@ -34,12 +34,18 @@ TEXT runtime·thr_start(SB),7,$0 // set up m, g get_tls(CX) + MOVQ 8(CX), AX MOVQ R13, m(CX) MOVQ m_g0(R13), DI MOVQ DI, g(CX) CALL runtime·stackcheck(SB) - CALL runtime·mstart(SB) + + // newosproc left the function we should call in mp->tls[2] for us. + get_tls(CX) + MOVQ 16(CX), AX + CALL AX + MOVQ 0, AX // crash (not reached)
MOVQ 8(CX), AX
とMOVQ 16(CX), AX
の違いは、TLSのオフセットがアーキテクチャやTLSのレイアウトによって異なるためです。
-
src/pkg/runtime/sys_freebsd_arm.s
:runtime·thr_start
関数内で、TLSから開始関数を取得して呼び出すように変更されました。--- a/src/pkg/runtime/sys_freebsd_arm.s +++ b/src/pkg/runtime/sys_freebsd_arm.s @@ -33,7 +33,11 @@ TEXT runtime·thr_start(SB),7,$0 // set up g MOVW m_g0(R9), R10 BL runtime·emptyfunc(SB) // fault if stack check is wrong - BL runtime·mstart(SB) + + // newosproc left the function we should call in mp->tls[2] for us. + MOVW (m_tls+8)(m), R0 + BL (R0) + MOVW $2, R9 // crash (not reached) MOVW R9, (R9)\n \tRET
-
src/pkg/runtime/sys_windows_386.s
:runtime·tstart
関数内で、TLSから開始関数を取得して呼び出すように変更されました。--- a/src/pkg/runtime/sys_windows_386.s +++ b/src/pkg/runtime/sys_windows_386.s @@ -260,7 +260,10 @@ TEXT runtime·tstart(SB),7,$0 CALL runtime·stackcheck(SB) // clobbers AX,CX - CALL runtime·mstart(SB) + // start function is in tls[2] + get_tls(CX) + MOVL 8(CX), AX + CALL AX RET
-
src/pkg/runtime/sys_windows_amd64.s
:runtime·tstart_stdcall
関数内で、TLSから開始関数を取得して呼び出すように変更されました。--- a/src/pkg/runtime/sys_windows_amd64.s +++ b/src/pkg/runtime/sys_windows_amd64.s @@ -329,7 +329,11 @@ TEXT runtime·tstart_stdcall(SB),7,$0 CLD CALL runtime·stackcheck(SB) // clobbers AX,CX - CALL runtime·mstart(SB) + + // start function is in tls[2] + get_tls(CX) + MOVQ 16(CX), AX + CALL AX XORL AX, AX // return 0 == success RET
-
src/pkg/runtime/thread_freebsd.c
:runtime·newosproc
関数内で、開始関数fn
をmp->tls[2]
に格納する行が追加されました。また、USED(fn)
とUSED(gp)
が削除され、gp
の検証が追加されました。--- a/src/pkg/runtime/thread_freebsd.c +++ b/src/pkg/runtime/thread_freebsd.c @@ -82,12 +82,13 @@ runtime·newosproc(M *mp, G *gp, void *stk, void (*fn)(void))\n ThrParam param;\n Sigset oset;\n \n - USED(fn); // thr_start assumes fn == mstart\n - USED(gp); // thr_start assumes gp == mp->g0\n + // thr_start assumes gp == mp->g0\n + if(gp != mp->g0)\n + runtime·throw("invalid newosproc gp");\n \n if(0){\n runtime·printf("newosproc stk=%p m=%p g=%p fn=%p id=%d/%d ostk=%p\\n",\n - stk, mp, gp, fn, mp->id, mp->tls[0], &mp);\n + stk, mp, gp, fn, mp->id, (int32)mp->tls[0], &mp);\n }\n \n runtime·sigprocmask(&sigset_all, &oset);\n @@ -103,6 +104,7 @@ runtime·newosproc(M *mp, G *gp, void *stk, void (*fn)(void))\n param.tls_size = sizeof mp->tls;\n \n mp->tls[0] = mp->id; // so 386 asm can find it\n + mp->tls[2] = (uintptr)fn;\n \n runtime·thr_new(¶m, sizeof param);\n runtime·sigprocmask(&oset, nil);\n ```
-
src/pkg/runtime/thread_windows.c
:runtime·newosproc
関数内で、開始関数fn
をmp->tls[2]
に格納する行が追加されました。また、USED(fn)
とUSED(gp)
が削除され、gp
の検証が追加されました。--- a/src/pkg/runtime/thread_windows.c +++ b/src/pkg/runtime/thread_windows.c @@ -192,8 +192,12 @@ runtime·newosproc(M *mp, G *gp, void *stk, void (*fn)(void))\n void *thandle;\n \n USED(stk);\n - USED(gp); // assuming gp = mp->g0\n - USED(fn); // assuming fn = mstart\n +\n + // assume gp == mp->g0\n + if(gp != mp->g0)\n + runtime·throw("invalid newosproc gp");\n +\n + mp->tls[2] = (uintptr)fn;\n \n thandle = runtime·stdcall(runtime·CreateThread, 6,\n \tnil, (uintptr)0x20000, runtime·tstart_stdcall, mp,\n ```
その他のthread_*.c
ファイル(darwin, linux, netbsd, openbsd, plan9)では、printf
のフォーマット文字列がmp->tls[0]
をint32
としてキャストするように変更されていますが、これはデバッグ出力の修正であり、本質的な機能変更ではありません。
コアとなるコードの解説
このコミットの核となる変更は、Goランタイムが新しいOSスレッドを起動する際に、そのスレッドが実行を開始すべき関数へのポインタを、スレッドローカルストレージ(TLS)を介して安全かつ確実に渡すメカニズムを確立した点にあります。
-
runtime.h
の変更:M
構造体のtls
フィールドがuint32 tls[8]
からuint64 tls[4]
に変更されたのは、主に64ビットアーキテクチャでポインタ(アドレス)を格納するためにuint64
を使用するためです。これにより、TLSに格納できるデータの種類とサイズがより柔軟になります。tls[2]
というインデックスが使用されていることから、TLSの特定のオフセットが開始関数ポインタの格納場所として予約されたことがわかります。
-
thread_freebsd.c
およびthread_windows.c
の変更:runtime·newosproc
関数は、GoランタイムがOSに対して新しいスレッドの作成を要求する際に呼び出されます。この関数内で、mp->tls[2] = (uintptr)fn;
という行が追加されました。- ここで、
fn
は新しく作成されるOSスレッドが起動後に実行すべき関数(通常はruntime.mstart
)へのポインタです。このポインタが、新しく作成されるM(OSスレッド)のTLS領域の特定のオフセット(インデックス2)に格納されます。 - これにより、OSスレッドが起動した際に、そのスレッド自身のTLS領域からこの関数ポインタを読み出すことが可能になります。
- また、
USED(fn)
やUSED(gp)
といったマクロが削除され、if(gp != mp->g0) runtime·throw("invalid newosproc gp");
のような明示的な検証が追加されたのは、TLSを介して関数ポインタを渡すことで、newosproc
が常に特定の関数(mstart
)とゴルーチン(mp->g0
)で呼び出されるという暗黙の前提が不要になったためです。これにより、コードの意図がより明確になり、将来的な変更にも対応しやすくなります。
-
sys_freebsd_*.s
およびsys_windows_*.s
のアセンブリコードの変更:- これらのファイルには、各OSおよびアーキテクチャにおける新しいOSスレッドの実際のエントリポイント(
runtime·thr_start
やruntime·tstart
)が記述されています。 - 以前は、これらのエントリポイントから直接
CALL runtime·mstart(SB)
のようにruntime.mstart
関数を呼び出していました。しかし、これはOSスレッドの起動メカニズムによっては、runtime.mstart
が期待通りに実行されない原因となっていました。 - 修正後、アセンブリコードは以下の手順を踏みます。
get_tls(CX)
: 現在のスレッドのTLSベースアドレスをCX
レジスタにロードします。MOVQ 8(CX), AX
(またはMOVQ 16(CX), AX
,MOVW (m_tls+8)(m), R0
):CX
レジスタが指すTLSベースアドレスから、特定のオフセット(mp->tls[2]
に相当する位置)にある64ビット値(関数ポインタ)をAX
レジスタ(またはR0
レジスタ)にロードします。このオフセットはアーキテクチャによって異なります。CALL AX
(またはBL (R0)
):AX
レジスタ(またはR0
レジスタ)に格納されたアドレス(つまり、runtime·newosproc
でTLSに格納された開始関数へのポインタ)を間接的に呼び出します。
- これらのファイルには、各OSおよびアーキテクチャにおける新しいOSスレッドの実際のエントリポイント(
この一連の変更により、Goランタイムは、新しいOSスレッドが起動する際に、そのスレッドが実行すべき初期関数をTLSというスレッド固有の安全な場所に格納し、起動後のアセンブリコードでその情報を正確に取得して実行できるようになりました。これにより、FreeBSDとWindows環境におけるGoランタイムの新しいスケジューラの安定性と信頼性が大幅に向上しました。
関連リンク
- Go CL 7443046: https://golang.org/cl/7443046
参考にした情報源リンク
- Go's Concurrency Model: Goroutines and the Go Scheduler: https://go.dev/doc/effective_go#concurrency
- Go Scheduler: M-P-G Model Explained: https://medium.com/a-journey-with-go/go-scheduler-m-p-g-model-explained-71fdd24a1c2f
- Thread-Local Storage (TLS) in C/C++: https://en.wikipedia.org/wiki/Thread-local_storage
- x86 Assembly Language Reference: https://www.felixcloutier.com/x86/
- ARM Assembly Language Reference: (General ARM assembly resources, e.g., ARM Architecture Reference Manual)
- Go runtime source code (relevant files):
src/runtime/runtime.h
(nowsrc/runtime/runtime2.go
or similar in newer Go versions)src/runtime/sys_freebsd_386.s
(and othersys_*.s
files)src/runtime/thread_freebsd.c
(and otherthread_*.c
files)