[インデックス 13030] ファイルの概要
このコミットは、Go言語のランタイムがPlan 9オペレーティングシステム上でシグナル(Plan 9では「ノート」と呼ばれる)を適切に処理するための機能を追加するものです。具体的には、Goプロセスが終了する際に、他の実行中のGoプロセスが意図せず終了してしまう問題を解決し、より堅牢なプロセス管理を実現します。
コミット
commit ccdca2cd6b84f290b0cf8709b11353e58cafdba9
Author: Akshat Kumar <seed@mail.nanosouffle.net>
Date: Fri May 4 03:48:34 2012 -0700
pkg/runtime: Plan 9 signal handling in Go
This adds proper note handling for Plan 9,
and fixes the issue of properly killing go procs.
Without this change, the first go proc that dies
(using runtime·exit()) would kill all the running
go procs. Proper signal handling is needed.
R=golang-dev, ality, rminnich, rsc
CC=golang-dev, john, mirtchovski
https://golang.org/cl/5617048
GitHub上でのコミットページへのリンク
https://github.com/golang/go/commit/ccdca2cd6b84f290b0cf8709b11353e58cafdba9
元コミット内容
pkg/runtime: Plan 9 signal handling in Go
This adds proper note handling for Plan 9,
and fixes the issue of properly killing go procs.
Without this change, the first go proc that dies
(using runtime·exit()) would kill all the running
go procs. Proper signal handling is needed.
変更の背景
この変更が導入される前、Go言語のランタイムがPlan 9上で動作する際、runtime·exit()
関数が呼び出されてGoプロセスが終了すると、同じプロセスグループに属する他のすべてのGoプロセスも強制的に終了してしまうという深刻な問題がありました。これは、Plan 9のプロセス間通信およびシグナル(ノート)の扱いの特性に起因していました。Goのランタイムは複数のゴルーチン(軽量スレッド)を管理し、それらがOSのスレッドやプロセスにマッピングされることがありますが、Plan 9の環境下では、一つのGoプロセスが終了する際に、その終了が他の関連プロセスに不適切に伝播し、意図しない連鎖的な終了を引き起こしていました。
この挙動は、Goアプリケーションが複数の独立したプロセスとして動作する場合や、バックグラウンドで継続的に実行されるサービスにおいて、信頼性と安定性を著しく損なうものでした。したがって、GoランタイムがPlan 9のノートシステムを適切に利用し、個々のGoプロセスが独立して終了できるような、より洗練されたシグナルハンドリングメカニズムを実装する必要がありました。
前提知識の解説
Plan 9オペレーティングシステム
Plan 9 from Bell Labsは、ベル研究所で開発された分散オペレーティングシステムです。Unixの設計思想をさらに推し進め、「すべてをファイルとして扱う」という原則を徹底しています。プロセス、デバイス、ネットワーク接続など、システム内のあらゆるリソースがファイルシステム上のファイルとして表現され、標準的なファイルI/O操作(open
, read
, write
, close
)を通じてアクセスされます。これにより、システム全体が統一されたインターフェースで操作可能となり、分散システム構築が容易になるという特徴があります。
Plan 9のシグナルハンドリング(Notes)
Unix系OSにおけるシグナル(SIGTERM
, SIGKILL
など)に相当する機能が、Plan 9では「ノート(Notes)」と呼ばれます。ノートは、プロセスに対して非同期にメッセージを送信するメカニズムです。Unixシグナルとは異なり、ノートは単なる数値ではなく、文字列メッセージとして送信されます。
notify
システムコール: プロセスがノートハンドラを登録するために使用します。登録されたハンドラは、プロセスがノートを受信した際に呼び出されます。noted
システムコール: ノートハンドラ内で呼び出され、ノートの処理方法(継続、デフォルト動作、無視など)をOSに伝えます。NCONT
(0): ノートを処理した後、通常の実行を継続します。NDFLT
(1): ノートのデフォルト動作を実行します(通常はプロセス終了)。
postnote
システムコール: あるプロセスが別のプロセスに対してノートを送信するために使用します。これはUnixのkill
コマンドに似ていますが、メッセージを伴います。
Plan 9では、プロセスは通常、プロセスグループではなく、個々のプロセスID(PID)に基づいてノートを受信します。また、/proc/<pid>/note
という特殊なファイルにメッセージを書き込むことで、そのPIDを持つプロセスにノートを送信できます。
Goランタイム
Go言語のランタイムは、Goプログラムの実行を管理する重要なコンポーネントです。これには、ガベージコレクション、スケジューラ(ゴルーチンをOSスレッドにマッピング)、メモリ管理、チャネル通信、そしてOSとのインターフェース(システムコール)などが含まれます。Goのプログラムは、ランタイムの助けを借りて並行処理を実現し、効率的にリソースを利用します。
runtime·exit()
関数
runtime·exit()
は、Goランタイム内部で使用される関数で、Goプログラムの実行を終了させる役割を担います。この関数は、通常、プログラムが正常に終了する場合や、致命的なエラーが発生した場合に呼び出されます。Plan 9環境下では、この関数が呼び出された際に、プロセスグループ全体に影響を及ぼす可能性があったため、今回の変更の焦点となりました。
技術的詳細
このコミットは、GoランタイムがPlan 9のノートシステムと連携し、プロセス終了時の挙動を改善するための複数の変更を含んでいます。
-
ノート関連システムコールの追加:
src/pkg/runtime/os_plan9.h
にruntime·notify
,runtime·noted
,runtime·gonote
の宣言が追加されました。これらはPlan 9のノートシステムコールをGoランタイムから呼び出すためのラッパーです。NCONT
とNDFLT
というノート処理の定数も定義されました。src/pkg/runtime/sys_plan9_386.s
には、notify
(システムコール番号28) とnoted
(システムコール番号29) のアセンブリスタブが追加され、Goランタイムからこれらのシステムコールを直接呼び出せるようになりました。
-
PID取得の改善:
getpid()
関数がsrc/pkg/runtime/thread_plan9.c
に追加されました。これは/c/pid
ファイル(Plan 9では現在のプロセスのPIDが記述されている)を読み込むことで、現在のプロセスのPIDを取得します。これは、他のGoプロセスにノートを送信する際に、対象のPIDを特定するために必要です。
-
ノートハンドラの登録:
runtime·osinit()
関数内でruntime·notify(runtime·gonote)
が呼び出されるようになりました。これにより、Goランタイムが起動する際に、runtime·gonote
関数がPlan 9のノートハンドラとして登録されます。以降、このGoプロセスがノートを受信すると、runtime·gonote
が呼び出されます。
-
runtime·gonote
の実装:runtime·gonote
は、Plan 9からノートを受信した際に実行されるコールバック関数です。- 受信したノートメッセージが
"sys:"
で始まる場合、それはシステム終了ノートと解釈され、runtime·exitstatus
に設定されます。この場合、goexitsall()
を呼び出して他のGoプロセスに終了を通知し、runtime·noted(NDFLT)
でデフォルトの終了動作をOSに伝えます。 - ノートメッセージが
"gointr"
(Goインターラプト)の場合、runtime·noted(NCONT)
を呼び出して、ノートを処理した後もプロセスが継続するようにします。これは、他のGoプロセスからの終了通知を受け取った際に、即座に終了するのではなく、適切にクリーンアップを行うための猶予を与えるためと考えられます。 - その他のノートについては、
runtime·noted(NDFLT)
でデフォルト動作をOSに伝えます。
-
goexitsall
による他のGoプロセスへの通知:goexitsall()
関数が追加されました。この関数は、現在のGoプロセスが終了する際に、runtime·allm
(すべてのM(OSスレッド)のリスト)を走査し、現在のPID以外のMに関連付けられたプロセスIDに対して"gointr"
ノートを送信します。これにより、一つのGoプロセスが終了する際に、他のGoプロセスにその旨を通知し、適切に終了処理を開始させることができます。
-
runtime·postnote
の実装:runtime·postnote(int32 pid, int8* msg)
関数が追加されました。これは、指定されたPIDのプロセスに対して、指定されたメッセージをノートとして送信するためのヘルパー関数です。内部的には/proc/<pid>/note
ファイルを開き、メッセージを書き込むことでノートを送信します。
-
runtime·exit
の変更:runtime·exit(int32 e)
関数が大幅に修正されました。- 終了ステータス
e
に基づいてruntime·exitstatus
を設定します。 - 最も重要な変更点は、
goexitsall()
を呼び出すようになったことです。これにより、runtime·exit()
が呼び出された際に、他のGoプロセスに終了を通知するメカニズムが組み込まれました。 - 最後に
runtime·exits(runtime·exitstatus)
を呼び出し、実際のプロセス終了を行います。
-
runtime·itoa
の追加:runtime·itoa(int32 n, byte *p, uint32 len)
関数が追加されました。これは整数をASCII文字列に変換するユーティリティ関数で、主にPIDを文字列に変換して/proc/<pid>/note
のようなパスを構築するために使用されます。
これらの変更により、GoランタイムはPlan 9のノートシステムをより細かく制御できるようになり、一つのGoプロセスが終了する際に、他のGoプロセスが適切にノートを受信し、協調的に終了処理を行うことが可能になりました。これにより、以前の「一つのGoプロセスが死ぬと全てが死ぬ」という問題が解決され、Plan 9上でのGoアプリケーションの堅牢性が向上しました。
コアとなるコードの変更箇所
src/pkg/runtime/os_plan9.h
: ノート関連の関数宣言と定数の追加。src/pkg/runtime/sys_plan9_386.s
:notify
とnoted
システムコールのためのアセンブリスタブの追加。src/pkg/runtime/thread_plan9.c
:runtime·osinit
関数にruntime·notify(runtime·gonote)
の呼び出しを追加。runtime·exit
関数のロジックを大幅に修正し、goexitsall()
の呼び出しを追加。getpid
関数の新規追加。runtime·itoa
関数の新規追加。goexitsall
関数の新規追加。runtime·gonote
関数の新規追加。runtime·postnote
関数の新規追加。
コアとなるコードの解説
runtime·gonote
(src/pkg/runtime/thread_plan9.c)
void
runtime·gonote(void*, byte *s)
{
uint8 buf[128];
int32 l;
l = runtime·findnull(s);
if(l > 4 && runtime·mcmp(s, (byte*)"sys:", 4) == 0) {
runtime·memclr(buf, sizeof buf);
runtime·memmove((void*)buf, (void*)s, runtime·findnull(s));
runtime·exitstatus = (int8*)buf;
goexitsall(); // 他のGoプロセスに終了を通知
runtime·noted(NDFLT); // デフォルトの終了動作をOSに伝える
}
if(runtime·exitstatus)
runtime·exits(runtime·exitstatus); // 終了ステータスがあれば終了
if(runtime·strcmp(s, (byte*)"gointr") == 0)
runtime·noted(NCONT); // "gointr"ノートの場合は継続
runtime·noted(NDFLT); // それ以外はデフォルト動作
}
この関数は、Plan 9カーネルからノートを受信した際にGoランタイムによって呼び出されます。
- ノートメッセージが
"sys:"
で始まる場合、それはシステム終了の指示と見なされ、runtime·exitstatus
に設定されます。その後、goexitsall()
を呼び出して他のGoプロセスに終了を通知し、runtime·noted(NDFLT)
でOSにデフォルトの終了動作を要求します。 runtime·exitstatus
が設定されている場合(つまり、既に終了が決定している場合)、runtime·exits()
を呼び出してプロセスを終了します。- ノートメッセージが
"gointr"
(Goインターラプト)の場合、runtime·noted(NCONT)
を呼び出し、ノートを処理した後もプロセスが実行を継続するようにOSに伝えます。これは、他のGoプロセスからの終了通知に対して、即座に終了するのではなく、クリーンアップなどの処理を行うための猶予を与えるためです。 - それ以外のノートについては、
runtime·noted(NDFLT)
を呼び出し、OSにデフォルトのノート処理(通常はプロセス終了)を要求します。
goexitsall
(src/pkg/runtime/thread_plan9.c)
void
goexitsall(void)
{
M *m;
int32 pid;
pid = getpid(); // 現在のプロセスのPIDを取得
for(m=runtime·atomicloadp(&runtime·allm); m; m=m->alllink)
if(m->procid != pid)
runtime·postnote(m->procid, "gointr"); // 他のGoプロセスに"gointr"ノートを送信
}
この関数は、現在のGoプロセスが終了する際に呼び出されます。runtime·allm
リストを走査し、現在のプロセスID(PID)と異なるPIDを持つすべてのM(GoランタイムのOSスレッド表現)に対して、"gointr"
というノートを送信します。これにより、同じGoプログラムの一部として実行されている他のプロセスに、現在のプロセスが終了しようとしていることを通知し、それらのプロセスが適切に反応できるようにします。
runtime·postnote
(src/pkg/runtime/thread_plan9.c)
int32
runtime·postnote(int32 pid, int8* msg)
{
int32 fd, len;
uint8 buf[128];
uint8 tmp[16];
uint8 *p, *q;
runtime·memclr(buf, sizeof buf);
q = tmp;
runtime·itoa(pid, tmp, sizeof tmp); // PIDを文字列に変換
runtime·memmove((void*)p, (void*)"/proc/", 6);
for(p += 6; *p++ = *q++; ); // /proc/PID/ を構築
p--;
runtime·memmove((void*)p, (void*)"/note", 5); // /proc/PID/note パスを構築
fd = runtime·open(buf, OWRITE); // /proc/PID/note を書き込みモードで開く
if(fd < 0)
return -1;
len = runtime·findnull((byte*)msg);
if(runtime·write(fd, msg, len) != len) { // ノートメッセージを書き込む
runtime·close(fd);
return -1;
}
runtime·close(fd);
return 0;
}
この関数は、指定されたpid
を持つプロセスにmsg
をノートとして送信します。Plan 9の「すべてはファイル」という原則に従い、/proc/<pid>/note
という特殊なファイルにノートメッセージを書き込むことで、ノートを送信します。runtime·itoa
を使用してPIDを文字列に変換し、正しいファイルパスを構築しています。
runtime·exit
(src/pkg/runtime/thread_plan9.c)
void
runtime·exit(int32 e)
{
byte tmp[16];
if(e == 0)
runtime·exitstatus = "";
else {
runtime·itoa(e, tmp, sizeof tmp);
runtime·exitstatus = (int8*)tmp;
}
goexitsall(); // 他のGoプロセスに終了を通知
runtime·exits(runtime·exitstatus); // 実際のプロセス終了
}
Goランタイムの終了関数です。
- 引数
e
(終了コード)に基づいてruntime·exitstatus
を設定します。 - 重要な変更点は、
goexitsall()
を呼び出すようになったことです。これにより、このプロセスが終了する前に、他の関連するGoプロセスに終了の意図を通知します。 - 最後に
runtime·exits(runtime·exitstatus)
を呼び出し、OSレベルでのプロセス終了を実行します。
関連リンク
参考にした情報源リンク
- Plan 9 from Bell Labs: https://9p.io/plan9/
- Plan 9 Notes (Signals): https://9p.io/magic/man2html/2/notify
- Go Runtime Source Code (for general understanding): https://github.com/golang/go/tree/master/src/runtime
- Plan 9
proc
filesystem: https://9p.io/magic/man2html/4/proc - Plan 9
pid
file: https://9p.io/magic/man2html/3/pid - Plan 9
itoa
function (general C library context): https://man.cat-v.org/plan_9/2/itoa