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

[インデックス 15531] ファイルの概要

このコミットは、GoランタイムにおけるOSスレッド(M)の起動関数(mstartfn)の管理方法を改善するものです。具体的には、これまで汎用的なスレッドローカルストレージ(TLS)配列の一部として扱われていたmstartfnを、M構造体内の専用フィールドとして独立させる変更を行っています。これにより、コードの明確性、型安全性、および保守性が向上し、より堅牢なランタイムの構築に貢献しています。

コミット

commit d0d7416d3f9065141e1abe85528803afa9217371
Author: Russ Cox <rsc@golang.org>
Date:   Fri Mar 1 09:24:17 2013 -0500

    runtime: more build fixing
    
    Move the mstartfn into its own field.
    Simpler, more likely to be correct.
    
    R=golang-dev, devon.odell
    CC=golang-dev
    https://golang.org/cl/7414046

GitHub上でのコミットページへのリンク

https://github.com/golang/go/commit/d0d7416d3f9065141e1abe85528803afa9217371

元コミット内容

このコミットは、Goランタイムのビルドに関するさらなる修正を目的としています。主な変更点は、OSスレッドの開始関数であるmstartfnを、M構造体内の独立したフィールドとして移動させたことです。これにより、コードがよりシンプルになり、正確性が向上するとされています。

変更の背景

Go言語のランタイムは、ゴルーチン(Goroutine)のスケジューリングと実行を管理するために、OSスレッドを効率的に利用します。このシステムの中核には、M(Machine)、P(Processor)、G(Goroutine)という3つの主要な抽象概念があります。MはOSスレッドを表し、Goコードを実行するための基盤となります。

新しいOSスレッドが作成される際、そのスレッドが最初に実行すべき関数(エントリポイント)を指定する必要があります。これまでの実装では、この開始関数がM構造体内のtls(Thread-Local Storage)配列の特定のインデックス(tls[2])にuintptr型として格納されていました。

このアプローチにはいくつかの課題がありました。

  1. 不明瞭さ: tls[2]というインデックスは、その値がOSスレッドの開始関数であることを直接的に示していませんでした。コードを読む際に、その意味を理解するためには、ランタイムの内部実装に関する深い知識が必要でした。
  2. 型安全性: uintptrは汎用的なポインタ型であり、任意のメモリアドレスを保持できます。これにより、誤った型の値を格納してしまう可能性があり、コンパイル時の型チェックの恩恵を受けられませんでした。
  3. 保守性: tls配列のレイアウトが将来変更された場合、tls[2]に依存するコードは容易に壊れる可能性がありました。

これらの課題を解決し、ランタイムのコードベースをより堅牢で理解しやすいものにするために、mstartfnを専用のフィールドとして独立させる変更が提案されました。これにより、その目的が明確になり、型安全性が確保され、将来の変更に対する耐性が向上します。

前提知識の解説

このコミットを理解するためには、以下の概念について基本的な知識が必要です。

1. GoランタイムのM-P-Gモデル

  • G (Goroutine): Go言語における軽量な実行単位です。OSスレッドよりもはるかに軽量で、数百万個作成することも可能です。
  • P (Processor): 論理的なプロセッサを表します。Goスケジューラは、GをPにディスパッチし、PはMにアタッチされてGを実行します。Pは、Goコードを実行するために必要なリソース(例えば、実行キュー)を保持します。
  • M (Machine): OSスレッドを表します。Goコードを実行する実際のOSスレッドです。MはPにアタッチされ、PがディスパッチしたGを実行します。Mは、OSスレッドのスタックやレジスタなどのOSレベルのリソースを管理します。

2. スレッドローカルストレージ (TLS)

TLSは、各スレッドが独自のデータコピーを持つことを可能にするメカニズムです。これにより、グローバル変数を使用せずにスレッド固有のデータを安全に管理できます。Goランタイムでは、M(OSスレッド)に関連する特定の情報をTLSに格納することがあります。

3. アセンブリ言語 (x86/x64)

