[インデックス 16885] ファイルの概要
このコミットは、Goランタイムにおける特定のバグ修正に関するものです。具体的には、runtime.Goexit
が初期化フェーズ中に呼び出された際に発生する問題を解決します。この問題は、メインのgoroutineがOSスレッドにロックされている状態でruntime.Goexit
が実行されると、OSスレッドのロックが適切に解除されずに内部エラーを引き起こす可能性がありました。
コミット
commit 14062efb16e3c69adaf655d0b545189036929368
Author: Russ Cox <rsc@golang.org>
Date: Fri Jul 26 13:54:44 2013 -0400
runtime: handle runtime.Goexit during init
Fixes #5963.
R=golang-dev, dsymonds, dvyukov
CC=golang-dev
https://golang.org/cl/11879045
GitHub上でのコミットページへのリンク
https://github.com/golang/go/commit/14062efb16e3c69adaf655d0b545189036929368
元コミット内容
--- a/src/pkg/runtime/proc.c
+++ b/src/pkg/runtime/proc.c
@@ -160,10 +160,14 @@ extern void main·main(void);
static FuncVal scavenger = {runtime·MHeap_Scavenger};
+static FuncVal initDone = { runtime·unlockOSThread };
+
// The main goroutine.
void
runtime·main(void)
{
+\tDefer d;
+\
newm(sysmon, nil);\
// Lock the main goroutine onto this, the main OS thread,\
@@ -173,10 +177,24 @@ runtime·main(void)\
// by calling runtime.LockOSThread during initialization
// to preserve the lock.\
runtime·lockOSThread();
+\t\
+\t// Defer unlock so that runtime.Goexit during init does the unlock too.\
+\td.fn = &initDone;\
+\td.siz = 0;\
+\td.link = g->defer;\
+\td.argp = (void*)-1;\
+\td.special = true;\
+\td.free = false;\
+\tg->defer = &d;\
+\
\tif(m != &runtime·m0)\
\t\truntime·throw(\"runtime·main not on m0\");\
\truntime·newproc1(&scavenger, nil, 0, 0, runtime·main);\
\tmain·init();
+\
+\tif(g->defer != &d || d.fn != &initDone)\
+\t\truntime·throw(\"runtime: bad defer entry after init\");
+\tg->defer = d.link;\
\truntime·unlockOSThread();
\tmain·main();
diff --git a/test/fixedbugs/issue5963.go b/test/fixedbugs/issue5963.go
new file mode 100644
index 0000000000..190e8f4564
--- /dev/null
+++ b/test/fixedbugs/issue5963.go
@@ -0,0 +1,50 @@
+// run
+
+// Copyright 2013 The Go Authors. All rights reserved.\n// Use of this source code is governed by a BSD-style\n// license that can be found in the LICENSE file.\n+\n+// Used to die in runtime due to init goroutine exiting while\n+// locked to main thread.\n+\n+package main\n+\n+import (\n+\t\"os\"\n+\t\"runtime\"\n+)\n+\n+func init() {\n+\tc := make(chan int, 1)\n+\tdefer func() {\n+\t\tc <- 0\n+\t}()\n+\tgo func() {\n+\t\tos.Exit(<-c)\n+\t}()\n+\truntime.Goexit()\n+}\n+\n+func main() {\n+}\n+\n+/* Before fix:\n+\n+invalid m->locked = 2\n+fatal error: internal lockOSThread error\n+\n+goroutine 2 [runnable]:\n+runtime.MHeap_Scavenger()\n+\t/Users/rsc/g/go/src/pkg/runtime/mheap.c:438\n+runtime.goexit()\n+\t/Users/rsc/g/go/src/pkg/runtime/proc.c:1313\n+created by runtime.main\n+\t/Users/g/go/src/pkg/runtime/proc.c:165\n+\n+goroutine 3 [runnable]:\n+main.func·002()\n+\t/Users/rsc/g/go/test/fixedbugs/issue5963.go:22\n+created by main.init·1\n+\t/Users/rsc/g/go/test/fixedbugs/issue5963.go:24 +0xb9\n+exit status 2\n+*/
変更の背景
このコミットは、Goプログラムの初期化処理中にruntime.Goexit()
が呼び出された際に発生する、ランタイムのクラッシュを修正するために導入されました。具体的には、Issue 5963で報告された問題に対応しています。
Goプログラムの起動時、メインのgoroutineは特定のOSスレッドにロックされます(runtime.LockOSThread()
)。これは、一部のOS固有の操作やCgo呼び出しが、特定のOSスレッドからしか実行できない場合に必要となるためです。通常、プログラムの実行が終了する際には、このロックはruntime.unlockOSThread()
によって解除されます。
しかし、init()
関数(パッケージの初期化時に実行される関数)内でruntime.Goexit()
が呼び出された場合、メインのgoroutineは終了しますが、OSスレッドのロックが適切に解除されないという問題がありました。これにより、ランタイムが「invalid m->locked = 2
」や「fatal error: internal lockOSThread error
」といったエラーを出力して異常終了していました。
この問題は、runtime.Goexit()
がgoroutineを終了させるものの、そのgoroutineがOSスレッドにロックされている場合のクリーンアップ処理が不十分であったことに起因します。特に、初期化フェーズはプログラムのライフサイクルにおいて非常に重要な部分であり、ここでの予期せぬ終了はシステム全体の不安定性につながるため、修正が急務でした。
前提知識の解説
このコミットを理解するためには、以下のGoランタイムの概念を理解しておく必要があります。
- Goroutine: Go言語における軽量な実行単位です。OSスレッドよりもはるかに軽量で、数百万個のgoroutineを同時に実行できます。GoランタイムのスケジューラがgoroutineをOSスレッドにマッピングして実行します。
- OSスレッド (M): オペレーティングシステムが管理するスレッドです。Goランタイムは、goroutineをOSスレッド上で実行します。
- P (Processor): Goランタイムのスケジューラがgoroutineを実行するために使用する論理プロセッサです。PはOSスレッドと関連付けられ、goroutineの実行コンテキストを提供します。
runtime.LockOSThread()
: 現在のgoroutineを、それを実行しているOSスレッドに「ロック」する関数です。これにより、そのgoroutineは他のOSスレッドに移動することなく、常に同じOSスレッド上で実行されることが保証されます。これは、特定のOSスレッドに依存するCgo呼び出しや、OSのGUIライブラリなどを使用する場合に必要となることがあります。ロックされたOSスレッドは、runtime.unlockOSThread()
が呼び出されるまで、他のgoroutineを実行するために使用されません。runtime.unlockOSThread()
:runtime.LockOSThread()
によって設定されたロックを解除する関数です。runtime.Goexit()
: 現在のgoroutineを終了させる関数です。この関数が呼び出されると、現在のgoroutineは直ちに終了し、そのgoroutineに紐付けられたdefer
関数が実行されます。ただし、プログラム全体が終了するわけではありません。他のgoroutineが実行中であれば、それらは引き続き実行されます。init()
関数: Goプログラムにおいて、各パッケージはinit()
関数を持つことができます。init()
関数は、そのパッケージがインポートされた際に、main()
関数が実行されるよりも前に自動的に実行されます。複数のinit()
関数がある場合、それらは定義された順序で実行されます。Defer
: Go言語のdefer
ステートメントは、関数がリターンする直前(パニックが発生した場合も含む)に実行される関数をスケジュールします。ランタイム内部では、Defer
構造体がこの遅延実行される関数を管理しています。
この問題は、runtime.main
関数が起動時にruntime.lockOSThread()
を呼び出し、その後main.init()
を実行する際に発生しました。main.init()
内でruntime.Goexit()
が呼び出されると、メインのgoroutineは終了しますが、runtime.unlockOSThread()
が実行される機会が失われ、OSスレッドがロックされたままになっていました。
技術的詳細
このコミットの技術的な核心は、runtime.main
関数内でruntime.unlockOSThread
をdefer
のように扱うことで、main.init()
の実行中にruntime.Goexit()
が呼び出された場合でも、OSスレッドのロックが確実に解除されるようにした点です。
Goランタイムのproc.c
ファイルには、Goプログラムのメインエントリポイントであるruntime·main
関数が定義されています。この関数は、プログラムの初期化、スケジューラの起動、そしてmain.main()
関数の呼び出しを担当します。
修正前は、runtime·main
内でruntime·lockOSThread()
が呼び出された後、main·init()
が実行され、その後にruntime·unlockOSThread()
が呼び出されていました。問題は、main·init()
の実行中にruntime·Goexit()
が呼び出されると、main·init()
から戻ることなくgoroutineが終了するため、runtime·unlockOSThread()
が実行されないことでした。
このコミットでは、以下の変更が加えられました。
Defer
構造体の導入:runtime·main
関数内にDefer d;
という行が追加されました。これは、Goのdefer
ステートメントが内部的に使用するDefer
構造体と同じものです。initDone
FuncVal
の定義:static FuncVal initDone = { runtime·unlockOSThread };
という行が追加されました。これは、runtime·unlockOSThread
関数へのポインタを持つFuncVal
(関数値)を定義しています。- 手動での
defer
登録:d.fn = &initDone;
:Defer
構造体d
の関数ポインタをruntime·unlockOSThread
に設定します。d.siz = 0;
: 引数のサイズを設定します(この場合は引数がないため0)。d.link = g->defer;
: 現在のgoroutine (g
) の既存のdefer
リストの先頭に、この新しいdefer
エントリをリンクします。d.argp = (void*)-1;
: 引数ポインタを設定します。-1
は特別な値で、このdefer
が通常のGoコードではなくランタイムによって設定されたものであることを示唆します。d.special = true;
: このdefer
が特別なランタイムのdefer
であることを示します。d.free = false;
: このdefer
が解放されるべきではないことを示します。g->defer = &d;
: 現在のgoroutineのdefer
リストの先頭を、新しく作成したd
に設定します。 これにより、runtime·main
関数が終了する際(またはruntime·Goexit()
が呼び出された際)に、runtime·unlockOSThread()
が確実に実行されるように、手動でdefer
エントリが登録されます。
defer
エントリの検証と解除:if(g->defer != &d || d.fn != &initDone)
:main·init()
の実行後、defer
リストの先頭が期待通りにd
であり、その関数がinitDone
であることを確認します。これは、main·init()
内で予期せぬdefer
の変更や、このdefer
エントリが誤って削除されていないことを保証するためのチェックです。g->defer = d.link;
: 検証後、手動で追加したdefer
エントリd
をdefer
リストから削除します。これにより、runtime·unlockOSThread()
が二重に呼び出されることを防ぎ、通常のruntime·unlockOSThread()
の呼び出しパスが機能するようにします。
この修正により、main.init()
内でruntime.Goexit()
が呼び出された場合でも、Goランタイムのdefer
メカニズムが発動し、runtime.unlockOSThread()
が実行されるため、OSスレッドのロックが適切に解除され、クラッシュが回避されます。
コアとなるコードの変更箇所
変更は主にsrc/pkg/runtime/proc.c
ファイル内のruntime·main
関数に集中しています。
// src/pkg/runtime/proc.c
void
runtime·main(void)
{
Defer d; // 新しく追加されたDefer構造体の宣言
newm(sysmon, nil);
// Lock the main goroutine onto this, the main OS thread,
// so that init() and main() will run on it.
// This is important for some programs that call runtime.LockOSThread during initialization
// to preserve the lock.
runtime·lockOSThread();
// Defer unlock so that runtime.Goexit during init does the unlock too.
d.fn = &initDone; // initDoneはruntime·unlockOSThreadへのポインタを持つFuncVal
d.siz = 0;
d.link = g->defer; // 既存のdeferリストの先頭にリンク
d.argp = (void*)-1; // 特別なdeferであることを示す
d.special = true;
d.free = false;
g->defer = &d; // goroutineのdeferリストの先頭をこの新しいエントリに設定
if(m != &runtime·m0)
runtime·throw("runtime·main not on m0");
runtime·newproc1(&scavenger, nil, 0, 0, runtime·main);
main·init(); // ここでruntime.Goexit()が呼ばれる可能性がある
// main·init()から戻った場合、手動で追加したdeferエントリを削除
if(g->defer != &d || d.fn != &initDone)
runtime·throw("runtime: bad defer entry after init");
g->defer = d.link;
runtime·unlockOSThread(); // 通常のunlockOSThreadの呼び出し
main·main();
}
また、このバグを再現し、修正が正しく機能することを確認するためのテストケースがtest/fixedbugs/issue5963.go
として追加されています。
// test/fixedbugs/issue5963.go
package main
import (
"os"
"runtime"
)
func init() {
c := make(chan int, 1)
defer func() {
c <- 0
}()
go func() {
os.Exit(<-c)
}()
runtime.Goexit() // ここでGoexitが呼ばれる
}
func main() {
}
このテストケースでは、init()
関数内でruntime.Goexit()
を呼び出しています。修正前は、このコードを実行するとランタイムエラーでクラッシュしていましたが、修正後は正常に終了するようになります。
コアとなるコードの解説
runtime·main
関数はGoプログラムの起動シーケンスにおいて非常に重要な役割を担っています。この関数は、OSスレッドのロック、スケジューラの初期化、そしてユーザーが記述したinit
関数やmain
関数の呼び出しを行います。
このコミットの変更は、runtime·main
関数がmain·init()
を呼び出す前に、runtime·unlockOSThread
関数を「擬似的なdefer
」として登録する点にあります。
Defer d;
: これはC言語のスタック上にDefer
構造体のインスタンスを確保します。この構造体は、Goのdefer
ステートメントが内部的に使用するもので、遅延実行される関数の情報(関数ポインタ、引数など)を保持します。static FuncVal initDone = { runtime·unlockOSThread };
:runtime·unlockOSThread
関数へのポインタを保持するFuncVal
型の静的変数を定義します。これにより、runtime·unlockOSThread
をdefer
のターゲットとして指定できるようになります。d.fn = &initDone; ... g->defer = &d;
: ここで、d
というDefer
構造体を初期化し、現在のgoroutine (g
) のdefer
リストの先頭に手動で追加しています。これにより、runtime·main
関数が正常に終了する場合でも、あるいはmain·init()
内でruntime.Goexit()
が呼び出されてruntime·main
が途中で終了する場合でも、d
に登録されたruntime·unlockOSThread
が確実に実行されるようになります。d.special = true;
は、このdefer
エントリが通常のGoコードによって追加されたものではなく、ランタイム内部の特別な目的のために追加されたものであることを示します。
main·init();
: ここでユーザーのinit
関数が実行されます。もしこの中でruntime.Goexit()
が呼び出されると、現在のgoroutineはここで終了し、d
に登録されたruntime·unlockOSThread
が実行されます。if(g->defer != &d || d.fn != &initDone) ... g->defer = d.link;
:main·init()
が正常に完了し、runtime.Goexit()
が呼び出されなかった場合、このブロックが実行されます。- 最初の
if
文は、main·init()
の実行中にdefer
リストが予期せず変更されていないか、またはd
がまだリストの先頭にあるかを確認するための防御的なチェックです。 g->defer = d.link;
は、手動で追加したd
をdefer
リストから削除します。これは、main·init()
が正常に完了した場合は、後続のruntime·unlockOSThread()
の直接呼び出しで十分であり、d
による遅延実行は不要になるためです。これにより、unlockOSThread
が二重に呼び出されることを防ぎます。
- 最初の
この巧妙な変更により、Goランタイムは、初期化フェーズにおけるruntime.Goexit()
の呼び出しに対してより堅牢になり、OSスレッドのロック状態の一貫性を保つことができるようになりました。
関連リンク
- Go Issue 5963: https://github.com/golang/go/issues/5963
- Go CL 11879045: https://golang.org/cl/11879045
参考にした情報源リンク
- Go言語の公式ドキュメント (runtimeパッケージ): https://pkg.go.dev/runtime
- Go言語のソースコード (特に
src/runtime/proc.go
,src/runtime/proc.c
,src/runtime/runtime2.go
): https://github.com/golang/go - Go言語の
defer
ステートメントに関する解説: https://go.dev/blog/defer-panic-and-recover - Go言語のスケジューラに関する解説 (M, P, Gモデル): https://go.dev/doc/effective_go#concurrency
- Go言語の
init
関数に関する解説: https://go.dev/doc/effective_go#initialization - Go言語の
runtime.LockOSThread
に関する解説: https://pkg.go.dev/runtime#LockOSThread - Go言語の
runtime.Goexit
に関する解説: https://pkg.go.dev/runtime#Goexit - Go言語の
defer
実装に関する技術記事 (例: "Go's defer, panic, and recover" by Dave Cheney): https://dave.cheney.net/2012/01/18/gos-defer-panic-and-recover (これは一般的な情報源であり、特定のコミットに直接関連するものではありませんが、defer
の理解に役立ちます) - Go言語のランタイムに関する書籍やブログ記事 (例: "Go言語による並行処理" by Rob Pike, "Go言語徹底入門" by 柴田 芳樹): これらはGoランタイムの内部動作を理解するための一般的な情報源です。