[インデックス 13747] ファイルの概要
このコミットは、Goランタイムが混合言語アプリケーション(GoとC++など)でSIGPROF
シグナルを処理する際の安定性の問題を解決するためのものです。特に、Goランタイムが管理していない非GoスレッドにSIGPROF
が配送された場合に発生するクラッシュを防ぐための修正が含まれています。
コミット
- コミットハッシュ:
532dee3842298ad242355fd210efbd658cc93196
- 作者: Alan Donovan adonovan@google.com
- 日付: Tue Sep 4 14:40:49 2012 -0400
GitHub上でのコミットページへのリンク
https://github.com/golang/go/commit/532dee3842298ad242355fd210efbd658cc93196
元コミット内容
runtime: discard SIGPROF delivered to non-Go threads.
Signal handlers are global resources but many language
environments (Go, C++ at Google, etc) assume they have sole
ownership of a particular handler. Signal handlers in
mixed-language applications must therefore be robust against
unexpected delivery of certain signals, such as SIGPROF.
The default Go signal handler runtime·sigtramp assumes that it
will never be called on a non-Go thread, but this assumption
is violated by when linking in C++ code that spawns threads.
Specifically, the handler asserts the thread has an associated
"m" (Go scheduler).
This CL is a very simple workaround: discard SIGPROF delivered to non-Go threads. runtime.badsignal(int32) now receives the signal number; if it returns without panicking (e.g. sig==SIGPROF) the signal is discarded.
I don't think there is any really satisfactory solution to the
problem of signal-based profiling in a mixed-language
application. It's not only the issue of handler clobbering,
but also that a C++ SIGPROF handler called in a Go thread
can't unwind the Go stack (and vice versa). The best we can
hope for is not crashing.
Note:
- I've ported this to all POSIX platforms, except ARM-linux which already ignores unexpected signals on m-less threads.
- I've avoided tail-calling runtime.badsignal because AFAICT the 6a/6l don't support it.
- I've avoided hoisting 'push sig' (common to both function calls) because it makes the code harder to read.
- Fixed an (apparently incorrect?) docstring.
R=iant, rsc, minux.ma
CC=golang-dev
https://golang.org/cl/6498057
変更の背景
シグナルハンドラはグローバルなリソースであり、GoやC++のような多くの言語環境は、特定のハンドラを単独で所有していると仮定しています。このため、混合言語アプリケーションでは、SIGPROF
のような特定のシグナルの予期せぬ配送に対して、シグナルハンドラが堅牢である必要があります。
Goのデフォルトのシグナルハンドラであるruntime·sigtramp
は、非Goスレッドでは呼び出されないと仮定していました。しかし、C++コードがスレッドを生成してリンクされる場合、この仮定が破られます。具体的には、ハンドラはスレッドが関連する"m"(Goスケジューラ)を持っていることをアサートしますが、非Goスレッドには"m"が存在しないため、クラッシュが発生していました。
この問題は、混合言語環境におけるシグナルベースのプロファイリングの根本的な課題を示しています。ハンドラの上書きだけでなく、Goスレッドで呼び出されたC++のSIGPROF
ハンドラがGoスタックをアンワインドできない(逆もまた然り)という問題も存在します。このコミットは、クラッシュを回避するための実用的な回避策を提供することを目的としています。
前提知識の解説
SIGPROF
シグナル:SIGPROF
は、プロファイリングのために使用されるタイマーシグナルです。通常、CPUプロファイラは定期的にこのシグナルを送信し、プログラムの実行を一時停止させてスタックトレースを収集します。Goランタイムは、CPUプロファイリング機能のためにSIGPROF
を積極的に利用しています。- Goランタイムの
m
とg
:g
(goroutine): Goの軽量スレッドであり、Goプログラムの並行実行の単位です。m
(machine): オペレーティングシステムのスレッドを表します。Goランタイムは、m
を介してOSスレッドを管理し、その上でg
をスケジュールして実行します。各m
は、現在実行中のg
や、シグナルハンドラなどのOSレベルのリソースへのポインタを持っています。
runtime·sigtramp
: Goランタイム内のアセンブリレベルの関数で、Goプログラムに配送されるすべてのシグナルの初期エントリポイントとして機能します。その主な役割は、C ABIからGo ABIへの移行を行い、必要なGo実行コンテキストを設定し、その後、シグナルをより高レベルのGo関数(runtime/signal_unix.go
にあるsigtrampgo
など)にディスパッチすることです。- 混合言語環境におけるシグナルハンドリングの課題: シグナルハンドラはプロセス全体で共有されるグローバルなリソースです。GoとC++のような異なる言語ランタイムがそれぞれ独自のシグナルハンドラを設定しようとすると、競合が発生し、予期せぬ動作やクラッシュにつながる可能性があります。特に、Goランタイムが管理していないスレッドにシグナルが配送された場合、Goのシグナルハンドラが期待する
m
やg
のコンテキストが存在しないため、問題が生じます。
技術的詳細
このコミットの主要な目的は、Goランタイムが管理していない非GoスレッドにSIGPROF
シグナルが配送された場合に発生するクラッシュを回避することです。これは、runtime·sigtramp
がm
(Goスケジューラが管理するOSスレッド)が存在しないスレッドで呼び出された場合に、SIGPROF
を破棄するというシンプルな回避策によって実現されます。
具体的な変更点は以下の通りです。
-
runtime·sigtramp
の変更:- 各プラットフォーム(Darwin, FreeBSD, Linux, NetBSD, OpenBSDの386およびamd64アーキテクチャ)のアセンブリコード(
sys_*.s
ファイル)において、runtime·sigtramp
関数内でm
が存在するかどうかをチェックするロジックが変更されました。 - 以前は
m
が存在しない場合にruntime·badsignal
を呼び出してパニックさせていましたが、今回の変更では、m
が存在しない場合にシグナル番号をruntime·badsignal
に渡し、runtime·badsignal
がSIGPROF
であれば単にリターンするように修正されました。これにより、SIGPROF
が非Goスレッドに配送されてもクラッシュせずに無視されるようになります。 - アセンブリコードでは、シグナル番号をスタックにプッシュし、
runtime·badsignal
を呼び出した後、RET
命令で関数から戻るように変更されています。
- 各プラットフォーム(Darwin, FreeBSD, Linux, NetBSD, OpenBSDの386およびamd64アーキテクチャ)のアセンブリコード(
-
runtime·badsignal
の変更:runtime·badsignal
関数(thread_*.c
ファイルに定義)のシグネチャが変更され、int32 sig
という引数を受け取るようになりました。- 関数内で、受け取ったシグナルが
SIGPROF
であるかどうかをチェックする条件分岐が追加されました。 - もしシグナルが
SIGPROF
であれば、関数は何もせずにreturn
します。これにより、SIGPROF
は実質的に無視されます。 SIGPROF
以外のシグナルが非Goスレッドに配送された場合は、以前と同様にエラーメッセージを標準エラー出力に書き込み、runtime·exit(1)
を呼び出してプログラムを終了させます。
この修正は、ARM-Linuxプラットフォームを除くすべてのPOSIXプラットフォームに移植されました。ARM-Linuxは既にm
を持たないスレッドでの予期せぬシグナルを無視するようになっているため、この変更は適用されていません。
コアとなるコードの変更箇所
このコミットでは、Goランタイムのシグナルハンドリングに関連する複数のアセンブリファイルとCファイルが変更されています。
src/pkg/runtime/signal_linux_amd64.c
:sa_restorer
に関するコメントが追加されました。src/pkg/runtime/sys_darwin_386.s
:runtime·sigtramp
でm
が存在しない場合の処理が変更され、runtime·badsignal
にシグナル番号を渡すようになりました。src/pkg/runtime/sys_darwin_amd64.s
: 同上。src/pkg/runtime/sys_freebsd_386.s
: 同上。src/pkg/runtime/sys_freebsd_amd64.s
: 同上。src/pkg/runtime/sys_linux_386.s
: 同上。src/pkg/runtime/sys_linux_amd64.s
: 同上。src/pkg/runtime/sys_linux_arm.s
:runtime·badsignal
を呼び出す可能性についてのTODOコメントが追加されました。src/pkg/runtime/sys_netbsd_386.s
: 同上。src/pkg/runtime/sys_netbsd_amd64.s
: 同上。src/pkg/runtime/sys_openbsd_386.s
: 同上。src/pkg/runtime/sys_openbsd_amd64.s
: 同上。src/pkg/runtime/thread_darwin.c
:runtime·badsignal
のシグネチャが変更され、SIGPROF
を無視するロジックが追加されました。src/pkg/runtime/thread_freebsd.c
: 同上。src/pkg/runtime/thread_linux.c
: 同上。src/pkg/runtime/thread_netbsd.c
: 同上。src/pkg/runtime/thread_openbsd.c
: 同上。src/pkg/runtime/thread_plan9.c
:runtime·badsignal
のシグネチャが変更されましたが、SIGPROF
を無視するロジックは追加されていません(Plan 9はPOSIXシグナルとは異なるシグナルメカニズムを持つため)。
コアとなるコードの解説
このコミットの核心は、runtime·sigtramp
アセンブリ関数とruntime·badsignal
C関数の連携にあります。
runtime·sigtramp
(例: src/pkg/runtime/sys_darwin_386.s
)
TEXT runtime·sigtramp(SB),7,$40
// check that m exists
MOVL m(CX), BP
CMPL BP, $0
JNE 5(PC) // mが存在すれば通常の処理へジャンプ
MOVL sig+8(FP), BX // シグナル番号をBXレジスタにロード
MOVL BX, 0(SP) // シグナル番号をスタックにプッシュ
CALL runtime·badsignal(SB) // runtime·badsignalを呼び出す
RET // 関数から戻る
このアセンブリコードは、シグナルハンドラが呼び出された際に、現在のスレッドがGoランタイムによって管理されているm
(マシン)を持っているかどうかをチェックします。
MOVL m(CX), BP
:CX
レジスタ(Goのg
とm
へのポインタを含む)からm
の値をBP
レジスタにロードします。CMPL BP, $0
:BP
がゼロ(m
が存在しない)かどうかを比較します。JNE 5(PC)
:m
が存在する場合(BP
がゼロでない場合)は、通常のシグナル処理フローに進みます。MOVL sig+8(FP), BX
とMOVL BX, 0(SP)
:m
が存在しない場合、スタックフレームからシグナル番号を取得し、それをruntime·badsignal
に渡すためにスタックにプッシュします。CALL runtime·badsignal(SB)
:runtime·badsignal
関数を呼び出します。RET
:runtime·badsignal
から戻った後、sigtramp
関数自体もリターンします。
runtime·badsignal
(例: src/pkg/runtime/thread_darwin.c
)
void
runtime·badsignal(int32 sig)
{
if (sig == SIGPROF) {
return; // Ignore SIGPROFs intended for a non-Go thread.
}
runtime·write(2, badsignal, sizeof badsignal - 1);
runtime·exit(1);
}
このCコードは、runtime·sigtramp
から呼び出され、シグナル番号sig
を受け取ります。
if (sig == SIGPROF)
: 渡されたシグナルがSIGPROF
であるかをチェックします。return;
: もしSIGPROF
であれば、関数はすぐにリターンし、シグナルは実質的に無視されます。これにより、非Goスレッドに配送されたSIGPROF
がクラッシュを引き起こすのを防ぎます。runtime·write(2, badsignal, sizeof badsignal - 1);
とruntime·exit(1);
:SIGPROF
以外のシグナルが非Goスレッドに配送された場合は、エラーメッセージを標準エラー出力に書き込み、プログラムを終了させます。これは、Goランタイムが予期しないシグナルを非Goスレッドで受け取った場合の安全策です。
これらの変更により、Goランタイムは、Goが管理していないスレッドにSIGPROF
が配送されても、安全にそれを無視し、アプリケーションの安定性を向上させることができます。
関連リンク
- https://github.com/golang/go/commit/532dee3842298ad242355fd210efbd658cc93196
- https://golang.org/cl/6498057
参考にした情報源リンク
- SIGPROF in Go Runtime - go.dev
- Go Runtime Signal Handling (
sigtramp
) - sobyte.net - Go Mixed C++ Signal Handling - appspot.com
- Go CPU Profiling - felixge.de
- Go Runtime Signal Handling - googlesource.com
- Go Runtime Signal Handling - go.dev
- C++ Signal Handling - stackoverflow.com
- C++ Signal Handling - ibm.com