Goランタイムの低レベルな部分は、パフォーマンス最適化やOSとの直接的なインタラクションのためにアセンブリ言語で記述されています。このコミットでは、特にOSスレッドの起動シーケンスに関連するアセンブリコードが変更されています。

  • MOVQ / MOVL: レジスタ間またはレジスタとメモリ間でデータを移動する命令です。MOVQは64ビット、MOVLは32ビットのデータを扱います。
  • CALL: 関数を呼び出す命令です。
  • レジスタ: CPU内部の高速な記憶領域です。AXCXR13などは汎用レジスタの一部です。
  • メモリ参照: m(CX)のような表記は、CXレジスタが指すアドレスからm構造体のオフセットにあるメモリ位置を参照することを示します。

4. newosproc関数

newosprocはGoランタイムの内部関数で、新しいOSスレッドを作成する役割を担います。この関数は、作成されたOSスレッドが実行を開始すべき関数へのポインタを受け取ります。

技術的詳細

このコミットの技術的な変更は、主に以下の3つの側面から構成されています。

1. M構造体の変更 (src/pkg/runtime/runtime.h)

M構造体は、GoランタイムにおけるOSスレッドの表現です。この構造体に、新しいフィールドmstartfnが追加されました。

--- a/src/pkg/runtime/runtime.h
+++ b/src/pkg/runtime/runtime.h
@@ -265,7 +265,8 @@ struct	M
 	uintptr	cret;		// return value from C
 	uint64	procid;		// for debuggers, but offset not hard-coded
 	G*	gsignal;	// signal-handling G
-	uint64	tls[4];		// thread-local storage (for x86 extern register)
+	uintptr	tls[4];		// thread-local storage (for x86 extern register)
+	void	(*mstartfn)(void);
 	G*	curg;		// current running goroutine
 	P*	p;		// attached P for executing Go code (nil if not executing Go code)
 	P*	nextp;
  • uint64 tls[4];uintptr tls[4]; に変更されています。これは、tls配列の要素の型を、ポインタサイズに合わせたuintptrに変更したものです。これはこのコミットの主要な変更ではありませんが、関連する型の一貫性を保つための変更と考えられます。
  • void (*mstartfn)(void); が追加されました。これは、引数を取らず、何も返さない関数へのポインタを格納するための専用フィールドです。このフィールドが、新しく作成されたOSスレッドが実行を開始するエントリポイントを明示的に保持する役割を担います。

2. OSスレッド作成ロジックの変更 (src/pkg/runtime/thread_freebsd.c, src/pkg/runtime/thread_windows.c)

newosproc関数は、新しいOSスレッドを起動する際に、そのスレッドが実行すべき関数へのポインタを受け取ります。このコミットでは、そのポインタをM構造体のmstartfnフィールドに直接格納するように変更されました。

--- a/src/pkg/runtime/thread_freebsd.c
+++ b/src/pkg/runtime/thread_freebsd.c
@@ -104,7 +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+	mp->mstartfn = fn;\
 \n 	runtime·thr_new(&param, sizeof param);\
 	runtime·sigprocmask(&oset, nil);

同様の変更がsrc/pkg/runtime/thread_windows.cにも適用されています。これにより、newosprocがOSスレッドの開始関数をtls[2]ではなく、mstartfnに設定することが明確になります。

3. アセンブリコードの変更 (各OS/アーキテクチャ固有のファイル)

新しく作成されたOSスレッドが実際に起動する際、その開始関数をM構造体から読み出す必要があります。このコミットでは、tls[2]から読み出す代わりに、mstartfnフィールドから読み出すようにアセンブリコードが修正されました。

例えば、src/pkg/runtime/sys_freebsd_386.sでは、以下の変更が行われています。

--- a/src/pkg/runtime/sys_freebsd_386.s
+++ b/src/pkg/runtime/sys_freebsd_386.s
@@ -39,9 +39,10 @@ TEXT runtime·thr_start(SB),7,$0
 	MOVL	AX, m(CX)
 	CALL	runtime·stackcheck(SB)		// smashes AX
 
-	// newosproc left the function we should call in mp->tls[2] for us.\
+	// newosproc left the function we should call in mp->mstartfn.\
 	get_tls(CX)
