[インデックス 12171] ファイルの概要
このコミットは、GoランタイムにおけるLinuxシグナルマスキングの挙動を修正し、特にLinux環境での安定性向上を目的としています。Goプログラムが新しいOSスレッド(M)を作成する際に、シグナルが適切に処理されるように、rt_sigprocmask
システムコールを介したシグナルマスクの操作が導入されました。これにより、Issue 3101(Linuxにおけるシグナル関連の問題)が解決されました。
コミット
commit 240b1d5b44f51e6bda24256f276909f64fc4b0ea
Author: Russ Cox <rsc@golang.org>
Date: Thu Feb 23 14:43:58 2012 -0500
runtime: linux signal masking
Fixes #3101 (Linux).
R=golang-dev, bradfitz, minux.ma
CC=golang-dev
https://golang.org/cl/5696043
---
src/pkg/runtime/os_linux.h | 11 +++++++++++
src/pkg/runtime/sys_linux_386.s | 12 ++++++++++++
src/pkg/runtime/sys_linux_amd64.s | 12 ++++++++++++
src/pkg/runtime/sys_linux_arm.s | 10 ++++++++++
src/pkg/runtime/thread_linux.c | 13 ++++++++++++-
5 files changed, 57 insertions(+), 1 deletion(-)
GitHub上でのコミットページへのリンク
https://github.com/golang/go/commit/240b1d5b44f51e6bda24256f276909f64fc4b0ea
元コミット内容
runtime: linux signal masking
Fixes #3101 (Linux).
R=golang-dev, bradfitz, minux.ma CC=golang-dev https://golang.org/cl/5696043
変更の背景
このコミットの主な背景は、GoランタイムがLinux上で新しいOSスレッドを作成する際のシグナル処理に関する問題、特にGo Issue 3101の解決です。Goランタイムは、GoルーチンをOSスレッドにマッピングして実行しますが、このOSスレッドの作成時(clone
システムコールを使用)にシグナルマスクが適切に設定されていないと、予期せぬシグナルが新しいスレッドに配送され、プログラムの不安定性やクラッシュを引き起こす可能性がありました。
具体的には、FreeBSDやLinux/ARMなどの特定の環境で、シグナル関連の不安定性が観測されており、この修正はこれらの環境での安定性向上を目的としていました。新しいスレッドが起動する際に、一時的にすべてのシグナルを無効にし、スレッドの初期化が完了した後に再度シグナルを有効にすることで、シグナル配送のタイミングに起因する競合状態や問題を回避しようとしています。
前提知識の解説
Linuxシグナルとシグナルマスキング
Linuxにおけるシグナルは、プロセスやスレッドに対して非同期的にイベントを通知するメカニズムです。例えば、Ctrl+Cによる割り込み(SIGINT)、不正なメモリアクセス(SIGSEGV)、子プロセスの終了(SIGCHLD)などがあります。 シグナルマスキングとは、特定のスレッドが特定のシグナルを受け取らないようにする(ブロックする)機能です。これにより、重要な処理中にシグナルによる割り込みを防ぎ、プログラムの整合性を保つことができます。
sigprocmask
とrt_sigprocmask
システムコール
sigprocmask
は、プロセスのシグナルマスクを変更するためのPOSIX標準関数です。しかし、Linuxではより高機能なrt_sigprocmask
システムコールが提供されており、これはリアルタイムシグナル(SIGRTMIN
からSIGRTMAX
までの範囲のシグナル)も扱えます。Goランタイムは、低レベルなOS操作を行うため、直接rt_sigprocmask
システムコールを呼び出す必要があります。
rt_sigprocmask
の主な引数は以下の通りです。
how
: シグナルマスクの変更方法(SIG_BLOCK
で追加、SIG_UNBLOCK
で削除、SIG_SETMASK
で設定)。set
: 新しいシグナルマスク(設定したいシグナルのビットマスク)。oldset
: 変更前のシグナルマスクを格納するポインタ。sigsetsize
:set
とoldset
のサイズ(通常はsizeof(sigset_t)
)。
clone
システムコール
clone
システムコールは、Linuxで新しいスレッドやプロセスを作成するための低レベルなシステムコールです。fork
とは異なり、親プロセスと子プロセス(またはスレッド)間でメモリ空間、ファイルディスクリプタ、シグナルハンドラなどを共有するかどうかを細かく制御できます。Goランタイムは、新しいOSスレッドを作成する際にこのclone
システムコールを利用します。
Goランタイムの構造
Goプログラムは、Goルーチンと呼ばれる軽量な並行処理単位を使用します。これらのGoルーチンは、GoランタイムによってOSスレッド(M: Machine)に多重化されて実行されます。Goランタイムは、スケジューラ、メモリ管理、ガベージコレクション、そしてOSとの低レベルなインタラクション(システムコール呼び出しなど)を担当します。このコミットは、GoランタイムのOSとのインタラクション層、特にLinux固有のシグナル処理部分に焦点を当てています。
アセンブリ言語
Goランタイムの低レベルな部分は、パフォーマンスとOSとの直接的なインタラクションのためにアセンブリ言語で記述されることがあります。このコミットでは、rt_sigprocmask
システムコールを呼び出すためのアセンブリコードが、386(x86)、AMD64(x86-64)、ARMの各アーキテクチャ向けに追加されています。アセンブリコードは、レジスタの操作やシステムコール番号の指定など、ハードウェアに非常に近いレベルで動作します。
技術的詳細
このコミットの核心は、Goランタイムが新しいOSスレッドを生成する際に、シグナルマスクを一時的に操作することです。
-
Sigset
構造体の定義:src/pkg/runtime/os_linux.h
にSigset
という構造体が定義されました。これは、Linuxのsigset_t
に対応するもので、シグナルマスクを表現します。GoランタイムはC言語で記述されている部分もあるため、Cの構造体として定義されています。typedef struct Sigset Sigset; struct Sigset { uint32 mask[2]; };
uint32 mask[2]
は、64ビットのシグナルマスクを表現するために2つの32ビット整数を使用しています。これは、Linuxのsigset_t
が通常64ビット(またはそれ以上)のビットマスクであることに対応しています。 -
runtime·rtsigprocmask
関数の追加:src/pkg/runtime/os_linux.h
でruntime·rtsigprocmask
という関数が宣言され、各アーキテクチャ(386, amd64, arm)のアセンブリファイル(sys_linux_386.s
,sys_linux_amd64.s
,sys_linux_arm.s
)にその実装が追加されました。この関数は、rt_sigprocmask
システムコールを呼び出すためのGoランタイムのラッパーです。- 386 (x86): システムコール番号175 (
SYS_rt_sigprocmask
) をAX
レジスタに設定し、引数をBX
,CX
,DX
,SI
レジスタに渡して_vdso
(vDSO: virtual Dynamic Shared Object)経由でシステムコールを呼び出します。エラーチェックも含まれています。 - AMD64 (x86-64): システムコール番号14 (
SYS_rt_sigprocmask
) をAX
レジスタに設定し、引数をDI
,SI
,DX
,R10
レジスタに渡してSYSCALL
命令でシステムコールを呼び出します。エラーチェックも含まれています。 - ARM: システムコール番号175 (
SYS_rt_sigprocmask
) をR7
レジスタに設定し、引数をR0
,R1
,R2
,R3
レジスタに渡してSWI $0
(ソフトウェア割り込み)でシステムコールを呼び出します。
- 386 (x86): システムコール番号175 (
-
スレッド作成時のシグナルマスク操作:
src/pkg/runtime/thread_linux.c
のruntime·newosproc
関数(新しいOSスレッドを作成する関数)内で、clone
システムコールを呼び出す前後にruntime·rtsigprocmask
が使用されるようになりました。clone
呼び出しの直前に、sigset_all
(すべてのシグナルをブロックするマスク)を使用して、現在のスレッドのシグナルマスクを一時的に変更し、すべてのシグナルを無効にします。変更前のシグナルマスクはoset
に保存されます。clone
呼び出しの後、oset
に保存されていた元のシグナルマスクを復元します。- これにより、新しいスレッドが作成される間、親スレッドはシグナルを受け取らず、新しいスレッドも初期状態ではシグナルが無効な状態で起動します。
-
runtime·minit
でのシグナルマスク初期化:src/pkg/runtime/thread_linux.c
のruntime·minit
関数(GoランタイムのM(OSスレッド)の初期化関数)内で、runtime·rtsigprocmask
がsigset_none
(すべてのシグナルを許可するマスク)を使用して呼び出されます。これにより、新しく起動したOSスレッドは、初期化が完了した後にシグナルを受け取れるようになります。
コードレビューでの議論点
sigprocmask
の戻り値: レビューアからsigprocmask
の戻り値の扱いについて質問がありましたが、386とAMD64のアセンブリコードにはエラー時にプログラムをクラッシュさせるチェックが含まれていることが説明されました。- ARMアーキテクチャのエラーチェック: ARMのアセンブリコードには、他のアーキテクチャのようなシステムコール失敗時のエラーチェックが含まれていないことが指摘されました。これは、ARMでは歴史的にそのようなチェックがなかったためであり、この変更では追加しない方針が取られましたが、別の変更として追加される可能性が示唆されました。
コアとなるコードの変更箇所
このコミットでは、以下の5つのファイルが変更されています。
-
src/pkg/runtime/os_linux.h
:Sigset
構造体の定義が追加されました。runtime·rtsigprocmask
関数の宣言が追加されました。SIG_SETMASK
マクロが定義されました。
-
src/pkg/runtime/sys_linux_386.s
:runtime·rtsigprocmask
のアセンブリ実装が追加されました。これは、386アーキテクチャ向けのrt_sigprocmask
システムコールラッパーです。
-
src/pkg/runtime/sys_linux_amd64.s
:runtime·rtsigprocmask
のアセンブリ実装が追加されました。これは、AMD64アーキテクチャ向けのrt_sigprocmask
システムコールラッパーです。
-
src/pkg/runtime/sys_linux_arm.s
:SYS_rt_sigprocmask
システムコール番号の定義が追加されました。runtime·rtsigprocmask
のアセンブリ実装が追加されました。これは、ARMアーキテクチャ向けのrt_sigprocmask
システムコールラッパーです。
-
src/pkg/runtime/thread_linux.c
:sigset_all
とsigset_none
というSigset
型のグローバル変数が追加されました。runtime·newosproc
関数内で、clone
システムコール呼び出しの前後にruntime·rtsigprocmask
を使用してシグナルマスクを一時的に変更するロジックが追加されました。runtime·minit
関数内で、runtime·rtsigprocmask
を使用してシグナルマスクを初期化するロジックが追加されました。
コアとなるコードの解説
src/pkg/runtime/os_linux.h
+// It's hard to tease out exactly how big a Sigset is, but
+// rt_sigprocmask crashes if we get it wrong, so if binaries
+// are running, this is right.
+typedef struct Sigset Sigset;
+struct Sigset
+{
+ uint32 mask[2];
+};
+void runtime·rtsigprocmask(int32, Sigset*, Sigset*, int32);
+#define SIG_SETMASK 2
Sigset
構造体は、Linuxのシグナルセット(sigset_t
)をGoランタイム内で表現するためのものです。mask[2]
は、64ビットのシグナルマスクを格納するために2つの32ビット整数配列を使用しています。runtime·rtsigprocmask
は、rt_sigprocmask
システムコールを呼び出すためのGoランタイムのC関数プロトタイプです。SIG_SETMASK
は、シグナルマスクを設定するための操作モードを定義しています。
src/pkg/runtime/sys_linux_amd64.s
(AMD64の例)
+TEXT runtime·rtsigprocmask(SB),7,$0-32
+ MOVL 8(SP), DI
+ MOVQ 16(SP), SI
+ MOVQ 24(SP), DX
+ MOVL 32(SP), R10
+ MOVL $14, AX // syscall entry
+ SYSCALL
+ CMPQ AX, $0xfffffffffffff001
+ JLS 2(PC)
+ CALL runtime·notok(SB)
+ RET
これはAMD64アーキテクチャ向けのruntime·rtsigprocmask
関数のアセンブリ実装です。
MOVL
やMOVQ
命令で、スタック上の引数(how
,set
,oldset
,sigsetsize
)を対応するレジスタ(DI
,SI
,DX
,R10
)に移動します。MOVL $14, AX
で、rt_sigprocmask
システムコールの番号(14)をAX
レジスタに設定します。SYSCALL
命令でシステムコールを実行します。CMPQ AX, $0xfffffffffffff001
とJLS 2(PC)
、CALL runtime·notok(SB)
は、システムコールがエラーを返した場合(通常は負の値)に、runtime·notok
関数を呼び出してプログラムをクラッシュさせるためのエラーチェックです。
src/pkg/runtime/thread_linux.c
+static Sigset sigset_all = { ~(uint32)0, ~(uint32)0 };
+static Sigset sigset_none;
// ...
runtime·newosproc(M *m, G *g, void *stk, void (*fn)(void))
{
int32 ret;
int32 flags;
+ Sigset oset;
// ...
+ // 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);
+ ret = runtime·clone(flags, stk, m, g, fn);
+ runtime·rtsigprocmask(SIG_SETMASK, &oset, nil, sizeof oset);
+
+ if(ret < 0) {
runtime·printf("runtime: failed to create new OS thread (have %d already; errno=%d)\\n", runtime·mcount(), -ret);
runtime·throw("runtime.newosproc");
}
// ...
void runtime·minit(void)
{
// ...
// Initialize signal handling.
m->gsignal = runtime·malg(32*1024); // OS X wants >=8K, Linux >=2K
runtime·signalstack(m->gsignal->stackguard - StackGuard, 32*1024);
+ runtime·rtsigprocmask(SIG_SETMASK, &sigset_none, nil, sizeof sigset_none);
}
sigset_all
はすべてのビットが1に設定されており、すべてのシグナルをブロックするマスクを表します。sigset_none
はすべてのビットが0で、すべてのシグナルを許可するマスクを表します。runtime·newosproc
関数内で、runtime·clone
(新しいOSスレッドを作成するGoランタイムのラッパー)を呼び出す直前に、runtime·rtsigprocmask(SIG_SETMASK, &sigset_all, &oset, sizeof oset);
が実行されます。これは、現在のスレッドのシグナルマスクをsigset_all
に設定し、元のマスクをoset
に保存します。これにより、clone
実行中にシグナルが配送されるのを防ぎます。clone
が完了した後、runtime·rtsigprocmask(SIG_SETMASK, &oset, nil, sizeof oset);
で元のシグナルマスクを復元します。runtime·minit
関数内で、runtime·rtsigprocmask(SIG_SETMASK, &sigset_none, nil, sizeof sigset_none);
が実行されます。これは、新しく作成されたOSスレッドが初期化された後、すべてのシグナルを受け取れるようにシグナルマスクをsigset_none
に設定します。
これらの変更により、GoランタイムはLinux上でのスレッド作成とシグナル処理をより堅牢に行えるようになり、特定の環境での不安定性が解消されました。
関連リンク
- Go Issue 3101: https://golang.org/issue/3101 (このコミットによって修正された問題)
- GitHubコミットページ: https://github.com/golang/go/commit/240b1d5b44f51e6bda24256f276909f64fc4b0ea
参考にした情報源リンク
- Go Code Review 5696043: https://golang.org/cl/5696043
- Linux
sigprocmask
man page: (通常はman 2 sigprocmask
で参照可能) - Linux
clone
man page: (通常はman 2 clone
で参照可能) - Goランタイムの内部構造に関するドキュメントや記事 (一般的なGoランタイムの知識)