[インデックス 15272] ファイルの概要
このコミットは、GoランタイムにおけるLinuxビルドの問題を修正し、特にシグナルマスクの扱いに関連するクラッシュを解決することを目的としています。以前の変更でシグナルマスクを保存しようとした際に発生した不安定性を解消し、nohup
コマンドとの互換性を確保するために、シグナルマスクの保存をやめ、代わりにSIG_IGN
(シグナル無視)のチェックとsigset_zero
(空のシグナルセット)の再導入を行っています。
コミット
commit c7f7bbbf03415e1805e503846627f2e08423c360
Author: Russ Cox <rsc@golang.org>
Date: Fri Feb 15 12:18:33 2013 -0500
runtime: fix build on linux
In addition to the compile failure fixed in signal*.c,
preserving the signal mask led to very strange crashes.
Testing shows that looking for SIG_IGN is all that
matters to get along with nohup, so reintroduce
sigset_zero instead of trying to preserve the signal mask.
TBR=iant
CC=golang-dev
https://golang.org/cl/7323067
GitHub上でのコミットページへのリンク
https://github.com/golang/go/commit/c7f7bbbf03415e1805e503846627f2e08423c360
元コミット内容
このコミットは、GoランタイムがLinux上でビルドされる際の問題、特にsignal*.c
ファイルにおけるコンパイルエラーと、シグナルマスクの保存が引き起こす奇妙なクラッシュを修正します。以前の変更でシグナルマスクを保存する試みが行われましたが、これが不安定性の原因となっていました。テストの結果、nohup
コマンドとの互換性を保つためにはSIG_IGN
(シグナル無視)の状態を確認するだけで十分であることが判明したため、シグナルマスクの保存をやめ、sigset_zero
(空のシグナルセット)を再導入することで問題を解決しています。
変更の背景
Goランタイムは、OSの低レベルな機能と密接に連携して動作します。特にシグナルハンドリングは、プログラムの実行中に発生する非同期イベント(例えば、Ctrl+Cによる中断、セグメンテーション違反など)を適切に処理するために不可欠です。このコミット以前のGoランタイムでは、シグナルマスク(プロセスがブロックするシグナルのセット)を保存しようとする試みがありました。これは、GoルーチンがOSスレッド間で切り替わる際に、各スレッドのシグナルマスクの状態を維持しようとする意図があったと考えられます。
しかし、この「シグナルマスクの保存」というアプローチが、特にLinux環境において「非常に奇妙なクラッシュ」を引き起こす原因となっていました。クラッシュの具体的なメカニズムはコミットメッセージからは明らかではありませんが、シグナルマスクの不適切な復元や、OSのシグナルハンドリング機構との競合などが考えられます。
また、nohup
コマンドとの互換性も重要な考慮事項でした。nohup
は、ユーザーがログアウトしてもプロセスが実行を継続できるようにするために使用されるコマンドで、通常、SIGHUP(ハングアップシグナル)を無視するように設定します。Goプログラムがnohup
経由で実行された際に、ランタイムがSIGHUPの扱いを誤ると、予期せぬ終了や動作不良につながる可能性があります。
このコミットは、これらの問題を解決し、Goランタイムの安定性とnohup
との互換性を向上させることを目的としています。
前提知識の解説
1. シグナル (Signal)
Unix系OSにおけるシグナルは、プロセスに対して非同期にイベントを通知するメカニズムです。例えば、以下のようなシグナルがあります。
- SIGHUP (Hangup): 端末が切断された際にプロセスに送られるシグナル。
nohup
コマンドは、このシグナルを無視するようにプロセスを設定します。 - SIGINT (Interrupt): 通常、Ctrl+Cによってプロセスに送られ、プロセスを中断させます。
- SIGTERM (Terminate): プロセスを終了させるためのシグナル。
- SIGKILL (Kill): プロセスを強制終了させるシグナル。これは捕捉、無視、ブロックできません。
- SIGSEGV (Segmentation Violation): プロセスが不正なメモリアクセスを行った際に発生するシグナル。
プロセスは、特定のシグナルを受信した際に、デフォルトの動作(終了、コアダンプなど)を実行するか、カスタムのシグナルハンドラ関数を呼び出すか、シグナルを無視するか、シグナルをブロックするかを選択できます。
2. シグナルマスク (Signal Mask)
シグナルマスクは、プロセスまたはスレッドが現在ブロックしている(一時的に受信を保留している)シグナルのセットです。シグナルがブロックされている間は、そのシグナルがプロセスに送られても、ハンドラは実行されず、シグナルは保留状態になります。シグナルがブロック解除されると、保留されていたシグナルが配送されます。
sigprocmask
システムコールは、シグナルマスクを操作するために使用されます。
SIG_SETMASK
: 現在のシグナルマスクを指定されたマスクに置き換えます。SIG_BLOCK
: 指定されたシグナルを現在のマスクに追加します。SIG_UNBLOCK
: 指定されたシグナルを現在のマスクから削除します。
3. sigaction
と rt_sigaction
sigaction
: シグナルハンドラを設定するためのシステムコールです。シグナルを受信した際の動作(ハンドラ関数、シグナルマスクの変更など)を詳細に制御できます。rt_sigaction
:sigaction
のリアルタイム版で、より多くのシグナルをサポートし、より詳細な制御が可能です。Linuxカーネルでは、内部的にrt_sigaction
が使用されることが多いです。
4. SIG_IGN
SIG_IGN
は、シグナルハンドラとして設定される特別な値で、対応するシグナルを無視するようにOSに指示します。つまり、そのシグナルがプロセスに送られても、何も起こりません。
5. nohup
コマンド
nohup
コマンドは、プロセスがSIGHUPシグナルを無視するように設定し、ユーザーがログアウトしてもプロセスが実行を継続できるようにします。これは、バックグラウンドで長時間実行されるプロセス(例:Webサーバー、バッチ処理)でよく使用されます。
6. GoランタイムのM, G, P
Goランタイムは、独自のスケジューラを持っており、OSスレッド(M: Machine)、Goルーチン(G: Goroutine)、プロセッサ(P: Processor)という3つの主要なエンティティを管理します。
- M (Machine): OSスレッドを表します。Goルーチンを実行する実際のOSスレッドです。
- G (Goroutine): Goの軽量な並行処理単位です。Goランタイムによってスケジューリングされます。
- P (Processor): Goルーチンを実行するための論理的なプロセッサです。MとGを仲介し、GoルーチンをMに割り当てます。
Goランタイムは、これらのエンティティを効率的に管理することで、多数のGoルーチンを並行して実行します。シグナルハンドリングは、M(OSスレッド)のコンテキストで行われるため、GoランタイムはOSスレッドのシグナルマスクを適切に管理する必要があります。
技術的詳細
このコミットの技術的な核心は、GoランタイムがOSスレッドのシグナルマスクをどのように扱うかという点にあります。以前のアプローチでは、GoランタイムはOSスレッドのシグナルマスクを保存し、必要に応じて復元しようとしていました。これは、struct M
にsigset
フィールドが存在し、runtime·newosproc
(新しいOSスレッドを作成する関数)やruntime·minit
(Mの初期化関数)でそのフィールドを操作していたことから推測できます。
しかし、この「シグナルマスクの保存と復元」という戦略が、特にLinux環境で問題を引き起こしました。コミットメッセージにある「非常に奇妙なクラッシュ」は、シグナルマスクの不整合や、OSのシグナル配送メカニズムとの予期せぬ相互作用に起因する可能性があります。例えば、Goランタイムがシグナルマスクを保存した時点と、それを復元しようとした時点の間で、OSレベルでシグナルハンドリングの状態が変化した場合、問題が発生する可能性があります。
このコミットでは、この問題に対する根本的な解決策として、シグナルマスクの保存をやめることを選択しました。代わりに、以下の2つの主要な変更が行われました。
-
SIG_IGN
のチェックの重視:nohup
コマンドとの互換性を確保するため、SIGHUPシグナルに対してSIG_IGN
が設定されているかどうかを明示的にチェックするロジックが維持されました。これは、runtime·setsig
関数(シグナルハンドラを設定する関数)内で、SIGHUPに対してsigaction
(またはrt_sigaction
)を呼び出し、その結果として得られるsa.sa_handler
(またはsa.k_sa_handler
)がSIG_IGN
であるかどうかを確認することで行われます。もしSIG_IGN
であれば、ランタイムはSIGHUPに対して特別な処理を行わず、OSのデフォルトの無視動作に任せます。 -
sigset_zero
の再導入と利用: シグナルマスクの保存をやめた代わりに、sigset_none
(またはsigset_zero
に相当するもの)という空のシグナルセットが導入され、これが新しいOSスレッドの初期化時やMの初期化時に使用されるようになりました。具体的には、runtime·newosproc
で新しいOSスレッドが作成される際、およびruntime·minit
でMが初期化される際に、以前は保存されたシグナルマスクを復元していた箇所が、sigset_none
(つまり、すべてのシグナルをブロックしない状態)を設定するように変更されました。これにより、OSスレッドはクリーンなシグナルマスク状態で開始され、ランタイムがシグナルマスクの複雑な保存・復元ロジックを管理する必要がなくなります。
この変更は、シグナルマスクの管理を簡素化し、OSのシグナルハンドリング機構との競合を減らすことで、ランタイムの安定性を向上させる効果があります。特に、nohup
のような外部ツールがシグナルハンドリングに影響を与える場合でも、Goランタイムが予期せぬ動作をしないように設計されています。
また、Linux固有のsignal_linux_*.c
ファイルでは、runtime·sigaction
の代わりにruntime·rt_sigaction
が使用されるように変更されています。これは、rt_sigaction
がより新しいシステムコールであり、sigaction
よりも詳細な制御や、より大きなシグナルセットを扱うことができるため、特定のLinux環境でのビルド問題やシグナルハンドリングの正確性を向上させるために行われた可能性があります。sizeof(sa.sa_mask)
を引数として渡すことで、rt_sigaction
がシグナルマスクのサイズを正しく認識できるようにしています。
コアとなるコードの変更箇所
このコミットにおける主要なコード変更は、以下のファイルにまたがっています。
-
src/pkg/runtime/runtime.h
:struct M
からvoid* sigset;
フィールドが削除されました。これは、M(OSスレッド)がシグナルマスクを保存する必要がなくなったことを示します。
--- a/src/pkg/runtime/runtime.h +++ b/src/pkg/runtime/runtime.h @@ -290,7 +290,6 @@ struct M GCStats gcstats; bool tracecall; void* tracepc; - void* sigset; uint32 moreframesize_minalloc;
-
src/pkg/runtime/signal_linux_386.c
,src/pkg/runtime/signal_linux_amd64.c
,src/pkg/runtime/signal_linux_arm.c
:runtime·sigaction
の呼び出しがruntime·rt_sigaction
に変更され、sizeof(sa.sa_mask)
が追加の引数として渡されるようになりました。これは、Linux固有のシグナルハンドリングの修正です。
例 (
signal_linux_386.c
):--- a/src/pkg/runtime/signal_linux_386.c +++ b/src/pkg/runtime/signal_linux_386.c @@ -128,7 +128,7 @@ runtime·setsig(int32 i, void (*fn)(int32, Siginfo*, void*, G*), bool restart) // under nohup and do not set explicit handler. if(i == SIGHUP) { runtime·memclr((byte*)&sa, sizeof sa); - runtime·sigaction(i, nil, &sa); + runtime·rt_sigaction(i, nil, &sa, sizeof(sa.sa_mask)); if(sa.k_sa_handler == SIG_IGN) return; }
-
src/pkg/runtime/thread_darwin.c
,src/pkg/runtime/thread_freebsd.c
,src/pkg/runtime/thread_linux.c
,src/pkg/runtime/thread_netbsd.c
,src/pkg/runtime/thread_openbsd.c
:static Sigset sigset_none;
(またはそれに相当する空のシグナルセット)が導入されました。mp->sigset
へのメモリ割り当てと、以前のシグナルマスクの保存に関するコードが削除されました。runtime·sigprocmask
(またはruntime·rtsigprocmask
)の呼び出しで、以前はm->sigset
を使用していた箇所が、新しく導入された&sigset_none
を使用するように変更されました。これにより、OSスレッドのシグナルマスクが常に空の状態で初期化されるようになります。
例 (
thread_linux.c
):--- a/src/pkg/runtime/thread_linux.c +++ b/src/pkg/runtime/thread_linux.c @@ -13,6 +13,7 @@ int32 runtime·open(uint8*, int32, int32); int32 runtime·close(int32); int32 runtime·read(int32, void*, int32); +static Sigset sigset_none; static Sigset sigset_all = { ~(uint32)0, ~(uint32)0 }; // Linux futex. @@ -148,8 +149,6 @@ runtime·newosproc(M *mp, G *gp, void *stk, void (*fn)(void)) // Disable signals during clone, so that the new thread starts // with signals disabled. It will enable them in minit. runtime·rtsigprocmask(SIG_SETMASK, &sigset_all, &oset, sizeof oset); - mp->sigset = runtime·mal(sizeof(Sigset)); - *(Sigset*)mp->sigset = oset; ret = runtime·clone(flags, stk, mp, gp, fn); runtime·rtsigprocmask(SIG_SETMASK, &oset, nil, sizeof oset); @@ -178,8 +177,7 @@ runtime·minit(void) // Initialize signal handling. m->gsignal = runtime·malg(32*1024); // OS X wants >=8K, Linux >=2K runtime·signalstack((byte*)m->gsignal->stackguard - StackGuard, 32*1024); - if(m->sigset != nil) - runtime·rtsigprocmask(SIG_SETMASK, m->sigset, nil, sizeof *m->sigset); + runtime·rtsigprocmask(SIG_SETMASK, &sigset_none, nil, sizeof(Sigset)); }
コアとなるコードの解説
このコミットの核心は、GoランタイムがOSスレッドのシグナルマスクを管理する方法のパラダイムシフトにあります。
-
struct M
からのsigset
フィールド削除:- 以前は、各OSスレッド(M)が自身のシグナルマスクの状態を
m->sigset
に保存していました。これは、Goルーチンが異なるM間で移動する際に、シグナルマスクの状態を維持しようとする試みだったと考えられます。 - このフィールドの削除は、ランタイムがOSスレッドのシグナルマスクを明示的に保存・復元するというアプローチを完全に放棄したことを意味します。これにより、シグナルマスクの不整合によるクラッシュのリスクが排除されます。
- 以前は、各OSスレッド(M)が自身のシグナルマスクの状態を
-
runtime·rt_sigaction
への移行 (Linux):- Linux固有のシグナルハンドリングコードにおいて、
runtime·sigaction
からruntime·rt_sigaction
への変更は、より現代的で堅牢なシグナルハンドリングAPIへの移行を示唆しています。rt_sigaction
は、より大きなシグナルセットをサポートし、リアルタイムシグナルを扱う能力があるため、GoランタイムがLinuxカーネルとより効率的かつ正確に連携できるようになります。 sizeof(sa.sa_mask)
を引数として渡すことは、rt_sigaction
システムコールがシグナルマスクのサイズを正しく解釈するために必要です。これにより、異なるアーキテクチャやカーネルバージョン間での互換性が向上します。
- Linux固有のシグナルハンドリングコードにおいて、
-
sigset_none
の導入と利用:static Sigset sigset_none;
は、すべてのシグナルをブロックしない(空の)シグナルセットを表します。runtime·newosproc
(新しいOSスレッドの作成)およびruntime·minit
(Mの初期化)において、以前は保存されたシグナルマスクを復元していた箇所が、このsigset_none
を使用するように変更されました。- 具体的には、
runtime·sigprocmask(SIG_SETMASK, &sigset_none, nil);
のような呼び出しが行われます。これは、新しいOSスレッドが起動する際や、Mが初期化される際に、そのスレッドのシグナルマスクが常に「何もブロックしない」状態にリセットされることを意味します。 - この変更により、ランタイムはシグナルマスクの複雑な状態管理から解放され、OSスレッドは常に予測可能なシグナルマスク状態で動作するようになります。これにより、特に
nohup
のような外部ツールがシグナルハンドリングに影響を与える場合でも、Goプログラムが安定して動作することが期待されます。
これらの変更は、Goランタイムがシグナルハンドリングをよりシンプルかつ堅牢な方法で処理するように再設計されたことを示しています。シグナルマスクの保存による複雑さとそれに伴うクラッシュのリスクを排除し、OSのデフォルトのシグナル動作とSIG_IGN
のチェックに依存することで、ランタイムの安定性と互換性が向上しています。
関連リンク
- Go CL 7323067: https://golang.org/cl/7323067
参考にした情報源リンク
- Unix Signals: https://en.wikipedia.org/wiki/Unix_signal
sigprocmask
man page: https://man7.org/linux/man-pages/man2/sigprocmask.2.htmlsigaction
man page: https://man7.org/linux/man-pages/man2/sigaction.2.htmlnohup
man page: https://man7.org/linux/man-pages/man1/nohup.1.html- Go runtime scheduler (M, G, P): https://go.dev/doc/articles/go_scheduler.html (一般的なGoスケジューラに関する情報源)
- Go source code (for context on
runtime
package): https://github.com/golang/go/tree/master/src/runtime - Russ Cox's contributions to Go: https://github.com/golang/go/commits?author=rsc
- Go issue tracker (for related issues, if any): https://github.com/golang/go/issues
- Golang-dev mailing list archives (for discussions related to the CL): https://groups.google.com/g/golang-dev