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

[インデックス 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ランタイムのコンテキスト(mg)に依存しているためです。しかし、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のmgのコンテキストがない場合に、この関数が呼び出され、通常はエラーメッセージを出力してプログラムを終了させていました。

runtime.sigsend

Goランタイム内部の関数で、Goのos/signalパッケージを通じて登録されたシグナルチャネルにシグナルを送信する役割を担います。これにより、Goのユーザーコードがシグナルを非同期に受け取ることができます。

runtime.cgocallback

Cgoの重要なメカニズムの一つで、CコードからGoの関数を呼び出す(コールバックする)際に使用されます。Cの実行コンテキストからGoの実行コンテキストへの切り替え、スタックの切り替え、引数の変換など、複雑な処理を担います。これにより、CとGoの間で安全な関数呼び出しが可能になります。

技術的詳細

このコミットの核心は、外部スレッドでシグナルを受信した際に、従来の即時終了ではなく、Goランタイムのシグナル処理メカニズムにシグナルを安全に引き渡す方法を導入した点にあります。

従来のGoランタイムでは、シグナルがsigtramp(シグナルハンドラのエントリポイントとなるアセンブリコード)に到達し、そのスレッドがGoランタイムによって管理されていない(mnilである)場合、直接runtime.badsignalを呼び出してプログラムを終了させていました。これは、Goのコンテキストがない状態でシグナルを処理しようとすると、ランタイムの内部状態が破壊される可能性があるためです。

このコミットでは、この挙動を変更し、runtime.badsignalの役割を再定義しました。

  1. runtime.badsignalの変更: 以前は各OS固有のCファイル(os_darwin.c, os_freebsd.c, os_linux.cなど)に実装されていたruntime.badsignal関数が削除されました。これらの関数は、単にエラーメッセージを出力してプログラムを終了させるだけでした。
  2. sigqueue.gocへのruntime.badsignalの移動と再実装: 新しいruntime.badsignal関数はsrc/pkg/runtime/sigqueue.gocに移動され、その実装が大きく変更されました。この新しいruntime.badsignalは、シグナル番号を引数として受け取り、runtime.cgocallbackを呼び出すようになりました。
  3. runtime.cgocallbackruntime.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パッケージを通じて登録されたシグナルチャネルにシグナルを配信する役割を担います。
  4. アセンブリコードの変更: 各アーキテクチャおよびOS固有のアセンブリファイル(sys_darwin_386.s, sys_linux_amd64.sなど)内のruntime.sigtramp(シグナルハンドラのエントリポイント)が変更されました。mnilの場合に、以前は直接runtime.badsignalを呼び出していた箇所が、新しいruntime.badsignalを呼び出すように修正されました。これにより、外部スレッドでシグナルが発生した場合でも、Goランタイムの新しいシグナル処理パスが利用されるようになります。
  5. Plan 9の特殊な扱い: os_plan9.cでは、runtime.badsignalruntime.badsignal2にリネームされ、引き続きプログラムを終了させる役割を担っています。これはPlan 9のシグナル処理モデルが他のUnix系OSと異なるためと考えられます。

この変更により、外部スレッドでシグナルが発生しても、Goランタイムは即座に終了するのではなく、そのシグナルをGoのos/signalパッケージを通じてユーザーコードに通知できるようになります。これにより、Cgoを使用するGoプログラムの堅牢性が向上し、外部ライブラリからのシグナルに対してもより柔軟な対応が可能になります。

コアとなるコードの変更箇所

このコミットで変更された主要なファイルとコードの概要は以下の通りです。

  1. misc/cgo/test/cgo_test.go:

    • func Test3250(t *testing.T) { test3250(t) } が追加され、この変更に関連するテストケースが実行されるようになりました。
  2. misc/cgo/test/issue3250.go (新規ファイル):

    • GoとCgoを組み合わせたテストケースが追加されました。
    • Cコードで複数のpthreadを生成し、各スレッドが自身にSIGCHLDシグナルを繰り返し送信するロジックが含まれています。
    • Go側ではos/signalパッケージを使用してSIGCHLDを捕捉し、一定数以上のシグナルが捕捉されることを検証します。これにより、外部スレッドからのシグナルがGoのシグナルハンドラに正しく配信されることを確認します。
  3. misc/cgo/test/issue3250w.go (新規ファイル):

    • Windows環境向けのダミーテストファイル。test3250関数が空の実装になっています。
  4. src/pkg/runtime/os_darwin.c, os_freebsd.c, os_linux.c, os_netbsd.c, os_openbsd.c:

    • これらのファイルから、従来のruntime.badsignal関数の実装が削除されました。この関数は、シグナルを受信したスレッドがGoによって作成されていない場合に、エラーメッセージを出力してプログラムを終了させるものでした。
  5. src/pkg/runtime/os_plan9.c:

    • runtime.badsignal関数がruntime.badsignal2にリネームされました。Plan 9では引き続きこの関数がプログラムを終了させる役割を担います。
  6. 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" が追加されています。
  7. 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スレッドに対応)が存在しない(mnilである)場合に、以前は直接runtime.badsignalを呼び出していましたが、新しいruntime.badsignalを呼び出すように修正されました。具体的には、CALL runtime·badsignal(SB)の前にMOVL $runtime·badsignal(SB), AX(またはMOVQMOVWなどアーキテクチャに応じた命令)でruntime.badsignalのアドレスをレジスタにロードし、そのレジスタを介して呼び出す形式に変更されています。
    • 一部のOS/アーキテクチャでは、JMP sigtramp_retが追加され、runtime.badsignalが呼び出された後にシグナルハンドラから適切に復帰するパスが確保されています。
  8. src/pkg/runtime/sys_plan9_386.s, sys_plan9_amd64.s:

    • runtime.badsignal(SB)の呼び出しがruntime.badsignal2(SB)に変更されました。

