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

[インデックス 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)などがあります。 シグナルマスキングとは、特定のスレッドが特定のシグナルを受け取らないようにする(ブロックする)機能です。これにより、重要な処理中にシグナルによる割り込みを防ぎ、プログラムの整合性を保つことができます。

sigprocmaskrt_sigprocmaskシステムコール

sigprocmaskは、プロセスのシグナルマスクを変更するためのPOSIX標準関数です。しかし、Linuxではより高機能なrt_sigprocmaskシステムコールが提供されており、これはリアルタイムシグナル(SIGRTMINからSIGRTMAXまでの範囲のシグナル)も扱えます。Goランタイムは、低レベルなOS操作を行うため、直接rt_sigprocmaskシステムコールを呼び出す必要があります。

rt_sigprocmaskの主な引数は以下の通りです。

  • how: シグナルマスクの変更方法(SIG_BLOCKで追加、SIG_UNBLOCKで削除、SIG_SETMASKで設定)。
  • set: 新しいシグナルマスク(設定したいシグナルのビットマスク)。
  • oldset: 変更前のシグナルマスクを格納するポインタ。
  • sigsetsize: setoldsetのサイズ(通常は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スレッドを生成する際に、シグナルマスクを一時的に操作することです。

  1. Sigset構造体の定義: src/pkg/runtime/os_linux.hSigsetという構造体が定義されました。これは、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ビット(またはそれ以上)のビットマスクであることに対応しています。

  2. runtime·rtsigprocmask関数の追加: src/pkg/runtime/os_linux.hruntime·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(ソフトウェア割り込み)でシステムコールを呼び出します。
  3. スレッド作成時のシグナルマスク操作: src/pkg/runtime/thread_linux.cruntime·newosproc関数(新しいOSスレッドを作成する関数)内で、cloneシステムコールを呼び出す前後にruntime·rtsigprocmaskが使用されるようになりました。

    • clone呼び出しの直前に、sigset_all(すべてのシグナルをブロックするマスク)を使用して、現在のスレッドのシグナルマスクを一時的に変更し、すべてのシグナルを無効にします。変更前のシグナルマスクはosetに保存されます。
    • clone呼び出しの後、osetに保存されていた元のシグナルマスクを復元します。
    • これにより、新しいスレッドが作成される間、親スレッドはシグナルを受け取らず、新しいスレッドも初期状態ではシグナルが無効な状態で起動します。
  4. runtime·minitでのシグナルマスク初期化: src/pkg/runtime/thread_linux.cruntime·minit関数(GoランタイムのM(OSスレッド)の初期化関数)内で、runtime·rtsigprocmasksigset_none(すべてのシグナルを許可するマスク)を使用して呼び出されます。これにより、新しく起動したOSスレッドは、初期化が完了した後にシグナルを受け取れるようになります。

コードレビューでの議論点

  • sigprocmaskの戻り値: レビューアからsigprocmaskの戻り値の扱いについて質問がありましたが、386とAMD64のアセンブリコードにはエラー時にプログラムをクラッシュさせるチェックが含まれていることが説明されました。
  • ARMアーキテクチャのエラーチェック: ARMのアセンブリコードには、他のアーキテクチャのようなシステムコール失敗時のエラーチェックが含まれていないことが指摘されました。これは、ARMでは歴史的にそのようなチェックがなかったためであり、この変更では追加しない方針が取られましたが、別の変更として追加される可能性が示唆されました。

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

このコミットでは、以下の5つのファイルが変更されています。

  1. src/pkg/runtime/os_linux.h:

    • Sigset構造体の定義が追加されました。
    • runtime·rtsigprocmask関数の宣言が追加されました。
    • SIG_SETMASKマクロが定義されました。
  2. src/pkg/runtime/sys_linux_386.s:

    • runtime·rtsigprocmaskのアセンブリ実装が追加されました。これは、386アーキテクチャ向けのrt_sigprocmaskシステムコールラッパーです。
  3. src/pkg/runtime/sys_linux_amd64.s:

    • runtime·rtsigprocmaskのアセンブリ実装が追加されました。これは、AMD64アーキテクチャ向けのrt_sigprocmaskシステムコールラッパーです。
  4. src/pkg/runtime/sys_linux_arm.s:

    • SYS_rt_sigprocmaskシステムコール番号の定義が追加されました。
    • runtime·rtsigprocmaskのアセンブリ実装が追加されました。これは、ARMアーキテクチャ向けのrt_sigprocmaskシステムコールラッパーです。
  5. src/pkg/runtime/thread_linux.c:

    • sigset_allsigset_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関数のアセンブリ実装です。

  • MOVLMOVQ命令で、スタック上の引数(how, set, oldset, sigsetsize)を対応するレジスタ(DI, SI, DX, R10)に移動します。
  • MOVL $14, AXで、rt_sigprocmaskシステムコールの番号(14)をAXレジスタに設定します。
  • SYSCALL命令でシステムコールを実行します。
  • CMPQ AX, $0xfffffffffffff001JLS 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 Code Review 5696043: https://golang.org/cl/5696043
  • Linux sigprocmask man page: (通常はman 2 sigprocmaskで参照可能)
  • Linux clone man page: (通常はman 2 cloneで参照可能)
  • Goランタイムの内部構造に関するドキュメントや記事 (一般的なGoランタイムの知識)