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

[インデックス 15270] ファイルの概要

このコミットは、Goプログラムがnohupコマンドの下で実行された際に発生するシグナルハンドリングの問題を修正するものです。具体的には、nohupSIGHUPシグナルをマスクするか、そのハンドラをSIG_IGN(シグナルを無視する設定)に設定する挙動に対応し、Goランタイムがこれらの設定を適切に継承・尊重するように変更されました。これにより、nohupで起動されたGoプログラムが予期せず終了する問題を解決します。

コミット

commit f3407f445d51dac3b9415cb5025ac98ccbbc80eb
Author: Russ Cox <rsc@golang.org>
Date:   Fri Feb 15 11:18:55 2013 -0500

    runtime: fix running under nohup
    
    There are two ways nohup(1) might be implemented:
    it might mask away the signal, or it might set the handler
    to SIG_IGN, both of which are inherited across fork+exec.
    So two fixes:
    
    * Make sure to preserve the inherited signal mask at
    minit instead of clearing it.
    
    * If the SIGHUP handler is SIG_IGN, leave it that way.
    
    Fixes #4491.
    
    R=golang-dev, mikioh.mikioh, iant
    CC=golang-dev
    https://golang.org/cl/7308102

GitHub上でのコミットページへのリンク

https://github.com/golang/go/commit/f3407f445d51dac3b9415cb5025ac98ccbbc80eb

元コミット内容

このコミットは、Goランタイムがnohupコマンドの下で正しく動作するように修正することを目的としています。nohupは、プロセスが端末から切断された後も実行を継続させるためのコマンドですが、その実装方法には2つのパターンがあります。一つはシグナルマスクを操作してSIGHUPシグナルをブロックする方法、もう一つはSIGHUPのハンドラをSIG_IGN(無視)に設定する方法です。これらの設定はfork+execシステムコールを通じて子プロセスに継承されます。

このコミットでは、以下の2つの修正が導入されています。

  1. minit(Goランタイムの初期化処理の一部)において、継承されたシグナルマスクをクリアするのではなく、確実に保持するように変更。
  2. SIGHUPハンドラが既にSIG_IGNに設定されている場合、Goランタイムが独自のハンドラを設定せずに、その状態を維持するように変更。

