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

[インデックス 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モデルの基本的な流れは以下の通りです。

  1. Goランタイムは、Pの数だけM(OSスレッド)を作成します。
  2. 各MはPにアタッチされ、Pからゴルーチンを取得して実行します。
  3. ゴルーチンがシステムコールなどでブロックされると、MはPからデタッチされ、別のMがそのPにアタッチされて他のゴルーチンを実行できます。
  4. 新しいゴルーチンが作成されると、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環境では、スレッドの起動メカニズムが異なり、この直接的な方法では開始関数が正しく認識されない、または実行されない問題がありました。

この修正では、以下の技術的なアプローチが取られています。

  1. TLS (Thread Local Storage) の活用:

    • runtime.hM構造体において、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からこの関数ポインタを取得できるようになります。
  2. アセンブリコードの修正:

    • 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でその関数を間接的に呼び出すようになっています。
  3. USEDマクロの削除と検証の追加:

    • thread_freebsd.cthread_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環境における新しいスケジューラの安定性が向上しました。

コアとなるコードの変更箇所

このコミットにおける主要なコード変更は、以下のファイルに集中しています。

  1. 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の利用方法の変更を反映しています。
  2. 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)
      
  3. 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), AXMOVQ 16(CX), AXの違いは、TLSのオフセットがアーキテクチャやTLSのレイアウトによって異なるためです。
  4. 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
      
  5. 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
      
  6. 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
      
  7. src/pkg/runtime/thread_freebsd.c:

    • runtime·newosproc関数内で、開始関数fnmp->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(&param, sizeof param);\n         	runtime·sigprocmask(&oset, nil);\n        ```
      
      
  8. src/pkg/runtime/thread_windows.c:

    • runtime·newosproc関数内で、開始関数fnmp->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)を介して安全かつ確実に渡すメカニズムを確立した点にあります。

  1. runtime.hの変更:

    • M構造体のtlsフィールドがuint32 tls[8]からuint64 tls[4]に変更されたのは、主に64ビットアーキテクチャでポインタ(アドレス)を格納するためにuint64を使用するためです。これにより、TLSに格納できるデータの種類とサイズがより柔軟になります。tls[2]というインデックスが使用されていることから、TLSの特定のオフセットが開始関数ポインタの格納場所として予約されたことがわかります。
  2. 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)で呼び出されるという暗黙の前提が不要になったためです。これにより、コードの意図がより明確になり、将来的な変更にも対応しやすくなります。
  3. sys_freebsd_*.sおよびsys_windows_*.sのアセンブリコードの変更:

    • これらのファイルには、各OSおよびアーキテクチャにおける新しいOSスレッドの実際のエントリポイント(runtime·thr_startruntime·tstart)が記述されています。
    • 以前は、これらのエントリポイントから直接CALL runtime·mstart(SB)のようにruntime.mstart関数を呼び出していました。しかし、これはOSスレッドの起動メカニズムによっては、runtime.mstartが期待通りに実行されない原因となっていました。
    • 修正後、アセンブリコードは以下の手順を踏みます。
      1. get_tls(CX): 現在のスレッドのTLSベースアドレスをCXレジスタにロードします。
      2. MOVQ 8(CX), AX (またはMOVQ 16(CX), AX, MOVW (m_tls+8)(m), R0): CXレジスタが指すTLSベースアドレスから、特定のオフセット(mp->tls[2]に相当する位置)にある64ビット値(関数ポインタ)をAXレジスタ(またはR0レジスタ)にロードします。このオフセットはアーキテクチャによって異なります。
      3. CALL AX (またはBL (R0)): AXレジスタ(またはR0レジスタ)に格納されたアドレス(つまり、runtime·newosprocでTLSに格納された開始関数へのポインタ)を間接的に呼び出します。

この一連の変更により、Goランタイムは、新しいOSスレッドが起動する際に、そのスレッドが実行すべき初期関数をTLSというスレッド固有の安全な場所に格納し、起動後のアセンブリコードでその情報を正確に取得して実行できるようになりました。これにより、FreeBSDとWindows環境におけるGoランタイムの新しいスケジューラの安定性と信頼性が大幅に向上しました。

関連リンク

参考にした情報源リンク

[インデックス 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モデルの基本的な流れは以下の通りです。

  1. Goランタイムは、Pの数だけM(OSスレッド)を作成します。
  2. 各MはPにアタッチされ、Pからゴルーチンを取得して実行します。
  3. ゴルーチンがシステムコールなどでブロックされると、MはPからデタッチされ、別のMがそのPにアタッチされて他のゴルーチンを実行できます。
  4. 新しいゴルーチンが作成されると、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環境では、スレッドの起動メカニズムが異なり、この直接的な方法では開始関数が正しく認識されない、または実行されない問題がありました。

この修正では、以下の技術的なアプローチが取られています。

  1. TLS (Thread Local Storage) の活用:

    • runtime.hM構造体において、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からこの関数ポインタを取得できるようになります。
  2. アセンブリコードの修正:

    • 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でその関数を間接的に呼び出すようになっています。
  3. USEDマクロの削除と検証の追加:

    • thread_freebsd.cthread_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環境における新しいスケジューラの安定性が向上しました。

コアとなるコードの変更箇所

このコミットにおける主要なコード変更は、以下のファイルに集中しています。

  1. 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の利用方法の変更を反映しています。
  2. 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)
      
  3. 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), AXMOVQ 16(CX), AXの違いは、TLSのオフセットがアーキテクチャやTLSのレイアウトによって異なるためです。
  4. 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
      
  5. 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
      
  6. 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
      
  7. src/pkg/runtime/thread_freebsd.c:

    • runtime·newosproc関数内で、開始関数fnmp->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(&param, sizeof param);\n         	runtime·sigprocmask(&oset, nil);\n        ```
      
      
  8. src/pkg/runtime/thread_windows.c:

    • runtime·newosproc関数内で、開始関数fnmp->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)を介して安全かつ確実に渡すメカニズムを確立した点にあります。

  1. runtime.hの変更:

    • M構造体のtlsフィールドがuint32 tls[8]からuint64 tls[4]に変更されたのは、主に64ビットアーキテクチャでポインタ(アドレス)を格納するためにuint64を使用するためです。これにより、TLSに格納できるデータの種類とサイズがより柔軟になります。tls[2]というインデックスが使用されていることから、TLSの特定のオフセットが開始関数ポインタの格納場所として予約されたことがわかります。
  2. 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)で呼び出されるという暗黙の前提が不要になったためです。これにより、コードの意図がより明確になり、将来的な変更にも対応しやすくなります。
  3. sys_freebsd_*.sおよびsys_windows_*.sのアセンブリコードの変更:

    • これらのファイルには、各OSおよびアーキテクチャにおける新しいOSスレッドの実際のエントリポイント(runtime·thr_startruntime·tstart)が記述されています。
    • 以前は、これらのエントリポイントから直接CALL runtime·mstart(SB)のようにruntime.mstart関数を呼び出していました。しかし、これはOSスレッドの起動メカニズムによっては、runtime.mstartが期待通りに実行されない原因となっていました。
    • 修正後、アセンブリコードは以下の手順を踏みます。
      1. get_tls(CX): 現在のスレッドのTLSベースアドレスをCXレジスタにロードします。
      2. MOVQ 8(CX), AX (またはMOVQ 16(CX), AX, MOVW (m_tls+8)(m), R0): CXレジスタが指すTLSベースアドレスから、特定のオフセット(mp->tls[2]に相当する位置)にある64ビット値(関数ポインタ)をAXレジスタ(またはR0レジスタ)にロードします。このオフセットはアーキテクチャによって異なります。
      3. CALL AX (またはBL (R0)): AXレジスタ(またはR0レジスタ)に格納されたアドレス(つまり、runtime·newosprocでTLSに格納された開始関数へのポインタ)を間接的に呼び出します。

この一連の変更により、Goランタイムは、新しいOSスレッドが起動する際に、そのスレッドが実行すべき初期関数をTLSというスレッド固有の安全な場所に格納し、起動後のアセンブリコードでその情報を正確に取得して実行できるようになりました。これにより、FreeBSDとWindows環境におけるGoランタイムの新しいスケジューラの安定性と信頼性が大幅に向上しました。

関連リンク

参考にした情報源リンク