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

[インデックス 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つの主要な問題を解決するために行われました。

  1. ゴルーチン終了時のMのロック状態の不適切さ: Goのランタイムでは、特定の操作(特にcgo呼び出し)を行う際に、現在のOSスレッド(M)を特定のゴルーチンに「ロック」することがあります。これは、そのゴルーチンが特定のOSスレッド上で実行され続けることを保証するためです。しかし、ゴルーチンが終了した際に、このロック状態が適切にクリアされない場合がありました。その結果、次に同じM上で実行されるゴルーチンが、意図せずロックされた状態になってしまい、cgo呼び出しなどで内部ロックが有効になった際に問題を引き起こす可能性がありました。これは、GoのスケジューラがMを再利用する際に、前のゴルーチンの状態が引き継がれてしまうことによる潜在的なバグでした。

  2. 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.csrc/pkg/runtime/proc.cの2つのファイルにわたる変更を含んでいます。

src/pkg/runtime/cgocall.cの変更

このファイルは、GoからCへの呼び出し(cgo)に関連するランタイムの低レベルな処理を扱います。

  1. endcgo関数の導入とdeferの変更:

    • 以前は、runtime·cgocall内でruntime·unlockOSThreadを直接deferしていました。
    • 今回の変更で、endcgoという新しい静的関数が導入され、runtime·unlockOSThread()m->ncgo--の両方を実行するようになりました。
    • runtime·cgocall内のdeferは、unlockOSThreadではなく、この新しいendcgo関数を指すendcgoVというFuncValに設定されるようになりました。
    • これにより、cgo呼び出しが正常に終了した場合でも、パニックによって中断された場合でも、endcgoが確実に実行され、m->ncgoがデクリメントされるとともに、OSスレッドのロックが解除されるようになりました。
  2. パニック時のm->ncgoデクリメントの修正:

    • 以前のコードでは、runtime·cgocallの最後にm->ncgo--が直接記述されていました。しかし、cgo呼び出し中にパニックが発生した場合、この行に到達する前にスタックがアンワインドされてしまい、m->ncgoがデクリメントされない問題がありました。
    • 新しいendcgo関数にm->ncgo--を移動し、これをdeferすることで、パニック時でもendcgoが実行され、m->ncgoが確実にデクリメントされるようになりました。これは、deferがパニック発生時でも実行されるというGoの保証を利用したものです。
  3. deferエントリの検証の強化:

    • runtime·cgocallの終了時に、g->deferが期待されるd(現在のdeferエントリ)であり、その関数ポインタがendcgoVであることを確認するチェックが追加されました。これにより、ランタイムの内部状態の整合性がより厳密に検証されるようになりました。

src/pkg/runtime/proc.cの変更

このファイルは、Goランタイムのプロセッサ(P)とMの管理、ゴルーチンのスケジューリングなど、コアな処理を扱います。

  1. ゴルーチン終了時のm->lockedクリア:
    • goexit0関数(ゴルーチンが終了する際に呼び出される)において、m->locked = 0;という行が追加されました。
    • これにより、ゴルーチンが終了する際に、そのゴルーチンがMにロックされていた場合でも、m->lockedフラグが明示的に0にリセットされるようになりました。
    • 以前は、m->lockedLockExternal以外の値を持つ場合にパニックを発生させるチェックがありましたが、この変更により、ゴルーチン終了時にm->lockedが確実にクリアされるため、次に同じM上で実行されるゴルーチンが誤ってロック状態を引き継ぐことがなくなります。

src/pkg/runtime/syscall_windows_test.goの変更

このファイルは、Windows固有のシステムコールテストを扱います。

  1. 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を使用する際の安定性と信頼性を向上させるものです。

関連リンク

参考にした情報源リンク

  • コミット情報: /home/orange/Project/comemo/commit_data/15646.txt
  • Go言語の公式ドキュメント (Goランタイム、ゴルーチン、cgoに関する一般的な情報)
  • Goのソースコード (特にsrc/pkg/runtimeディレクトリ内のファイル)
  • Goのdeferステートメントに関するドキュメント
  • Goのスケジューラに関する一般的な知識