[インデックス 15646] ファイルの概要
このコミットは、Goランタイムにおける重要なバグ修正と改善を目的としています。具体的には、ゴルーチンが終了する際にM(マシン)のロック状態が適切にクリアされない問題と、cgo呼び出し中にパニックが発生した場合にm->ncgo
カウンタが正しくデクリメントされない問題を修正しています。これにより、OSスレッドの不適切なロック状態や、cgo関連の内部カウンタの不整合が解消され、ランタイムの安定性と正確性が向上します。
コミット
commit 1a4599b41ac2220c58959f434518eddb1a84060c
Author: Russ Cox <rsc@golang.org>
Date: Fri Mar 8 11:26:00 2013 -0500
runtime: clear locked bit when goroutine exits
Otherwise the next goroutine run on the m
can get inadvertently locked if it executes a cgo call
that turns on the internal lock.
While we're here, fix the cgo panic unwind to
decrement m->ncgo like the non-panic unwind does.
Fixes #4971.
R=golang-dev, iant, dvyukov
CC=golang-dev
https://golang.org/cl/7627043
GitHub上でのコミットページへのリンク
https://github.com/golang/go/commit/1a4599b41ac2220c58959f434518eddb1a84060c
元コミット内容
runtime: clear locked bit when goroutine exits
Otherwise the next goroutine run on the m
can get inadvertently locked if it executes a cgo call
that turns on the internal lock.
While we're here, fix the cgo panic unwind to
decrement m->ncgo like the non-panic unwind does.
Fixes #4971.
変更の背景
このコミットは、Goランタイムにおける2つの主要な問題を解決するために行われました。
-
ゴルーチン終了時のMのロック状態の不適切さ: Goのランタイムでは、特定の操作(特にcgo呼び出し)を行う際に、現在のOSスレッド(M)を特定のゴルーチンに「ロック」することがあります。これは、そのゴルーチンが特定のOSスレッド上で実行され続けることを保証するためです。しかし、ゴルーチンが終了した際に、このロック状態が適切にクリアされない場合がありました。その結果、次に同じM上で実行されるゴルーチンが、意図せずロックされた状態になってしまい、cgo呼び出しなどで内部ロックが有効になった際に問題を引き起こす可能性がありました。これは、GoのスケジューラがMを再利用する際に、前のゴルーチンの状態が引き継がれてしまうことによる潜在的なバグでした。
-
cgoパニック時の
m->ncgo
カウンタの不整合: cgo(C言語との相互運用)呼び出し中にGoコードでパニックが発生した場合、通常のエラーハンドリングパスとは異なるアンワインド(スタックの巻き戻し)処理が行われます。この際、Mが現在実行中のcgo呼び出しの数を追跡するカウンタであるm->ncgo
が、パニック発生時にも適切にデクリメントされないという問題がありました。これにより、m->ncgo
の値が実際のcgo呼び出し数と乖離し、ランタイムの内部状態が不整合になる可能性がありました。
これらの問題は、Goプログラムの安定性、特にcgoを多用するアプリケーションにおいて、デッドロックや予期せぬ動作を引き起こす可能性がありました。コミットメッセージに記載されているFixes #4971
は、これらの問題がGoのIssueトラッカーで報告されていたことを示唆していますが、現在の検索では該当するIssueが見つかりませんでした。しかし、コミット内容から、これらの問題がGoランタイムの正確な動作にとって重要であったことが伺えます。
前提知識の解説
このコミットを理解するためには、Goランタイムの内部構造と、ゴルーチン、M、P、cgo、およびdeferメカニズムに関する基本的な知識が必要です。
- Goランタイム: Goプログラムの実行を管理するシステムです。ゴルーチンのスケジューリング、メモリ管理、ガベージコレクション、システムコールなど、低レベルの操作を扱います。
- ゴルーチン (Goroutine): Goにおける軽量な実行単位です。OSスレッドよりもはるかに軽量で、数百万個のゴルーチンを同時に実行できます。Goランタイムがゴルーチンのスケジューリングを管理します。
- M (Machine): OSスレッドを表します。Goランタイムは、OSスレッド(M)上でゴルーチンを実行します。Mは、システムコールやcgo呼び出しなど、OSレベルの操作を実行するために必要です。
- P (Processor): 論理プロセッサを表します。Goランタイムは、MとPを組み合わせてゴルーチンをスケジューリングします。Pは、ゴルーチンを実行するためのコンテキストを提供し、MがPにアタッチされることでゴルーチンを実行できます。
- cgo: GoプログラムからC言語のコードを呼び出すためのメカニズムです。cgo呼び出しは、Goランタイムの通常のスケジューリングとは異なる特別な処理を必要とします。CコードがOSスレッドをブロックする可能性があるため、Goランタイムはcgo呼び出し中にMを特定のゴルーチンにロックすることがあります。
lockOSThread()
/unlockOSThread()
: Goランタイムの関数で、現在のゴルーチンを特定のOSスレッド(M)にロック/アンロックします。lockOSThread()
が呼び出されると、そのゴルーチンは他のMに移動することなく、現在のM上で実行され続けます。これは、OSスレッドのIDや状態に依存するCライブラリを呼び出す際に重要です。m->locked
: M構造体内のフラグで、Mが特定のゴルーチンにロックされているかどうかを示します。LockExternal
は、外部からのロック(例えばlockOSThread()
によるもの)を示します。m->ncgo
: M構造体内のカウンタで、現在このM上で実行されているcgo呼び出しの数を追跡します。cgo呼び出しの開始時にインクリメントされ、終了時にデクリメントされます。defer
: Goのキーワードで、関数の実行が終了する直前に指定された関数を呼び出すことを保証します。これは、リソースの解放やクリーンアップ処理によく使用されます。cgo呼び出しのコンテキストでは、パニックが発生した場合でもunlockOSThread
のようなクリーンアップ関数が確実に実行されるようにするために使用されます。runtime·throw
: Goランタイム内部で使用される関数で、回復不可能なエラーが発生した場合にプログラムを終了させます。
技術的詳細
このコミットは、主にsrc/pkg/runtime/cgocall.c
とsrc/pkg/runtime/proc.c
の2つのファイルにわたる変更を含んでいます。
src/pkg/runtime/cgocall.c
の変更
このファイルは、GoからCへの呼び出し(cgo)に関連するランタイムの低レベルな処理を扱います。
-
endcgo
関数の導入とdefer
の変更:- 以前は、
runtime·cgocall
内でruntime·unlockOSThread
を直接defer
していました。 - 今回の変更で、
endcgo
という新しい静的関数が導入され、runtime·unlockOSThread()
とm->ncgo--
の両方を実行するようになりました。 runtime·cgocall
内のdefer
は、unlockOSThread
ではなく、この新しいendcgo
関数を指すendcgoV
というFuncVal
に設定されるようになりました。- これにより、cgo呼び出しが正常に終了した場合でも、パニックによって中断された場合でも、
endcgo
が確実に実行され、m->ncgo
がデクリメントされるとともに、OSスレッドのロックが解除されるようになりました。
- 以前は、
-
パニック時の
m->ncgo
デクリメントの修正:- 以前のコードでは、
runtime·cgocall
の最後にm->ncgo--
が直接記述されていました。しかし、cgo呼び出し中にパニックが発生した場合、この行に到達する前にスタックがアンワインドされてしまい、m->ncgo
がデクリメントされない問題がありました。 - 新しい
endcgo
関数にm->ncgo--
を移動し、これをdefer
することで、パニック時でもendcgo
が実行され、m->ncgo
が確実にデクリメントされるようになりました。これは、defer
がパニック発生時でも実行されるというGoの保証を利用したものです。
- 以前のコードでは、
-
defer
エントリの検証の強化:runtime·cgocall
の終了時に、g->defer
が期待されるd
(現在のdeferエントリ)であり、その関数ポインタがendcgoV
であることを確認するチェックが追加されました。これにより、ランタイムの内部状態の整合性がより厳密に検証されるようになりました。
src/pkg/runtime/proc.c
の変更
このファイルは、Goランタイムのプロセッサ(P)とMの管理、ゴルーチンのスケジューリングなど、コアな処理を扱います。
- ゴルーチン終了時の
m->locked
クリア:goexit0
関数(ゴルーチンが終了する際に呼び出される)において、m->locked = 0;
という行が追加されました。- これにより、ゴルーチンが終了する際に、そのゴルーチンがMにロックされていた場合でも、
m->locked
フラグが明示的に0にリセットされるようになりました。 - 以前は、
m->locked
がLockExternal
以外の値を持つ場合にパニックを発生させるチェックがありましたが、この変更により、ゴルーチン終了時にm->locked
が確実にクリアされるため、次に同じM上で実行されるゴルーチンが誤ってロック状態を引き継ぐことがなくなります。
src/pkg/runtime/syscall_windows_test.go
の変更
このファイルは、Windows固有のシステムコールテストを扱います。
TestCallbackPanic
の有効化:- 以前は、
TestCallbackPanic
というテストがt.Skip
によって無効化されていました。スキップの理由としてhttp://golang.org/issue/4971
が挙げられていました。 - 今回のコミットで、この
t.Skip
の行が削除されました。これは、cgocall.c
におけるパニック時のm->ncgo
デクリメントの修正により、このテストが正しく動作するようになったため、テストを再度有効化したことを意味します。
- 以前は、
これらの変更は、Goランタイムの堅牢性と正確性を向上させ、特にcgoを使用するアプリケーションにおける潜在的なバグやデッドロックのリスクを低減します。
コアとなるコードの変更箇所
src/pkg/runtime/cgocall.c
--- a/src/pkg/runtime/cgocall.c
+++ b/src/pkg/runtime/cgocall.c
@@ -95,7 +95,8 @@ static void unwindm(void);\
// Call from Go to C.
-static FuncVal unlockOSThread = { runtime·unlockOSThread };
+static void endcgo(void);\
+static FuncVal endcgoV = { endcgo };
void
runtime·cgocall(void (*fn)(void*), void *arg)\
@@ -123,7 +124,7 @@ runtime·cgocall(void (*fn)(void*), void *arg)\
* cgo callback. Add entry to defer stack in case of panic.\
*/\
runtime·lockOSThread();\
-\td.fn = &unlockOSThread;\
+\td.fn = &endcgoV;\
d.siz = 0;\
d.link = g->defer;\
d.argp = (void*)-1; // unused because unlockm never recovers\
@@ -148,6 +149,16 @@ runtime·cgocall(void (*fn)(void*), void *arg)\
runtime·asmcgocall(fn, arg);\
runtime·exitsyscall();\
+\tif(g->defer != &d || d.fn != &endcgoV)\
+\t\truntime·throw(\"runtime: bad defer entry in cgocallback\");\
+\tg->defer = d.link;\
+\tendcgo();\
+}\
+\n+static void\
++endcgo(void)\
++{\
+\truntime·unlockOSThread();\
\tm->ncgo--;\
\tif(m->ncgo == 0) {\
\t\t// We are going back to Go and are not in a recursive\
@@ -156,11 +167,6 @@ runtime·cgocall(void (*fn)(void*), void *arg)\
\t\tm->cgomal = nil;\
\t}\
\n-\tif(g->defer != &d || d.fn != &unlockOSThread)\
-\t\truntime·throw(\"runtime: bad defer entry in cgocallback\");\
-\tg->defer = d.link;\
-\truntime·unlockOSThread();\
-\n \tif(raceenabled)\
\t\truntime·raceacquire(&cgosync);\
}\
src/pkg/runtime/proc.c
--- a/src/pkg/runtime/proc.c
+++ b/src/pkg/runtime/proc.c
@@ -1173,6 +1173,11 @@ goexit0(G *gp)\
gp->lockedm = nil;\
m->curg = nil;\
m->lockedg = nil;\
+\tif(m->locked & ~LockExternal) {\
+\t\truntime·printf(\"invalid m->locked = %d\", m->locked);\
+\t\truntime·throw(\"internal lockOSThread error\");\
+\t}\t\
+\tm->locked = 0;\
\truntime·unwindstack(gp, nil);\
\tgfput(m->p, gp);\
\tschedule();\
src/pkg/runtime/syscall_windows_test.go
--- a/src/pkg/runtime/syscall_windows_test.go
+++ b/src/pkg/runtime/syscall_windows_test.go
@@ -172,9 +172,6 @@ func TestCallbackGC(t *testing.T) {\
}\
\n func TestCallbackPanic(t *testing.T) {\
-\t// TODO(brainman): http://golang.org/issue/4971\
-\tt.Skip(\"TestCallbackPanic disabled: http://golang.org/issue/4971\")\
-\n \t// Make sure panic during callback unwinds properly.\
\tif runtime.LockedOSThread() {\
\t\tt.Fatal(\"locked OS thread on entry to TestCallbackPanic\")\
コアとなるコードの解説
src/pkg/runtime/cgocall.c
endcgo
関数の導入:static void endcgo(void);
とstatic FuncVal endcgoV = { endcgo };
が追加されました。これは、cgo呼び出しの終了時に実行される新しいヘルパー関数endcgo
を定義し、その関数ポインタをFuncVal
構造体endcgoV
に格納しています。runtime·cgocall
内のd.fn = &unlockOSThread;
がd.fn = &endcgoV;
に変更されました。これにより、defer
される関数がruntime·unlockOSThread
からendcgo
に変更されます。endcgo
関数は、runtime·unlockOSThread()
とm->ncgo--
の両方を実行します。これにより、cgo呼び出しが正常に完了した場合でも、パニックによって中断された場合でも、OSスレッドのロック解除とm->ncgo
カウンタのデクリメントが確実に実行されるようになります。これは、defer
の特性(関数が終了する際に必ず実行される)を利用した重要な修正です。
- 古い
defer
処理の削除:runtime·cgocall
の末尾にあった、g->defer
のチェックとruntime·unlockOSThread()
の呼び出し、m->ncgo--
の行が削除されました。これらの処理はすべてendcgo
関数に集約され、defer
によって実行されるようになりました。これにより、コードの重複が解消され、パニック時の処理も統一されました。
src/pkg/runtime/proc.c
goexit0
におけるm->locked
のクリア:goexit0
関数(ゴルーチンが終了する際に呼び出される)の最後に、m->locked = 0;
という行が追加されました。- この変更により、ゴルーチンが終了する際に、そのゴルーチンがOSスレッド(M)にロックされていた場合でも、
m->locked
フラグが明示的に0にリセットされます。これにより、次に同じM上で実行されるゴルーチンが、前のゴルーチンの不適切なロック状態を引き継ぐことがなくなり、ランタイムの動作がより予測可能になります。 if(m->locked & ~LockExternal)
のチェックは、LockExternal
以外の内部ロックが残っている場合にエラーを検出するためのものです。m->locked = 0;
の追加により、このチェックがより確実に成功するようになります。
src/pkg/runtime/syscall_windows_test.go
TestCallbackPanic
の有効化:TestCallbackPanic
関数内のt.Skip("TestCallbackPanic disabled: http://golang.org/issue/4971")
という行が削除されました。- これは、
cgocall.c
におけるパニック時のm->ncgo
デクリメントの修正により、このテストが正しく動作するようになったため、テストを再度有効化したことを意味します。これにより、cgo呼び出し中のパニック処理が正しく行われることが自動テストによって保証されるようになりました。
これらの変更は、Goランタイムの内部的な整合性を高め、特にcgoを使用する際の安定性と信頼性を向上させるものです。
関連リンク
- Go CL 7627043: https://golang.org/cl/7627043
参考にした情報源リンク
- コミット情報:
/home/orange/Project/comemo/commit_data/15646.txt
- Go言語の公式ドキュメント (Goランタイム、ゴルーチン、cgoに関する一般的な情報)
- Goのソースコード (特に
src/pkg/runtime
ディレクトリ内のファイル) - Goの
defer
ステートメントに関するドキュメント - Goのスケジューラに関する一般的な知識