これらの修正により、Goプログラムがnohup環境下で予期せず終了する問題(Go issue #4491)が解決されます。

変更の背景

Goプログラムがnohupコマンドを使用してバックグラウンドで実行された際に、予期せず終了してしまうという問題が報告されていました(Go issue #4491)。nohupは、ユーザーがログアウトしたり、端末が閉じられたりしても、プロセスがSIGHUP(ハングアップシグナル)を受け取って終了しないようにするために使用されます。

従来のGoランタイムのシグナルハンドリングの初期化ロジックでは、プロセス起動時にシグナルマスクをリセットしたり、特定のシグナルハンドラを上書きしたりする傾向がありました。この挙動がnohupの意図と衝突し、nohupによって設定されたSIGHUPの無視設定やシグナルマスクがGoランタイムによってクリアされてしまい、結果としてSIGHUPを受け取ったGoプログラムが終了してしまうという問題が発生していました。

このコミットは、nohupの挙動を正しく尊重し、Goプログラムが安定してバックグラウンド実行を継続できるようにするために必要とされました。

前提知識の解説

1. シグナル (Signals)

Unix系OSにおけるシグナルは、プロセスに対して非同期的にイベントを通知するソフトウェア割り込みの一種です。例えば、Ctrl+Cを押すとSIGINTが、プログラムが不正なメモリアクセスをするとSIGSEGVが、そして端末が切断されるとSIGHUPが送信されます。

  • SIGHUP (Hangup Signal): 端末が切断された際に、その端末から起動されたプロセスに送信されるシグナルです。通常、このシグナルを受け取ったプロセスは終了します。
  • SIG_DFL (Default Action): シグナルに対するデフォルトの動作を意味します。例えば、SIGHUPのデフォルト動作はプロセスの終了です。
  • SIG_IGN (Ignore Signal): シグナルを無視することを意味します。この設定がされているシグナルを受け取っても、プロセスは何も反応しません。

2. シグナルハンドラ (Signal Handler)

プロセスは、特定のシグナルを受け取った際に実行する関数(シグナルハンドラ)を登録することができます。これにより、デフォルトの動作を上書きし、シグナルに対してカスタムの処理を行うことが可能になります。

3. シグナルマスク (Signal Mask)

シグナルマスクは、プロセスが現在ブロックしているシグナルのセットです。ブロックされたシグナルは、そのシグナルがマスクから解除されるまで保留されます。これにより、重要な処理中にシグナルによって中断されるのを防ぐことができます。

4. nohup コマンド

nohup (no hang up) コマンドは、ユーザーがログアウトしたり、端末が閉じられたりしても、その後に続くコマンドがSIGHUPシグナルを受け取って終了しないようにするために使用されます。nohupは、以下のいずれかの方法でSIGHUPを処理します。

  • SIGHUPのシグナルハンドラをSIG_IGNに設定する: これが一般的な実装です。子プロセスはこの設定を継承します。
  • SIGHUPをシグナルマスクに追加してブロックする: この場合も、子プロセスは親プロセスのシグナルマスクを継承します。

5. forkexec

Unix系OSで新しいプロセスを起動する際によく使われるシステムコールです。

  • fork(): 現在のプロセス(親プロセス)のコピーである新しいプロセス(子プロセス)を作成します。子プロセスは親プロプロセスのシグナルハンドラやシグナルマスクなどの多くの属性を継承します。
  • exec(): 現在のプロセスイメージを、指定された新しいプログラムイメージで置き換えます。execが成功すると、現在のプロセスは新しいプログラムを実行し始めます。シグナルハンドラやシグナルマスクは、exec後も通常は維持されます。

6. GoランタイムのM, G, P (Goroutine, M, P)

Goランタイムは、並行処理を管理するためにM(Machine)、G(Goroutine)、P(Processor)という3つの主要な抽象化を使用します。

  • G (Goroutine): Goにおける軽量なスレッドのようなものです。Goプログラムの並行実行単位です。
  • M (Machine): OSのスレッドに相当します。Goランタイムは、MをOSスレッドにマッピングし、その上でGを実行します。
  • P (Processor): MがGを実行するために必要なリソース(スケジューラキューなど)を表します。

Goランタイムは、OSスレッド(M)上でゴルーチン(G)をスケジューリングして実行します。シグナルハンドリングやシグナルマスクの管理は、OSスレッドレベルで行われるため、GoランタイムがOSスレッドをどのように初期化し、シグナルを扱うかが重要になります。

技術的詳細

このコミットの技術的な核心は、Goランタイムがnohupによって設定されたSIGHUPの処理方法(無視またはブロック)を正しく認識し、それを上書きしないようにすることです。

問題点

Goランタイムは、起動時(特にminit関数内)にシグナルハンドラやシグナルマスクを初期化します。この初期化プロセスが、nohupによって親プロセスから継承されたSIGHUPの無視設定やブロック設定を意図せずクリアしてしまう可能性がありました。

具体的には、Goランタイムがminitでシグナルマスクをsigset_none(全てのシグナルをブロックしない状態)にリセットしたり、SIGHUPに対して独自のハンドラを設定しようとしたりすることで、nohupの保護が失われ、SIGHUPを受け取るとGoプログラムが終了してしまうという挙動につながっていました。

修正アプローチ

コミットは2つの主要なアプローチでこの問題を解決します。

  1. 継承されたシグナルマスクの保持:

    • newosproc関数(新しいOSスレッドを生成する際に呼ばれる)内で、現在のシグナルマスク(oset)を取得し、それをM構造体の新しいフィールドsigsetに保存します。
    • minit関数(OSスレッドの初期化時に呼ばれる)内で、m->sigsetnilでない場合(つまり、親プロセスからシグナルマスクが継承されている場合)、その継承されたシグナルマスクをSIG_SETMASKで設定し直します。これにより、nohupによってブロックされたSIGHUPがGoランタイムによって誤ってアンブロックされるのを防ぎます。
  2. SIG_IGNSIGHUPハンドラの尊重:

    • setsig関数(特定のシグナルに対するハンドラを設定する関数)内で、設定しようとしているシグナルがSIGHUPであるかどうかをチェックします。
    • もしSIGHUPであれば、まず現在のSIGHUPハンドラを取得します。
    • 取得したハンドラがSIG_IGN(無視)である場合、Goランタイムは独自のハンドラを設定せずに、そのままsetsig関数を終了します。これにより、nohupによって設定されたSIGHUPの無視設定がGoランタイムによって上書きされるのを防ぎます。

これらの修正により、Goランタイムはnohupの意図を尊重し、SIGHUPシグナルが適切に処理されるようになります。

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

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

  • src/pkg/runtime/os_*.h: 各OSのヘッダーファイルにSIGHUPの定義が追加されています。
  • src/pkg/runtime/runtime.h: M構造体にvoid* sigset;フィールドが追加され、OSスレッドが継承したシグナルマスクを保持できるようになります。
  • src/pkg/runtime/signal_*.c: 各OS/アーキテクチャごとのsetsig関数に、SIGHUPSIG_IGNである場合にハンドラを設定しないロジックが追加されています。
  • src/pkg/runtime/thread_*.c: 各OSごとのスレッド関連のファイルで、newosproc関数内で現在のシグナルマスクをM構造体のsigsetフィールドに保存する処理が追加され、minit関数内でこの保存されたシグナルマスクを復元するロジックが追加されています。また、不要になったsigset_none変数の定義が削除されています。

具体的なファイルと変更行数は以下の通りです。

  • src/pkg/runtime/os_darwin.h
  • src/pkg/runtime/os_freebsd.h
  • src/pkg/runtime/os_linux.h
  • src/pkg/runtime/os_netbsd.h
  • src/pkg/runtime/os_openbsd.h
    • これらのファイルに #define SIGHUP 1 が追加されています。
  • src/pkg/runtime/runtime.h
    • struct Mvoid* sigset; が追加されています。
  • src/pkg/runtime/signal_darwin_386.c
  • src/pkg/runtime/signal_darwin_amd64.c
  • src/pkg/runtime/signal_freebsd_386.c
  • src/pkg/runtime/signal_freebsd_amd64.c
  • src/pkg/runtime/signal_freebsd_arm.c
  • src/pkg/runtime/signal_linux_386.c
  • src/pkg/runtime/signal_linux_amd64.c
  • src/pkg/runtime/signal_linux_arm.c
  • src/pkg/runtime/signal_netbsd_386.c
  • src/pkg/runtime/signal_netbsd_amd64.c
  • src/pkg/runtime/signal_netbsd_arm.c
  • src/pkg/runtime/signal_openbsd_386.c
  • src/pkg/runtime/signal_openbsd_amd64.c
    • これらのファイル内の runtime·setsig 関数に、SIGHUPSIG_IGNである場合の早期リターンロジックが追加されています。
  • 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
    • これらのファイル内の runtime·newosproc 関数で、現在のシグナルマスクをmp->sigsetに保存する処理が追加されています。
    • これらのファイル内の runtime·minit 関数で、mp->sigsetが存在する場合にそのシグナルマスクを復元する処理が追加されています。
    • 不要になった static Sigset sigset_none; の定義が削除されています。

コアとなるコードの解説

src/pkg/runtime/runtime.h の変更

M構造体はGoランタイムのOSスレッドを表します。ここにvoid* sigset;が追加されたことで、各OSスレッドが自身の初期シグナルマスク(親プロセスから継承されたもの)を保持できるようになりました。これは、nohupのような外部ツールによって設定されたシグナルマスクをGoランタイムが尊重するための重要なステップです。

struct	M
{
	// ... 既存のフィールド ...
	void*	sigset; // 追加されたフィールド
	// ... 既存のフィールド ...
};

src/pkg/runtime/signal_*.cruntime·setsig 関数の変更

この変更は、SIGHUPシグナルが既にSIG_IGN(無視)に設定されている場合に、Goランタイムがそのハンドラを上書きしないようにするためのものです。

// 例: src/pkg/runtime/signal_linux_amd64.c
void
runtime·setsig(int32 i, void (*fn)(int32, Siginfo*, void*, G*), bool restart)
{
	Sigaction sa;

	// If SIGHUP handler is SIG_IGN, assume running
	// under nohup and do not set explicit handler.
	if(i == SIGHUP) {
		runtime·memclr((byte*)&sa, sizeof sa);
		runtime·sigaction(i, nil, &sa); // 現在のSIGHUPハンドラを取得
		if(sa.sa_handler == SIG_IGN) // SIG_IGNであれば
			return; // 独自のハンドラを設定せずに終了
	}

	runtime·memclr((byte*)&sa, sizeof sa);
	sa.sa_flags = SA_ONSTACK | SA_SIGINFO | SA_RESTORER;
	if(restart)
		sa.sa_flags |= SA_RESTART;
	sa.sa_mask = ~(uint64)0; // 全てのシグナルをブロック
	sa.sa_handler = (void*)fn;
	runtime·sigaction(i, &sa, nil);
}

このコードは、setsigが呼ばれた際に、まず設定対象のシグナルがSIGHUPであるかを確認します。もしSIGHUPであれば、runtime·sigactionを呼び出して現在のSIGHUPハンドラを取得します。そのハンドラがSIG_IGNであれば、それはnohupによって設定されたものである可能性が高いため、Goランタイムはそれ以上何もせずに関数を終了します。これにより、nohupの意図が尊重されます。

src/pkg/runtime/thread_*.cruntime·newosprocruntime·minit の変更

これらの変更は、OSスレッドがfork+execによって継承したシグナルマスクをGoランタイムが保持し、初期化時にそれを復元するようにするためのものです。

runtime·newosproc の変更例 (src/pkg/runtime/thread_linux.c)

runtime·newosprocは新しいOSスレッドを生成する際に呼ばれます。ここで、現在のシグナルマスクを保存します。

void
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)); // M構造体にシグナルマスクを保存するためのメモリを確保
	*(Sigset*)mp->sigset = oset; // 現在のシグナルマスクを保存
	ret = runtime·clone(flags, stk, mp, gp, fn);
	runtime·rtsigprocmask(SIG_SETMASK, &oset, nil, sizeof oset);

	// ... 既存のコード ...
}

