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

[インデックス 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. sigactionrt_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 Msigsetフィールドが存在し、runtime·newosproc(新しいOSスレッドを作成する関数)やruntime·minit(Mの初期化関数)でそのフィールドを操作していたことから推測できます。

しかし、この「シグナルマスクの保存と復元」という戦略が、特にLinux環境で問題を引き起こしました。コミットメッセージにある「非常に奇妙なクラッシュ」は、シグナルマスクの不整合や、OSのシグナル配送メカニズムとの予期せぬ相互作用に起因する可能性があります。例えば、Goランタイムがシグナルマスクを保存した時点と、それを復元しようとした時点の間で、OSレベルでシグナルハンドリングの状態が変化した場合、問題が発生する可能性があります。

このコミットでは、この問題に対する根本的な解決策として、シグナルマスクの保存をやめることを選択しました。代わりに、以下の2つの主要な変更が行われました。

  1. 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のデフォルトの無視動作に任せます。

  2. 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がシグナルマスクのサイズを正しく認識できるようにしています。

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

このコミットにおける主要なコード変更は、以下のファイルにまたがっています。

  1. 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;
    
  2. 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;
     	}
    
  3. 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スレッドのシグナルマスクを管理する方法のパラダイムシフトにあります。

  1. struct Mからのsigsetフィールド削除:

    • 以前は、各OSスレッド(M)が自身のシグナルマスクの状態をm->sigsetに保存していました。これは、Goルーチンが異なるM間で移動する際に、シグナルマスクの状態を維持しようとする試みだったと考えられます。
    • このフィールドの削除は、ランタイムがOSスレッドのシグナルマスクを明示的に保存・復元するというアプローチを完全に放棄したことを意味します。これにより、シグナルマスクの不整合によるクラッシュのリスクが排除されます。
  2. runtime·rt_sigactionへの移行 (Linux):

    • Linux固有のシグナルハンドリングコードにおいて、runtime·sigactionからruntime·rt_sigactionへの変更は、より現代的で堅牢なシグナルハンドリングAPIへの移行を示唆しています。rt_sigactionは、より大きなシグナルセットをサポートし、リアルタイムシグナルを扱う能力があるため、GoランタイムがLinuxカーネルとより効率的かつ正確に連携できるようになります。
    • sizeof(sa.sa_mask)を引数として渡すことは、rt_sigactionシステムコールがシグナルマスクのサイズを正しく解釈するために必要です。これにより、異なるアーキテクチャやカーネルバージョン間での互換性が向上します。
  3. 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のチェックに依存することで、ランタイムの安定性と互換性が向上しています。

関連リンク

参考にした情報源リンク