コアとなるコードの解説

このコミットの最も重要な変更は、runtime.badsignal関数の役割と実装の変更、そしてそれに関連するアセンブリコードの修正です。

変更前: Goランタイムが管理していない外部スレッドでシグナルを受信した場合、sigtramp(シグナルハンドラのアセンブリエントリポイント)内でmnilであることを検出し、直接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); // プログラムを終了
}

変更後:

  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)を実行します。
  2. runtime.sigsendへの委譲: runtime.sigsendは、Goのos/signalパッケージを通じて登録されたシグナルチャネルにシグナルを配信するGoランタイムの内部関数です。これにより、外部スレッドで受信したシグナルが、Goランタイムの通常のシグナル処理フローに乗せられ、Goのユーザーコードがos/signalパッケージを介してそのシグナルを捕捉できるようになります。
  3. アセンブリコードの修正: 各OS/アーキテクチャのアセンブリファイル(例: src/pkg/runtime/sys_linux_amd64.s)にあるruntime.sigtrampは、シグナルを受信した際に最初に実行されるコードです。このコードは、現在のスレッドがGoランタイムによって管理されているかどうか(mnilでないか)をチェックします。 変更前: mnilの場合、直接CALL runtime·badsignal(SB)を実行していました。 変更後: mnilの場合、MOVL $runtime·badsignal(SB), AX(またはMOVQなど)で新しいruntime.badsignalのアドレスをレジスタにロードし、そのレジスタを介してCALL AXを実行するように変更されました。これにより、新しいruntime.badsignalsigqueue.gocに定義されたもの)が呼び出され、シグナルがruntime.cgocallbackruntime.sigsendを通じてGoランタイムに安全に引き渡されるようになります。

この一連の変更により、Goランタイムは外部スレッドで発生したシグナルを即座に終了させるのではなく、Goのシグナル処理メカニズムに統合し、os/signalパッケージを通じてユーザーコードがシグナルを捕捉・処理できるようになりました。これは、Cgoを使用するGoアプリケーションの堅牢性と柔軟性を大幅に向上させる重要な改善です。

関連リンク

参考にした情報源リンク