[インデックス 18111] ファイルの概要
このコミットは、GoランタイムのCGO(C言語との相互運用)部分における、pthread_create
呼び出し前のシグナルマスク設定に関する修正です。特にFreeBSD/ARMおよびLinux/ARMアーキテクチャにおいて、この重要なステップが欠落していた問題を解決します。
コミット
commit 699dbb60b7274a525f53797aa1d145cc762cf953
Author: Ian Lance Taylor <iant@golang.org>
Date: Tue Dec 24 08:08:15 2013 -0800
runtime/cgo: always set signal mask before calling pthread_create
This was done correctly for most targets but was missing from
FreeBSD/ARM and Linux/ARM.
R=golang-codereviews, dave
CC=golang-codereviews
https://golang.org/cl/45180043
GitHub上でのコミットページへのリンク
https://github.com/golang/go/commit/699dbb60b7274a525f53797aa1d145cc762cf953
元コミット内容
runtime/cgo: always set signal mask before calling pthread_create
このコミットは、pthread_create
を呼び出す前に常にシグナルマスクを設定するように修正します。これはほとんどのターゲットで正しく行われていましたが、FreeBSD/ARMおよびLinux/ARMでは欠落していました。
変更の背景
GoプログラムがC言語のコードを呼び出す際にCGO(C-Go interoperability)を使用します。CGOは、GoランタイムとCランタイムの間でスレッドやメモリ管理を協調させる必要があります。特に、CGOが新しいOSスレッド(通常はpthread_create
を使用して作成される)を生成する際、そのスレッドが親スレッドから予期せぬシグナルマスクを継承する可能性があります。
シグナルマスクは、プロセスまたはスレッドがブロックする(無視する)シグナルのセットを定義します。新しいスレッドが作成される際、通常は親スレッドのシグナルマスクを継承します。しかし、Goランタイムは独自のシグナルハンドリングメカニズムを持っており、特定のシグナル(例えば、Goのスケジューラが使用するシグナルや、デバッグ目的のシグナル)を適切に処理する必要があります。もしCGOによって作成されたスレッドが、Goランタイムが期待しないシグナルマスクを継承してしまうと、シグナルがブロックされてしまい、Goランタイムの動作に悪影響を及ぼしたり、デッドロックやクラッシュの原因となる可能性がありました。
このコミット以前は、Goランタイムはほとんどのプラットフォームでpthread_create
を呼び出す前にシグナルマスクを適切に設定していましたが、FreeBSD/ARMおよびLinux/ARMアーキテクチャではこの処理が欠落していました。この欠落により、これらのプラットフォームでCGOを使用する際に、シグナル関連の未定義動作やバグが発生する可能性がありました。この修正は、これらの特定のアーキテクチャにおけるシグナルハンドリングの一貫性と堅牢性を確保することを目的としています。
前提知識の解説
1. CGO (C-Go interoperability)
Go言語は、C言語で書かれたライブラリやコードを直接呼び出すためのメカニズムを提供します。これがCGOです。GoプログラムとCライブラリが連携する際、GoランタイムとCランタイム(通常はglibcなどの標準Cライブラリ)の間で、スレッド管理、メモリ管理、シグナルハンドリングなどの低レベルな側面を協調させる必要があります。
2. POSIXスレッド (pthreads)
pthread_create
は、POSIXスレッド(pthreads)ライブラリの一部であり、新しいスレッドを作成するための標準的な関数です。Goランタイムは、CGOを介してCコードを呼び出す際に、必要に応じてOSレベルのスレッド(pthreads)を作成することがあります。
3. シグナルとシグナルマスク
- シグナル: オペレーティングシステムがプロセスやスレッドに非同期的に通知するイベントです。例えば、Ctrl+Cによる割り込み(SIGINT)、不正なメモリアクセス(SIGSEGV)、子プロセスの終了(SIGCHLD)などがあります。
- シグナルマスク: 各スレッドは、現在ブロックしている(つまり、受信してもすぐに処理せず保留する)シグナルのセットを持つことができます。このセットをシグナルマスクと呼びます。シグナルがブロックされている間は、そのシグナルは保留状態となり、スレッドのシグナルマスクからそのシグナルが削除されるまで配信されません。
sigset_t
: シグナルのセットを表すデータ型です。SIGFILLSET
:sigset_t
型の変数に、すべての標準シグナルを含むように設定するマクロまたは関数です。pthread_sigmask
: スレッドのシグナルマスクを検査または変更するための関数です。SIG_SETMASK
: シグナルマスクを完全に置き換えます。SIG_BLOCK
: 現在のシグナルマスクに指定されたシグナルを追加します。SIG_UNBLOCK
: 現在のシグナルマスクから指定されたシグナルを削除します。
4. スレッド作成時のシグナルマスクの継承
新しいスレッドがpthread_create
によって作成される際、そのスレッドはデフォルトで親スレッドのシグナルマスクを継承します。これは、親スレッドがブロックしているシグナルは子スレッドでもブロックされることを意味します。
5. Goランタイムのシグナルハンドリング
Goランタイムは、ガベージコレクション、スケジューリング、プロファイリングなどの内部処理のために、特定のシグナルを独自に利用します。例えば、Goのスケジューラは、スレッドの実行を一時停止したり再開したりするためにシグナルを使用することがあります。そのため、Goランタイムが管理するスレッドが、これらの内部シグナルをブロックしてしまうと、ランタイムの正常な動作が妨げられる可能性があります。
技術的詳細
このコミットの核心は、pthread_create
を呼び出す直前に、新しく作成されるスレッドのシグナルマスクを一時的に変更し、その後元の状態に戻すというパターンを適用することです。
具体的には、以下のステップが実行されます。
- 現在のシグナルマスクの保存:
pthread_sigmask(SIG_SETMASK, &ign, &oset)
の呼び出しにおいて、oset
引数に現在のスレッド(pthread_create
を呼び出すスレッド)のシグナルマスクが保存されます。これは、後で元のシグナルマスクを復元するために必要です。 - すべてのシグナルのブロック:
SIGFILLSET(ign)
またはsigfillset(&ign)
によって、ign
というsigset_t
型の変数にすべての可能なシグナルが設定されます。その後、pthread_sigmask(SIG_SETMASK, &ign, &oset)
を呼び出すことで、pthread_create
を呼び出すスレッドのシグナルマスクが一時的にすべてのシグナルをブロックするように設定されます。これにより、pthread_create
によって新しく作成されるスレッドも、この「すべてのシグナルをブロックする」シグナルマスクを継承します。 - スレッドの作成:
pthread_create(&p, &attr, threadentry, ts)
が呼び出され、新しいOSスレッドが作成されます。この新しいスレッドは、親スレッドから「すべてのシグナルをブロックする」シグナルマスクを継承します。 - 元のシグナルマスクの復元:
pthread_sigmask(SIG_SETMASK, &oset, nil)
を呼び出すことで、pthread_create
を呼び出したスレッドのシグナルマスクが、ステップ1で保存しておいた元の状態(oset
に格納されていた状態)に復元されます。
なぜこのような手順が必要なのでしょうか?
Goランタイムは、CGOによって作成されたスレッドが、Goランタイムが期待するシグナルマスクを持つことを保証したいと考えています。特に、Goランタイムが内部的に使用するシグナル(例えば、Goスケジューラがスレッドをプリエンプトするために使用するシグナル)が、CGOスレッドによってブロックされないようにする必要があります。
この修正は、pthread_create
が新しいスレッドを作成する際に、親スレッドのシグナルマスクを継承するというPOSIXの動作を利用しています。pthread_create
を呼び出す直前に親スレッドのシグナルマスクを「すべてのシグナルをブロックする」状態に設定することで、新しく作成されるCGOスレッドも一時的にすべてのシグナルをブロックする状態になります。その後、Goランタイムは必要に応じて、この新しく作成されたスレッドのシグナルマスクを、Goランタイムが適切に動作するために必要な状態に調整することができます。
このアプローチにより、GoランタイムはCGOスレッドのシグナルハンドリングをより細かく制御できるようになり、特にFreeBSD/ARMおよびLinux/ARMのような特定のアーキテクチャで発生していたシグナル関連のバグや不安定性を解消します。
コアとなるコードの変更箇所
変更は主に2つのファイルで行われています。
src/pkg/runtime/cgo/gcc_freebsd_arm.c
src/pkg/runtime/cgo/gcc_linux_arm.c
それぞれのファイルで、_cgo_sys_thread_start
関数内に以下のコードが追加されています。
--- a/src/pkg/runtime/cgo/gcc_freebsd_arm.c
+++ b/src/pkg/runtime/cgo/gcc_freebsd_arm.c
@@ -39,10 +39,14 @@ void
_cgo_sys_thread_start(ThreadStart *ts)
{
pthread_attr_t attr;
+\tsigset_t ign, oset;
pthread_t p;
size_t size;
int err;
+\tSIGFILLSET(ign);
+\tpthread_sigmask(SIG_SETMASK, &ign, &oset);
+\
// Not sure why the memset is necessary here,
// but without it, we get a bogus stack size
// out of pthread_attr_getstacksize. C'est la Linux.
@@ -52,6 +56,9 @@ _cgo_sys_thread_start(ThreadStart *ts)
pthread_attr_getstacksize(&attr, &size);
ts->g->stackguard = size;
err = pthread_create(&p, &attr, threadentry, ts);
+\
+\tpthread_sigmask(SIG_SETMASK, &oset, nil);
+\
if (err != 0) {
fprintf(stderr, "runtime/cgo: pthread_create failed: %s\\n", strerror(err));
abort();
gcc_linux_arm.c
も同様の変更です。
コアとなるコードの解説
追加されたコードは以下の通りです。
sigset_t ign, oset; // シグナルセットを格納するための変数を宣言
SIGFILLSET(ign); // ign にすべてのシグナルを設定(すべてのシグナルをブロック対象とする)
pthread_sigmask(SIG_SETMASK, &ign, &oset); // 現在のスレッドのシグナルマスクを ign で置き換え、元のマスクを oset に保存
// ... pthread_create の呼び出し ...
err = pthread_create(&p, &attr, threadentry, ts);
pthread_sigmask(SIG_SETMASK, &oset, nil); // 現在のスレッドのシグナルマスクを oset に保存しておいた元の状態に戻す
sigset_t ign, oset;
:ign
は新しいシグナルマスク(すべてのシグナルをブロックするマスク)を保持し、oset
はpthread_sigmask
呼び出し前の元のシグナルマスクを保存するために使用されます。SIGFILLSET(ign);
(FreeBSD/ARM) またはsigfillset(&ign);
(Linux/ARM):ign
変数にすべての標準シグナルを含めるように設定します。これにより、このシグナルセットが適用されると、すべてのシグナルがブロックされるようになります。pthread_sigmask(SIG_SETMASK, &ign, &oset);
:SIG_SETMASK
: シグナルマスクの操作モードを指定します。ここでは、現在のシグナルマスクを完全に置き換えることを意味します。&ign
: 新しいシグナルマスクとしてign
(すべてのシグナルをブロックするセット)を適用します。&oset
: この関数が呼び出される前の、現在のスレッドのシグナルマスクがoset
に保存されます。 この行の実行後、pthread_create
を呼び出すスレッドは、すべてのシグナルをブロックする状態になります。
err = pthread_create(&p, &attr, threadentry, ts);
: 新しいスレッドが作成されます。この新しいスレッドは、親スレッド(pthread_create
を呼び出したスレッド)の現在のシグナルマスク(つまり、すべてのシグナルをブロックするマスク)を継承します。pthread_sigmask(SIG_SETMASK, &oset, nil);
:pthread_create
の呼び出し後、pthread_create
を呼び出したスレッドのシグナルマスクを、oset
に保存しておいた元の状態に戻します。nil
は、この呼び出しで古いシグナルマスクを保存する必要がないことを示します。
この一連の操作により、pthread_create
によって生成されるCGOスレッドが、Goランタイムが期待するシグナルマスクを確実に継承するように制御されます。これにより、Goランタイムのシグナルハンドリングが正しく機能し、CGOとGoランタイム間の相互作用における潜在的な問題を回避します。
関連リンク
- Go言語のCGOに関する公式ドキュメント: https://pkg.go.dev/cmd/cgo
- POSIX
pthread_create
manページ (例: Linux): https://man7.org/linux/man-pages/man3/pthread_create.3.html - POSIX
pthread_sigmask
manページ (例: Linux): https://man7.org/linux/man-pages/man3/pthread_sigmask.3.html
参考にした情報源リンク
- Goのコミットメッセージと差分
- POSIXスレッドおよびシグナルハンドリングに関する一般的な知識
- Goランタイムの内部動作に関する一般的な理解
pthread_create
とシグナルマスクの継承に関する情報 (例: Stack Overflow, Linux man pages)- Goのコードレビューシステム (Gerrit) のCL (Change-list) リンク:
https://golang.org/cl/45180043
(現在はGoのGerritインスタンスにリダイレクトされます)- https://go-review.googlesource.com/c/go/+/45180043
- このCLページで、コミットに関する詳細な議論や背景情報が提供されている場合があります。