[インデックス 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