[インデックス 16736] ファイルの概要
このコミットは、GoランタイムがGoによって作成されていない外部スレッド(Cgoなどを介して作成されたスレッド)でシグナルを受信した際の挙動を修正するものです。特に、Goランタイムが管理していないスレッドでシグナルが発生した場合に、プログラムが予期せず終了するのではなく、適切にシグナルを処理できるように改善されています。
コミット
commit 2f1ead709548873463b93de549839d3acbd27633
Author: Shenghou Ma <minux.ma@gmail.com>
Date: Fri Jul 12 04:39:39 2013 +0800
runtime: correctly handle signals received on foreign threads
Fixes #3250.
R=rsc
CC=golang-dev
https://golang.org/cl/10757044
GitHub上でのコミットページへのリンク
https://github.com/golang/go/commit/2f1ead709548873463b93de549839d3acbd27633
元コミット内容
runtime: correctly handle signals received on foreign threads
Fixes #3250.
R=rsc
CC=golang-dev
https://golang.org/cl/10757044
変更の背景
GoプログラムがC言語のコードと連携するCgoを使用する場合、Goランタイムが管理していないスレッド(「外部スレッド」または「foreign threads」)が生成されることがあります。これらの外部スレッドでOSシグナル(例: SIGSEGV
, SIGCHLD
など)が発生した場合、Goランタイムはこれまでそのシグナルを適切に処理できず、runtime.badsignal
関数を呼び出してプログラムを強制終了させていました。
元の実装では、Goランタイムが管理するm
(machine) 構造体やg
(goroutine) 構造体が存在しないスレッドでシグナルを受信すると、runtime.badsignal
が呼び出され、エラーメッセージを出力してプログラムが終了していました。これは、Goのシグナルハンドリングメカニズムが、Goランタイムのコンテキスト(m
とg
)に依存しているためです。しかし、Cgoを介して作成された外部スレッドは、Goランタイムのスケジューラによって直接管理されていないため、シグナル受信時にこれらのコンテキストが利用できない場合があります。
この問題はGoのIssue #3250として報告されており、外部スレッドで発生したシグナルをGoプログラムが適切に処理し、予期せぬ終了を避ける必要がありました。特に、os/signal
パッケージを通じてGo側でシグナルを捕捉したい場合、外部スレッドからのシグナルもGoのチャネルに配信されるべきです。
前提知識の解説
Goランタイムとスケジューラ
Goランタイムは、Goプログラムの実行を管理するシステムです。その中核には、Goの並行処理モデルを支えるスケジューラがあります。スケジューラは、OSスレッド(M: Machine)上でゴルーチン(G: Goroutine)を実行し、必要に応じてゴルーチンをMに割り当てたり、Mから解放したりします。
- Goroutine (G): Goの軽量な実行単位です。数千から数百万のゴルーチンを同時に実行できます。
- Machine (M): OSスレッドを表します。Goランタイムは、OSスレッドを抽象化してMとして扱います。
- Processor (P): ゴルーチンを実行するための論理プロセッサです。MはPにアタッチされ、Pは実行可能なゴルーチンをキューから取得してM上で実行します。
シグナルとシグナルハンドリング
シグナルは、オペレーティングシステムがプロセスに非同期イベントを通知するメカニズムです。例えば、SIGINT
(Ctrl+C)、SIGSEGV
(セグメンテーション違反)、SIGCHLD
(子プロセスの状態変化)などがあります。
- シグナルハンドラ: シグナルを受信した際に実行される特定の関数です。
- 同期シグナル: プログラムの特定の操作(例: 無効なメモリアクセス)によって直接引き起こされるシグナル(
SIGSEGV
,SIGFPE
など)。 - 非同期シグナル: プログラムの実行とは独立して発生するシグナル(
SIGINT
,SIGTERM
,SIGCHLD
など)。
Goランタイムは、起動時に独自のシグナルハンドラをインストールします。これにより、GoプログラムはOSシグナルを捕捉し、適切に処理することができます。os/signal
パッケージを使用することで、Goのコード内でシグナルをチャネルで受け取ることができます。
Cgo
Cgoは、GoプログラムからC言語のコードを呼び出したり、C言語のコードからGoの関数を呼び出したりするためのGoの機能です。Cgoを使用すると、Goランタイムが直接管理しないOSスレッドが生成されることがあります。これらのスレッドは、Goのスケジューラから見ると「外部スレッド」となります。
runtime.badsignal
Goランタイム内部の関数で、Goランタイムが予期しない、または適切に処理できないシグナルを受信した際に呼び出されるものでした。特に、シグナルを受信したOSスレッドに有効なGoのm
やg
のコンテキストがない場合に、この関数が呼び出され、通常はエラーメッセージを出力してプログラムを終了させていました。
runtime.sigsend
Goランタイム内部の関数で、Goのos/signal
パッケージを通じて登録されたシグナルチャネルにシグナルを送信する役割を担います。これにより、Goのユーザーコードがシグナルを非同期に受け取ることができます。
runtime.cgocallback
Cgoの重要なメカニズムの一つで、CコードからGoの関数を呼び出す(コールバックする)際に使用されます。Cの実行コンテキストからGoの実行コンテキストへの切り替え、スタックの切り替え、引数の変換など、複雑な処理を担います。これにより、CとGoの間で安全な関数呼び出しが可能になります。
技術的詳細
このコミットの核心は、外部スレッドでシグナルを受信した際に、従来の即時終了ではなく、Goランタイムのシグナル処理メカニズムにシグナルを安全に引き渡す方法を導入した点にあります。
従来のGoランタイムでは、シグナルがsigtramp
(シグナルハンドラのエントリポイントとなるアセンブリコード)に到達し、そのスレッドがGoランタイムによって管理されていない(m
がnil
である)場合、直接runtime.badsignal
を呼び出してプログラムを終了させていました。これは、Goのコンテキストがない状態でシグナルを処理しようとすると、ランタイムの内部状態が破壊される可能性があるためです。
このコミットでは、この挙動を変更し、runtime.badsignal
の役割を再定義しました。
runtime.badsignal
の変更: 以前は各OS固有のCファイル(os_darwin.c
,os_freebsd.c
,os_linux.c
など)に実装されていたruntime.badsignal
関数が削除されました。これらの関数は、単にエラーメッセージを出力してプログラムを終了させるだけでした。sigqueue.goc
へのruntime.badsignal
の移動と再実装: 新しいruntime.badsignal
関数はsrc/pkg/runtime/sigqueue.goc
に移動され、その実装が大きく変更されました。この新しいruntime.badsignal
は、シグナル番号を引数として受け取り、runtime.cgocallback
を呼び出すようになりました。runtime.cgocallback
とruntime.sigsend
の連携:runtime.badsignal
内でruntime.cgocallback((void (*)(void))runtime.sigsend, &sig, sizeof(sig));
という呼び出しが行われます。これは、外部スレッドで受信したシグナルを、runtime.cgocallback
を介してGoランタイムのコンテキストに安全に引き渡し、最終的にruntime.sigsend
関数に処理させることを意味します。runtime.cgocallback
は、CのスタックからGoのスタックへの切り替えや、Goのスケジューラへの通知など、CとGoの間のコンテキストスイッチを安全に行います。runtime.sigsend
は、Goのos/signal
パッケージを通じて登録されたシグナルチャネルにシグナルを配信する役割を担います。
- アセンブリコードの変更: 各アーキテクチャおよびOS固有のアセンブリファイル(
sys_darwin_386.s
,sys_linux_amd64.s
など)内のruntime.sigtramp
(シグナルハンドラのエントリポイント)が変更されました。m
がnil
の場合に、以前は直接runtime.badsignal
を呼び出していた箇所が、新しいruntime.badsignal
を呼び出すように修正されました。これにより、外部スレッドでシグナルが発生した場合でも、Goランタイムの新しいシグナル処理パスが利用されるようになります。 - Plan 9の特殊な扱い:
os_plan9.c
では、runtime.badsignal
がruntime.badsignal2
にリネームされ、引き続きプログラムを終了させる役割を担っています。これはPlan 9のシグナル処理モデルが他のUnix系OSと異なるためと考えられます。
この変更により、外部スレッドでシグナルが発生しても、Goランタイムは即座に終了するのではなく、そのシグナルをGoのos/signal
パッケージを通じてユーザーコードに通知できるようになります。これにより、Cgoを使用するGoプログラムの堅牢性が向上し、外部ライブラリからのシグナルに対してもより柔軟な対応が可能になります。
コアとなるコードの変更箇所
このコミットで変更された主要なファイルとコードの概要は以下の通りです。
-
misc/cgo/test/cgo_test.go
:func Test3250(t *testing.T) { test3250(t) }
が追加され、この変更に関連するテストケースが実行されるようになりました。
-
misc/cgo/test/issue3250.go
(新規ファイル):- GoとCgoを組み合わせたテストケースが追加されました。
- Cコードで複数のpthreadを生成し、各スレッドが自身に
SIGCHLD
シグナルを繰り返し送信するロジックが含まれています。 - Go側では
os/signal
パッケージを使用してSIGCHLD
を捕捉し、一定数以上のシグナルが捕捉されることを検証します。これにより、外部スレッドからのシグナルがGoのシグナルハンドラに正しく配信されることを確認します。
-
misc/cgo/test/issue3250w.go
(新規ファイル):- Windows環境向けのダミーテストファイル。
test3250
関数が空の実装になっています。
- Windows環境向けのダミーテストファイル。
-
src/pkg/runtime/os_darwin.c
,os_freebsd.c
,os_linux.c
,os_netbsd.c
,os_openbsd.c
:- これらのファイルから、従来の
runtime.badsignal
関数の実装が削除されました。この関数は、シグナルを受信したスレッドがGoによって作成されていない場合に、エラーメッセージを出力してプログラムを終了させるものでした。
- これらのファイルから、従来の
-
src/pkg/runtime/os_plan9.c
:runtime.badsignal
関数がruntime.badsignal2
にリネームされました。Plan 9では引き続きこの関数がプログラムを終了させる役割を担います。
-
src/pkg/runtime/sigqueue.goc
:- 新しい
runtime.badsignal
関数が追加されました。 - この新しい
runtime.badsignal
は、引数としてシグナル番号(uintptr sig
)を受け取ります。 - 内部で
runtime.cgocallback((void (*)(void))runtime.sigsend, &sig, sizeof(sig));
を呼び出しています。これにより、外部スレッドで受信したシグナルがruntime.cgocallback
を介してGoランタイムのruntime.sigsend
に渡され、Goのシグナルチャネルに配信されるようになります。 #include "cgocall.h"
が追加されています。
- 新しい
-
src/pkg/runtime/sys_darwin_386.s
,sys_darwin_amd64.s
,sys_freebsd_386.s
,sys_freebsd_amd64.s
,sys_freebsd_arm.s
,sys_linux_386.s
,sys_linux_amd64.s
,sys_linux_arm.s
,sys_netbsd_386.s
,sys_netbsd_amd64.s
,sys_netbsd_arm.s
,sys_openbsd_386.s
,sys_openbsd_amd64.s
:- これらのアセンブリファイル内の
runtime.sigtramp
関数が変更されました。 m
(GoランタイムのMachine構造体、現在のOSスレッドに対応)が存在しない(m
がnil
である)場合に、以前は直接runtime.badsignal
を呼び出していましたが、新しいruntime.badsignal
を呼び出すように修正されました。具体的には、CALL runtime·badsignal(SB)
の前にMOVL $runtime·badsignal(SB), AX
(またはMOVQ
、MOVW
などアーキテクチャに応じた命令)でruntime.badsignal
のアドレスをレジスタにロードし、そのレジスタを介して呼び出す形式に変更されています。- 一部のOS/アーキテクチャでは、
JMP sigtramp_ret
が追加され、runtime.badsignal
が呼び出された後にシグナルハンドラから適切に復帰するパスが確保されています。
- これらのアセンブリファイル内の
-
src/pkg/runtime/sys_plan9_386.s
,sys_plan9_amd64.s
:runtime.badsignal(SB)
の呼び出しがruntime.badsignal2(SB)
に変更されました。
コアとなるコードの解説
このコミットの最も重要な変更は、runtime.badsignal
関数の役割と実装の変更、そしてそれに関連するアセンブリコードの修正です。
変更前:
Goランタイムが管理していない外部スレッドでシグナルを受信した場合、sigtramp
(シグナルハンドラのアセンブリエントリポイント)内でm
がnil
であることを検出し、直接runtime.badsignal
を呼び出していました。このruntime.badsignal
は、各OS固有のCファイル(例: src/pkg/runtime/os_linux.c
)に実装されており、単にエラーメッセージを標準エラー出力に書き出し、runtime.exit(1)
を呼び出してプログラムを強制終了させていました。これは、Goランタイムのコンテキストがない状態でシグナルを処理しようとすると、ランタイムの内部状態が破壊される可能性があるため、安全策としてプログラムを終了させていたものです。
// src/pkg/runtime/os_linux.c (変更前の抜粋)
void
runtime·badsignal(int32 sig)
{
// ... エラーメッセージの出力 ...
runtime·exit(1); // プログラムを終了
}
変更後:
runtime.badsignal
の再定義: 各OS固有のCファイルからruntime.badsignal
の実装が削除されました。代わりに、src/pkg/runtime/sigqueue.goc
に新しいruntime.badsignal
が定義されました。
この新しい// src/pkg/runtime/sigqueue.goc (変更後の抜粋) // This runs on a foreign stack, without an m or a g. No stack split. #pragma textflag 7 void runtime·badsignal(uintptr sig) { runtime·cgocallback((void (*)(void))runtime·sigsend, &sig, sizeof(sig)); }
runtime.badsignal
は、シグナル番号を引数として受け取り、runtime.cgocallback
を呼び出します。runtime.cgocallback
は、CのスタックからGoのスタックへの安全なコンテキストスイッチを行い、指定されたGo関数(ここではruntime.sigsend
)を実行します。runtime.sigsend
への委譲:runtime.sigsend
は、Goのos/signal
パッケージを通じて登録されたシグナルチャネルにシグナルを配信するGoランタイムの内部関数です。これにより、外部スレッドで受信したシグナルが、Goランタイムの通常のシグナル処理フローに乗せられ、Goのユーザーコードがos/signal
パッケージを介してそのシグナルを捕捉できるようになります。- アセンブリコードの修正: 各OS/アーキテクチャのアセンブリファイル(例:
src/pkg/runtime/sys_linux_amd64.s
)にあるruntime.sigtramp
は、シグナルを受信した際に最初に実行されるコードです。このコードは、現在のスレッドがGoランタイムによって管理されているかどうか(m
がnil
でないか)をチェックします。 変更前:m
がnil
の場合、直接CALL runtime·badsignal(SB)
を実行していました。 変更後:m
がnil
の場合、MOVL $runtime·badsignal(SB), AX
(またはMOVQ
など)で新しいruntime.badsignal
のアドレスをレジスタにロードし、そのレジスタを介してCALL AX
を実行するように変更されました。これにより、新しいruntime.badsignal
(sigqueue.goc
に定義されたもの)が呼び出され、シグナルがruntime.cgocallback
とruntime.sigsend
を通じてGoランタイムに安全に引き渡されるようになります。
この一連の変更により、Goランタイムは外部スレッドで発生したシグナルを即座に終了させるのではなく、Goのシグナル処理メカニズムに統合し、os/signal
パッケージを通じてユーザーコードがシグナルを捕捉・処理できるようになりました。これは、Cgoを使用するGoアプリケーションの堅牢性と柔軟性を大幅に向上させる重要な改善です。
関連リンク
- Go Issue #3250: https://github.com/golang/go/issues/3250
- Go CL 10757044: https://golang.org/cl/10757044
参考にした情報源リンク
- https://go.dev/src/runtime/signal_unix.go
- https://go.dev/src/runtime/signal_sighandler.go
- https://go.dev/src/runtime/cgocall.go
- https://go.dev/src/runtime/cgo/callbacks.go
- https://go.dev/doc/articles/cgo
- https://go.dev/blog/cgo
- https://stackoverflow.com/questions/tagged/go-cgo
- https://stackoverflow.com/questions/6007900/what-is-the-purpose-of-sigtramp
- https://go.dev/src/runtime/proc.go (Go runtime
m
andg
structs) - https://go.dev/src/runtime/runtime.go
- https://go.dev/src/os/signal/signal.go
- https://pkg.go.dev/os/signal
- https://go.dev/src/runtime/debug.go (GOTRACEBACK)
- https://github.com/golang/go/blob/master/src/runtime/signal_unix.go
- https://github.com/golang/go/blob/master/src/runtime/signal_sighandler.go
- https://github.com/golang/go/blob/master/src/runtime/cgocall.go
- https://github.com/golang/go/blob/master/src/runtime/cgo/callbacks.go
- https://thegreenplace.net/2019/go-and-cgo-part-2-callbacks/
- https://go.dev/src/runtime/sys_linux_amd64.s (Example of
sigtramp
assembly) - https://go.dev/src/runtime/sigqueue.goc
- https://go.dev/src/runtime/os_linux.c
- https://go.dev/src/runtime/os_darwin.c
- https://go.dev/src/runtime/os_freebsd.c
- https://go.dev/src/runtime/os_netbsd.c
- https://go.dev/src/runtime/os_openbsd.c
- https://go.dev/src/runtime/os_plan9.c
- https://go.dev/src/misc/cgo/test/issue3250.go
- https://go.dev/src/misc/cgo/test/issue3250w.go
- https://go.dev/src/misc/cgo/test/cgo_test.go
- https://go.dev/src/runtime/sys_darwin_386.s
- https://go.dev/src/runtime/sys_darwin_amd64.s
- https://go.dev/src/runtime/sys_freebsd_386.s
- https://go.dev/src/runtime/sys_freebsd_amd64.s
- https://go.dev/src/runtime/sys_freebsd_arm.s
- https://go.dev/src/runtime/sys_linux_386.s
- https://go.dev/src/runtime/sys_linux_arm.s
- https://go.dev/src/runtime/sys_netbsd_386.s
- https://go.dev/src/runtime/sys_netbsd_amd64.s
- https://go.dev/src/runtime/sys_netbsd_arm.s
- https://go.dev/src/runtime/sys_openbsd_386.s
- https://go.dev/src/runtime/sys_openbsd_amd64.s
- https://go.dev/src/runtime/sys_plan9_386.s
- https://go.dev/src/runtime/sys_plan9_amd64.s