runtime·rtsigprocmaskを呼び出して現在のシグナルマスク(oset)を取得し、それを新しく追加されたmp->sigsetにコピーしています。これにより、新しいOSスレッドが起動する際に、親プロセスから継承されたシグナルマスクが失われることなく、M構造体に関連付けられます。

runtime·minit の変更例 (src/pkg/runtime/thread_linux.c)

runtime·minitは、各OSスレッドが初期化される際に呼ばれる関数です。ここで、保存されたシグナルマスクを復元します。

void
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) // sigsetが保存されていれば
		runtime·rtsigprocmask(SIG_SETMASK, m->sigset, nil, sizeof *m->sigset); // そのシグナルマスクを復元
}

この変更により、minitが実行される際に、もしmp->sigsetにシグナルマスクが保存されていれば、それを現在のOSスレッドのシグナルマスクとして設定し直します。これにより、nohupによって設定されたシグナルマスク(SIGHUPをブロックする設定など)がGoランタイムの初期化によってクリアされるのを防ぎ、nohupの意図が維持されます。

また、static Sigset sigset_none; の定義が削除されています。これは、以前はシグナルマスクを完全にクリアするために使用されていましたが、継承されたシグナルマスクを尊重する新しいアプローチでは不要になったためです。

これらの変更は、Goプログラムがnohupのような外部環境とより協調して動作し、予期せぬ終了を防ぐために不可欠なものです。

関連リンク

参考にした情報源リンク