[インデックス 15079] ファイルの概要
コミット
commit b0a29f393b5672c37355eb7a5f126cc0e1537834
Author: Russ Cox <rsc@golang.org>
Date: Fri Feb 1 08:34:41 2013 -0800
runtime: cgo-related fixes
* Separate internal and external LockOSThread, for cgo safety.
* Show goroutine that made faulting cgo call.
* Never start a panic due to a signal caused by a cgo call.
Fixes #3774.
Fixes #3775.
Fixes #3797.
R=golang-dev, iant
CC=golang-dev
https://golang.org/cl/7228081
GitHub上でのコミットページへのリンク
https://github.com/golang/go/commit/b0a29f393b5672c37355eb7a5f126cc0e1537834
元コミット内容
このコミットは、GoランタイムにおけるCgo関連の複数の修正を目的としています。具体的には以下の3点です。
LockOSThread
の内部と外部の分離: Cgoの安全性向上のため、runtime.LockOSThread
の内部的な使用と外部(ユーザーコード)からの使用を分離します。- Cgo呼び出しで障害が発生したGoroutineの表示: Cgo呼び出し中に発生した障害(例: セグメンテーション違反)の原因となったGoroutineをトレースバックに表示するように改善します。これにより、デバッグ時の情報が豊富になります。
- Cgo呼び出しによって引き起こされたシグナルによるパニックの防止: Cgoコードの実行中にOSからシグナルが送られた場合でも、Goランタイムがパニックを開始しないように修正します。これにより、CgoとGoの間のシグナルハンドリングの競合や予期せぬ挙動を防ぎます。
これらの変更は、GoのIssue #3774、#3775、#3797を修正するものです。
変更の背景
GoプログラムがCコードと連携するCgoを使用する場合、GoランタイムとCランタイムの間でOSスレッドの管理、シグナルハンドリング、スタックの扱いなど、様々な低レベルな相互作用が発生します。これらの相互作用は複雑であり、GoのGoroutineスケジューラとOSスレッドの挙動が絡み合うことで、デバッグが困難な問題や予期せぬクラッシュを引き起こす可能性がありました。
このコミットは、特に以下の問題に対処するために行われました。
LockOSThread
の誤用/競合:runtime.LockOSThread
は、現在のGoroutineを特定のOSスレッドに固定するために使用されます。これは、Cgoが特定のOSスレッドのコンテキストを必要とする場合に重要ですが、Goランタイム内部でも使用されるため、内部と外部の呼び出しが競合したり、意図しないロック/アンロックが発生したりする可能性がありました。- Cgo起因のクラッシュのデバッグの困難さ: Cgo呼び出し中にCコード側でセグメンテーション違反などの致命的なエラーが発生した場合、Goランタイムのトレースバックには、そのエラーを引き起こしたGoのGoroutineの情報が不足していることがありました。これにより、問題の根本原因を特定するのが困難でした。
- シグナルハンドリングの不整合: OSからのシグナル(例: SIGSEGV, SIGBUS)は、GoランタイムとCランタイムの両方で処理される可能性があります。Cgo呼び出し中にCコードがシグナルを受け取った場合、Goランタイムがそれをパニックとして処理しようとすると、GoとCのシグナルハンドリングメカニズムが衝突し、不安定な挙動やクラッシュにつながることがありました。
これらの問題は、GoとCの相互運用性の安定性とデバッグの容易性を損なうものであり、このコミットはそれらを改善することを目的としています。
前提知識の解説
このコミットを理解するためには、以下のGoランタイムとCgoに関する概念を理解しておく必要があります。
- Goroutine (ゴルーチン): Goランタイムによって管理される軽量な実行スレッドです。Goの並行処理の基本単位であり、OSスレッドよりもはるかに軽量で、数百万個作成することも可能です。GoスケジューラがGoroutineをOSスレッドにマッピングして実行します。
- OS Thread (OSスレッド): オペレーティングシステムによって管理される実行単位です。Goランタイムは、Goroutineを実行するために複数のOSスレッドを使用します。
- M (Machine): Goランタイム内部の概念で、OSスレッドを表します。各Mは1つのOSスレッドに対応し、Goroutineを実行するためのコンテキスト(スタック、レジスタなど)を保持します。
- G (Goroutine): Goランタイム内部の概念で、Goroutineを表します。
- P (Processor): Goランタイム内部の概念で、Goroutineを実行するための論理プロセッサを表します。PはMにアタッチされ、Goroutineの実行をスケジュールします。
- Cgo: GoプログラムからC言語のコードを呼び出すためのGoの機能です。Cgoを使用すると、既存のCライブラリをGoから利用したり、Goでは実装が難しい低レベルな処理をCで記述したりすることができます。
runtime.LockOSThread()
: Goの標準ライブラリruntime
パッケージが提供する関数で、現在のGoroutineを現在のOSスレッドに固定します。この関数が呼び出されると、そのGoroutineは他のOSスレッドに移動することなく、常に同じOSスレッド上で実行されるようになります。これは、Cライブラリがスレッドローカルストレージを使用したり、特定のOSスレッドのコンテキストに依存したりする場合に必要となることがあります。runtime.UnlockOSThread()
:runtime.LockOSThread()
で固定されたGoroutineのOSスレッドへの固定を解除します。- シグナル (Signal): オペレーティングシステムがプロセスに送信する非同期の通知です。シグナルは、エラー(例: セグメンテーション違反、浮動小数点例外)、外部イベント(例: Ctrl+Cによる割り込み)、または他のプロセスからの通信など、様々な理由で発生します。
- パニック (Panic): Goにおける回復不可能なエラーのメカニズムです。パニックが発生すると、通常のプログラムフローは中断され、遅延関数が実行された後、プログラムはクラッシュします。
技術的詳細
このコミットの技術的詳細は、主に以下の3つの領域に分けられます。
-
LockOSThread
の内部/外部分離:- 以前は、
runtime.LockOSThread
とruntime.UnlockOSThread
は、Goランタイム内部とユーザーコードの両方から直接呼び出されていました。これにより、ランタイムが内部的にスレッドをロックしたい場合と、ユーザーがCgoのためにスレッドをロックしたい場合とで、状態管理が複雑になる可能性がありました。 - このコミットでは、
m
(OSスレッドを表す構造体) にlocked
という新しいフィールドが追加されました。このフィールドは、LockExternal
(ユーザーコードからのロック) とLockInternal
(ランタイム内部からのロック) の2つのビットフラグと、内部ロックのネスト深度を追跡するためのカウンタとして機能します。 runtime.LockOSThread
(外部API) はm->locked
にLockExternal
フラグを設定し、内部のLockOSThread
関数を呼び出します。runtime.lockOSThread
(新しい内部API) はm->locked
にLockInternal
を加算し、内部のLockOSThread
関数を呼び出します。- 同様に、
runtime.UnlockOSThread
(外部API) はLockExternal
フラグをクリアし、内部のUnlockOSThread
関数を呼び出します。 runtime.unlockOSThread
(新しい内部API) はLockInternal
を減算し、内部のUnlockOSThread
関数を呼び出します。- これにより、ランタイムは自身の内部的なスレッドロックの必要性を、ユーザーがCgoのために行うスレッドロックとは独立して管理できるようになります。特に、
runtime.cgocall
の中でLockOSThread
を呼び出す際に、この新しい内部APIが使用されるようになります。
- 以前は、
-
Cgo呼び出しで障害が発生したGoroutineの表示:
- Cgo呼び出し中にCコード側でシグナル(例: SIGSEGV)が発生し、Goランタイムのシグナルハンドラがそれを捕捉した場合、以前はトレースバックに適切なGoroutine情報が含まれないことがありました。
- このコミットでは、各OS固有のシグナルハンドラ(
signal_darwin_386.c
,signal_linux_amd64.c
など)に修正が加えられました。 - シグナルが
m->g0
(スケジューラGoroutine) で発生し、かつm->lockedg
(Cgo呼び出しによってOSスレッドにロックされたGoroutine) が存在し、m->ncgo > 0
(Cgo呼び出しが進行中) の場合、トレースバックの対象となるGoroutineをm->lockedg
に切り替えるロジックが追加されました。 - これにより、Cgo呼び出し中にC側で発生した障害であっても、そのCgo呼び出しを行ったGoのGoroutineのスタックトレースが正確に表示されるようになり、デバッグが大幅に容易になります。
-
Cgo呼び出しによって引き起こされたシグナルによるパニックの防止:
- Cgo呼び出し中にCコードがシグナルを受け取った場合、GoランタイムがそのシグナルをGoのパニックとして処理しようとすると、問題が発生する可能性がありました。
- シグナルハンドラ内の
SigPanic
フラグが設定されているシグナル(通常、致命的なエラーを示すシグナル)について、gp == nil || gp == m->g0
の場合にgoto Throw
(パニックを開始する処理) する条件が追加されました。 - これは、シグナルがGoのGoroutineコンテキストではなく、OSスレッドのコンテキスト(特に
m->g0
)で発生した場合に、Goのパニックメカニズムを迂回し、より低レベルなエラー処理(通常はプログラムの終了)に委ねることを意味します。 - これにより、CgoとGoのシグナルハンドリングの間の競合が緩和され、Cgo呼び出し中のシグナルによってGoランタイムが予期せぬパニックに陥ることを防ぎます。
コアとなるコードの変更箇所
このコミットでは、主に以下のファイルが変更されています。
src/pkg/runtime/cgocall.c
: Cgo呼び出しのGo側エントリポイント。LockOSThread
の呼び出しが変更されています。src/pkg/runtime/proc.c
: Goランタイムのプロセッサ管理とスケジューリング関連のコード。LockOSThread
とUnlockOSThread
の実装が変更され、内部/外部の分離が導入されています。src/pkg/runtime/runtime.h
: Goランタイムの主要なヘッダファイル。M
構造体にlocked
フィールドが追加され、新しいLockExternal
とLockInternal
の列挙型が定義されています。また、LockOSThread
とUnlockOSThread
の関数シグネチャが変更されています。src/pkg/runtime/signal_*.c
: 各OSおよびアーキテクチャ固有のシグナルハンドラ。シグナル発生時のGoroutine情報の取得ロジックと、パニック開始条件が変更されています。src/pkg/runtime/signal_darwin_386.c
src/pkg/runtime/signal_darwin_amd64.c
src/pkg/runtime/signal_freebsd_386.c
src/pkg/runtime/signal_freebsd_amd64.c
src/pkg/runtime/signal_freebsd_arm.c
src/pkg/runtime/signal_linux_386.c
src/pkg/runtime/signal_linux_amd64.c
src/pkg/runtime/signal_linux_arm.c
src/pkg/runtime/signal_netbsd_386.c
src/pkg/runtime/signal_netbsd_amd64.c
src/pkg/runtime/signal_openbsd_386.c
src/pkg/runtime/signal_openbsd_amd64.c
src/pkg/runtime/signal_windows_386.c
src/pkg/runtime/signal_windows_amd64.c
src/pkg/runtime/traceback_arm.c
,src/pkg/runtime/traceback_x86.c
: トレースバック生成関連のコード。Gsyscall
状態のGoroutineのレジスタ情報をオーバーライドするロジックが追加されています。misc/cgo/test/cgo_test.go
,misc/cgo/test/issue3775.go
: 新しいテストケースが追加されています。
コアとなるコードの解説
src/pkg/runtime/runtime.h
struct M
{
// ...
uint32 locked; // tracking for LockOSThread
// ...
};
// The m->locked word holds a single bit saying whether
// external calls to LockOSThread are in effect, and then a counter
// of the internal nesting depth of lockOSThread / unlockOSThread.
enum
{
LockExternal = 1,
LockInternal = 2,
};
// ...
void runtime·lockOSThread(void);
void runtime·unlockOSThread(void);
M
構造体に locked
フィールドが追加され、LockExternal
と LockInternal
という新しい列挙型が定義されています。locked
フィールドは、外部からの LockOSThread
呼び出しの状態と、内部的な lockOSThread
/unlockOSThread
のネスト深度を追跡するために使用されます。また、新しい内部関数 runtime·lockOSThread
と runtime·unlockOSThread
が宣言されています。
src/pkg/runtime/proc.c
// 以前の LockOSThread
// void runtime·LockOSThread(void) { ... }
static void
LockOSThread(void)
{
m->lockedg = g;
g->lockedm = m;
}
void
runtime·LockOSThread(void)
{
m->locked |= LockExternal;
LockOSThread();
}
void
runtime·lockOSThread(void)
{
m->locked += LockInternal;
LockOSThread();
}
// 以前の UnlockOSThread
// void runtime·UnlockOSThread(void) { ... }
static void
UnlockOSThread(void)
{
if(m->locked != 0)
return;
m->lockedg = nil;
g->lockedm = nil;
}
void
runtime·UnlockOSThread(void)
{
m->locked &= ~LockExternal;
UnlockOSThread();
}
void
runtime·unlockOSThread(void)
{
if(m->locked < LockInternal)
runtime·throw("runtime: internal error: misuse of lockOSThread/unlockOSThread");
m->locked -= LockInternal;
UnlockOSThread();
}
LockOSThread
と UnlockOSThread
の実装が大きく変更されています。
static
なLockOSThread
とUnlockOSThread
関数が導入され、実際のOSスレッドへのGoroutineの固定/解除ロジックがカプセル化されています。- 公開APIである
runtime.LockOSThread
はm->locked
にLockExternal
フラグを設定してから内部のLockOSThread
を呼び出します。 - 新しい内部APIである
runtime.lockOSThread
はm->locked
にLockInternal
を加算してから内部のLockOSThread
を呼び出します。 UnlockOSThread
も同様に、m->locked
の状態に基づいてロックを解除するかどうかを判断します。runtime.unlockOSThread
では、内部ロックのネスト深度が不正な場合にパニックを発生させるチェックが追加されています。schedule
関数内で、GoroutineがロックされていたMから切り離される際にm->locked = 0;
が追加され、ロック状態がリセットされるようになっています。
src/pkg/runtime/cgocall.c
// 以前の unlockm 関数は削除され、runtime·unlockOSThread が直接呼ばれる
// static void unlockm(void);
void runtime·cgocall(void (*fn)(void*), void *arg)
{
// ...
// Lock g to m to ensure we stay on the same stack if we do a
// cgo callback. Add entry to defer stack in case of panic.
runtime·lockOSThread(); // 新しい内部APIを使用
d.fn = (byte*)runtime·unlockOSThread; // defer関数も新しい内部APIに
// ...
m->ncgo++;
// ...
if(g->defer != &d || d.fn != (byte*)runtime·unlockOSThread)
runtime·throw("runtime: bad defer entry in cgocallback");
g->defer = d.link;
runtime·unlockOSThread(); // 新しい内部APIを使用
// ...
}
runtime.cgocall
関数内で、OSスレッドのロック/アンロックに新しい内部APIである runtime.lockOSThread()
と runtime.unlockOSThread()
が使用されるようになりました。これにより、Cgo呼び出し中のスレッド固定が、ランタイムの他の内部的なスレッド固定と適切に協調するようになります。また、パニック時のdeferスタックに登録される関数も runtime.unlockOSThread
に変更されています。
src/pkg/runtime/signal_darwin_386.c
(他のOS/アーキテクチャのシグナルハンドラも同様)
void runtime·sighandler(int32 sig, Siginfo *info, void *context, G *gp)
{
// ...
if(info->si_code != SI_USER && (t->flags & SigPanic)) {
if(gp == nil || gp == m->g0) // 変更点: gp == m->g0 の条件が追加
goto Throw;
// ...
}
// ...
Throw:
// ...
runtime·printf("PC=%x\\n", r->eip);
if(m->lockedg != nil && m->ncgo > 0 && gp == m->g0) { // 変更点: Cgo関連のGoroutine情報表示ロジック
runtime·printf("signal arrived during cgo execution\\n");
gp = m->lockedg; // トレースバック対象のGoroutineを lockedg に変更
}
runtime·printf("\\n");
// ...
}
シグナルハンドラでは、シグナルが m->g0
(スケジューラGoroutine) で発生した場合にパニックを回避する条件が追加されました。これは、Cgo呼び出し中にCコード側で発生したシグナルがGoランタイムのパニックを引き起こすのを防ぐためです。
さらに、m->lockedg
が存在し、Cgo呼び出しが進行中 (m->ncgo > 0
) で、シグナルが m->g0
で捕捉された場合、トレースバックの対象となるGoroutineを m->lockedg
(Cgo呼び出しによってOSスレッドに固定されたGoroutine) に切り替えるロジックが追加されました。これにより、Cgo起因のクラッシュ時に、より関連性の高いGoのGoroutineスタックトレースが表示されるようになります。
misc/cgo/test/issue3775.go
package cgotest
/*
void lockOSThreadCallback(void);
inline static void lockOSThreadC(void)
{
lockOSThreadCallback();
}
int usleep(unsigned usec);
*/
import "C"
import (
"runtime"
"testing"
)
func test3775(t *testing.T) {
// Used to panic because of the UnlockOSThread below.
C.lockOSThreadC()
}
//export lockOSThreadCallback
func lockOSThreadCallback() {
runtime.LockOSThread()
runtime.UnlockOSThread()
go C.usleep(10000)
runtime.Gosched()
}
この新しいテストケースは、Issue #3775に関連する問題を再現し、修正が正しく機能することを確認するために追加されました。Cgoを介して runtime.LockOSThread()
と runtime.UnlockOSThread()
を呼び出し、その後にGoroutineをスケジュールすることで、以前パニックを引き起こしていたシナリオをテストします。
関連リンク
- Go CL 7228081: https://golang.org/cl/7228081
参考にした情報源リンク
- GoのIssue #3774, #3775, #3797は、Goの公式GitHubリポジトリでは直接見つかりませんでした。これらは非常に古い、または内部的なトラッカーのIssue番号である可能性があります。
- GoのCgoに関する公式ドキュメント: https://pkg.go.dev/cmd/cgo
- Goのランタイムに関する一般的な情報源 (例: Goのスケジューラ、Goroutine、M, P, Gの概念):
- The Go scheduler: https://go.dev/doc/articles/go_scheduler.html
- Go's work-stealing scheduler: https://go.dev/blog/go11sched
- Goのシグナルハンドリングに関する情報源: https://pkg.go.dev/os/signal
- Goのパニックとリカバリに関する情報源: https://go.dev/blog/defer-panic-and-recover
runtime.LockOSThread
のドキュメント: https://pkg.go.dev/runtime#LockOSThreadruntime.UnlockOSThread
のドキュメント: https://pkg.go.dev/runtime#UnlockOSThread