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

[インデックス 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.unlockOSThreaddeferのように扱うことで、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()が実行されないことでした。

このコミットでは、以下の変更が加えられました。

  1. Defer構造体の導入: runtime·main関数内にDefer d;という行が追加されました。これは、Goのdeferステートメントが内部的に使用するDefer構造体と同じものです。
  2. initDone FuncValの定義: static FuncVal initDone = { runtime·unlockOSThread };という行が追加されました。これは、runtime·unlockOSThread関数へのポインタを持つFuncVal(関数値)を定義しています。
  3. 手動での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エントリが登録されます。
  4. deferエントリの検証と解除:
    • if(g->defer != &d || d.fn != &initDone): main·init()の実行後、deferリストの先頭が期待通りにdであり、その関数がinitDoneであることを確認します。これは、main·init()内で予期せぬdeferの変更や、このdeferエントリが誤って削除されていないことを保証するためのチェックです。
    • g->defer = d.link;: 検証後、手動で追加したdeferエントリddeferリストから削除します。これにより、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」として登録する点にあります。

  1. Defer d;: これはC言語のスタック上にDefer構造体のインスタンスを確保します。この構造体は、Goのdeferステートメントが内部的に使用するもので、遅延実行される関数の情報(関数ポインタ、引数など)を保持します。
  2. static FuncVal initDone = { runtime·unlockOSThread };: runtime·unlockOSThread関数へのポインタを保持するFuncVal型の静的変数を定義します。これにより、runtime·unlockOSThreaddeferのターゲットとして指定できるようになります。
  3. 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コードによって追加されたものではなく、ランタイム内部の特別な目的のために追加されたものであることを示します。
  4. main·init();: ここでユーザーのinit関数が実行されます。もしこの中でruntime.Goexit()が呼び出されると、現在のgoroutineはここで終了し、dに登録されたruntime·unlockOSThreadが実行されます。
  5. if(g->defer != &d || d.fn != &initDone) ... g->defer = d.link;: main·init()が正常に完了し、runtime.Goexit()が呼び出されなかった場合、このブロックが実行されます。
    • 最初のif文は、main·init()の実行中にdeferリストが予期せず変更されていないか、またはdがまだリストの先頭にあるかを確認するための防御的なチェックです。
    • g->defer = d.link; は、手動で追加したddeferリストから削除します。これは、main·init()が正常に完了した場合は、後続のruntime·unlockOSThread()の直接呼び出しで十分であり、dによる遅延実行は不要になるためです。これにより、unlockOSThreadが二重に呼び出されることを防ぎます。

この巧妙な変更により、Goランタイムは、初期化フェーズにおけるruntime.Goexit()の呼び出しに対してより堅牢になり、OSスレッドのロック状態の一貫性を保つことができるようになりました。

関連リンク

参考にした情報源リンク