-	MOVQ	8(CX), AX
+	MOVL	m(CX), AX
+	MOVL	m_mstartfn(AX), AX
 	CALL	AX
 
 	MOVL	0, AX			// crash (not reached)
  • コメントが「newosproc left the function we should call in mp->tls[2] for us.」から「newosproc left the function we should call in mp->mstartfn.」に変更され、意図が明確になっています。
  • MOVQ 8(CX), AX の代わりに、MOVL m(CX), AXMOVL m_mstartfn(AX), AX が使用されています。
    • get_tls(CX): スレッドローカルストレージから現在のM(OSスレッド)へのポインタを取得し、CXレジスタに格納します。
    • MOVL m(CX), AX: CXが指すTLS領域から、現在のM構造体へのポインタをAXレジスタにロードします。
    • MOVL m_mstartfn(AX), AX: AXが指すM構造体から、mstartfnフィールドの値をAXレジスタにロードします。m_mstartfnは、M構造体内のmstartfnフィールドへのオフセットを示すシンボルです。
  • 最終的に、CALL AXによって、AXレジスタにロードされた開始関数が呼び出されます。

同様の変更が、sys_freebsd_amd64.s, sys_windows_386.s, sys_windows_amd64.sといった他のOSおよびアーキテクチャ固有のアセンブリファイルにも適用されています。

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

このコミットにおけるコアとなるコードの変更箇所は以下の通りです。

  1. src/pkg/runtime/runtime.h:

    • struct Mvoid (*mstartfn)(void);フィールドを追加。
    • uint64 tls[4];uintptr tls[4];に変更。
  2. src/pkg/runtime/thread_freebsd.c および src/pkg/runtime/thread_windows.c:

    • newosproc関数内で、mp->tls[2] = (uintptr)fn;mp->mstartfn = fn; に変更。
  3. src/pkg/runtime/sys_freebsd_386.s, src/pkg/runtime/sys_freebsd_amd64.s, src/pkg/runtime/sys_windows_386.s, src/pkg/runtime/sys_windows_amd64.s:

    • OSスレッドの開始関数を読み出すアセンブリコードを、mp->tls[2]からmp->mstartfnを指すように変更。具体的には、メモリオフセットの計算とレジスタへのロード方法が変更されています。

コアとなるコードの解説

このコミットの核心は、Goランタイムが新しいOSスレッドを起動する際のエントリポイントの管理方法を、より明示的で安全なものにすることです。

以前は、OSスレッドの開始関数はM構造体のtls配列の3番目の要素(インデックス2)にuintptrとして格納されていました。これは、汎用的なスレッドローカルストレージを流用したものであり、その目的がコードから直接読み取れませんでした。また、uintptrは型情報を持たないため、誤った関数ポインタが格納されてもコンパイル時に検出されにくいという問題がありました。

今回の変更では、M構造体にmstartfnという専用のvoid (*)(void)型の関数ポインタフィールドが追加されました。

  • void (*mstartfn)(void);: この宣言は、mstartfnが引数を取らず、何も返さない関数へのポインタであることを明確に示しています。これにより、コンパイラが型チェックを行うことができ、誤った関数が割り当てられるのを防ぎます。
  • newosprocの変更: newosproc関数は、新しいOSスレッドを作成する際に、そのスレッドが実行を開始すべき関数をfn引数として受け取ります。このfnが直接mp->mstartfnに代入されるようになりました。これにより、開始関数の設定がより直感的になります。
  • アセンブリコードの変更: OSスレッドが実際に起動する際、アセンブリコードはM構造体から開始関数を読み出してCALL命令で実行します。以前はtls配列のオフセットを計算してアクセスしていましたが、変更後はmstartfnフィールドのオフセットを直接利用してアクセスします。これは、m_mstartfnのようなシンボルによって、コンパイラ/リンカがM構造体内のmstartfnフィールドの正確なメモリオフセットを解決するため、より直接的でエラーの少ないアクセス方法となります。

この一連の変更により、OSスレッドの開始関数がM構造体内で明確に定義された専用の場所を持つことになり、コードの可読性、保守性、および堅牢性が大幅に向上しました。これは「Simpler, more likely to be correct.(よりシンプルで、より正確である可能性が高い)」というコミットメッセージの意図を正確に反映しています。

関連リンク

  • Go言語のM-P-Gモデルに関する公式ドキュメントや解説記事
  • Goランタイムのソースコード(特にsrc/runtimeディレクトリ)
  • GoのIssueトラッカーやCL (Change List) システム(https://golang.org/cl/7414046

参考にした情報源リンク