[インデックス 14925] ファイルの概要
このコミットは、Go言語のsyscall
およびos
パッケージにおけるPlan 9オペレーティングシステム特有のfork-exec/wait
における競合状態(race condition)を修正するものです。具体的には、Plan 9のプロセス管理モデルとGoのゴルーチン(goroutine)スケジューリングの間の不整合によって発生する問題に対処しています。
コミット
commit b62847000b3c9c3f7837b54dab0d1234944b0722
Author: Akshat Kumar <seed@mail.nanosouffle.net>
Date: Fri Jan 18 16:43:25 2013 -0500
syscall, os: fix a fork-exec/wait race in Plan 9.
On Plan 9, only the parent of a given process can enter its wait
queue. When a Go program tries to fork-exec a child process
and subsequently waits for it to finish, the goroutines doing
these two tasks do not necessarily tie themselves to the same
(or any single) OS thread. In the case that the fork and the wait
system calls happen on different OS threads (say, due to a
goroutine being rescheduled somewhere along the way), the
wait() will either return an error or end up waiting for a
completely different child than was intended.
This change forces the fork and wait syscalls to happen in the
same goroutine and ties that goroutine to its OS thread until
the child exits. The PID of the child is recorded upon fork and
exit, and de-queued once the child's wait message has been read.
The Wait API, then, is translated into a synthetic implementation
that simply waits for the requested PID to show up in the queue
and then reads the associated stats.
R=rsc, rminnich, npe, mirtchovski, ality
CC=golang-dev
https://golang.org/cl/6545051
GitHub上でのコミットページへのリンク
https://github.com/golang/go/commit/b62847000b3c9c3f7837b54dab0d1234944b0722
元コミット内容
このコミットは、GoプログラムがPlan 9上で子プロセスをfork-exec
し、その終了をwait
する際に発生する競合状態を修正します。Plan 9では、特定の子プロセスの待機キューに入れることができるのはその親プロセスのみという制約があります。GoのゴルーチンはOSスレッドに厳密に紐付けられていないため、fork
とwait
のシステムコールが異なるOSスレッドで実行される可能性があり、その結果、wait()
がエラーを返したり、意図しない子プロセスを待機してしまったりする問題がありました。
この変更は、fork
とwait
のシステムコールを同じゴルーチン内で実行させ、そのゴルーチンを子プロセスが終了するまでOSスレッドに固定することで問題を解決します。子プロセスのPIDはfork
時に記録され、終了時にデキューされます。Wait
APIは、要求されたPIDがキューに現れるのを待機し、関連する統計情報を読み取る合成的な実装に変換されます。
変更の背景
Go言語は、その設計思想として、OSの抽象化とクロスプラットフォーム対応を重視しています。しかし、各OSにはそれぞれ独自のプロセス管理やシステムコールのセマンティクスが存在します。特にPlan 9のようなユニークな設計を持つOSでは、一般的なUnix系OSとは異なる挙動を示すことがあります。
このコミットの背景には、GoのランタイムがゴルーチンをOSスレッドにどのようにマッピングし、スケジューリングするかの特性が深く関わっています。Goのランタイムは、M:Nスケジューリングモデルを採用しており、多数のゴルーチン(M)を少数のOSスレッド(N)に多重化して実行します。これにより、高い並行性と効率性を実現していますが、特定のOSシステムコール、特にプロセス間通信やプロセス待機に関連するシステムコールにおいては、ゴルーチンが実行されるOSスレッドが途中で変更される可能性があるという問題を引き起こすことがあります。
Plan 9のwait
システムコールは、呼び出し元のプロセス(親プロセス)が子プロセスの終了を待機するためのもので、その子プロセスが生成されたOSスレッドと同じスレッドでwait
が呼び出されることを期待する、あるいは少なくとも親プロセスがその子プロセスの待機キューにアクセスできるという厳密な制約があります。Goのゴルーチンがfork
を実行したスレッドとは異なるスレッドでwait
を実行しようとすると、Plan 9のカーネルはそのwait
要求を無効と判断し、エラーを返したり、あるいは全く関係のないプロセスからの待機メッセージを受け取ってしまったりする可能性がありました。
この問題は、Goプログラムが外部コマンドを実行し、その結果を待機するようなシナリオで顕在化します。例えば、os/exec
パッケージを通じて外部プログラムを起動し、その終了を待つような場合です。安定したプロセス管理は、信頼性の高いアプリケーションを構築する上で不可欠であるため、この競合状態の修正はGoのPlan 9サポートにおいて重要な改善でした。
前提知識の解説
このコミットを理解するためには、以下の概念についての前提知識が必要です。
-
Plan 9 From Bell Labs:
- ベル研究所で開発された分散オペレーティングシステム。Unixの後継として設計され、"Everything is a file"(すべてはファイルである)という哲学を徹底しています。
- プロセス管理: Plan 9のプロセス管理はUnixに似ていますが、いくつかの重要な違いがあります。特に、
wait
システムコールは、子プロセスを生成した親プロセスのみがその子プロセスの終了を待機できるという厳密なセマンティクスを持ちます。これは、子プロセスの状態変更に関するメッセージ(waitmsg
)が、親プロセスの特定の待機キューに送られるためです。 - 名前空間: 各プロセスは独自のファイルシステム名前空間を持ち、リソースはファイルとして表現されます。
-
Go言語のゴルーチンとOSスレッド:
- ゴルーチン (Goroutine): Goランタイムによって管理される軽量な並行実行単位です。OSスレッドよりもはるかに軽量で、数百万個のゴルーチンを同時に実行できます。
- M:Nスケジューリング: Goランタイムは、多数のゴルーチン(M)を少数のOSスレッド(N)にマッピングして実行します。ランタイムスケジューラがゴルーチンをOSスレッド上で実行し、必要に応じて別のOSスレッドに移動させることができます。
runtime.LockOSThread()
: この関数は、現在のゴルーチンを現在のOSスレッドに固定します。これにより、そのゴルーチンは他のOSスレッドに移動することなく、特定のOSスレッド上で実行され続けることが保証されます。これは、特定のOSシステムコールがスレッドアフィニティ(スレッドの固定)を要求する場合に特に重要です。
-
fork-exec-wait
パターン:- Unix系OSやPlan 9における一般的なプロセス起動と待機のパターンです。
fork
: 現在のプロセス(親プロセス)のコピーである新しいプロセス(子プロセス)を作成します。子プロセスは親プロセスのメモリ空間、ファイルディスクリプタなどを継承します。exec
:fork
によって作成された子プロセスが、新しいプログラムをロードして実行します。これにより、子プロセスは全く異なる機能を持つプロセスに変わります。wait
: 親プロセスが子プロセスの終了を待機し、子プロセスの終了ステータスなどの情報を取得します。
-
競合状態 (Race Condition):
- 複数の並行プロセスやスレッドが共有リソースにアクセスする際に、そのアクセス順序によって結果が非決定的に変わってしまう状態を指します。このコミットの場合、
fork
とwait
が異なるOSスレッドで実行される可能性が競合状態を引き起こしていました。
- 複数の並行プロセスやスレッドが共有リソースにアクセスする際に、そのアクセス順序によって結果が非決定的に変わってしまう状態を指します。このコミットの場合、
技術的詳細
このコミットの技術的な核心は、GoのゴルーチンとPlan 9のwait
システムコールの間の不整合を解消することにあります。
元の実装では、Goのos/exec
パッケージが内部的にsyscall.ForkExec
を呼び出して子プロセスを生成し、その後os/Process.wait()
メソッドでその子プロセスの終了を待機していました。問題は、ForkExec
が実行されたゴルーチンが、その後にwait
を実行する際に、Goランタイムのスケジューリングによって異なるOSスレッドに移動してしまう可能性があったことです。
Plan 9のwait
システムコールは、子プロセスを生成した親プロセス(より正確には、その親プロセスが実行されているOSスレッド)が、その子プロセスの終了メッセージを受け取るための待機キューにアクセスできるという制約があります。もしfork
とwait
が異なるOSスレッドで実行されると、wait
を呼び出したOSスレッドは、期待する子プロセスの待機キューにアクセスできず、結果としてエラーになったり、意図しないプロセスからのメッセージを受け取ったりする可能性がありました。
このコミットは、この問題を解決するために以下の戦略を採用しています。
-
OSスレッドへのゴルーチンの固定:
syscall
パッケージにstartProcess
という新しい関数が導入されました。この関数は、子プロセスのfork-exec
とそれに続くwait
処理を単一のゴルーチン内で実行します。- このゴルーチンは、
runtime.LockOSThread()
を呼び出すことで、自身を現在のOSスレッドに固定します。これにより、fork
とwait
の両方のシステムコールが同じOSスレッド上で実行されることが保証され、Plan 9のwait
システムコールの制約を満たします。
-
合成的な
Wait
APIの実装:syscall
パッケージにWaitProcess
という新しい関数が追加されました。これは、os/Process.wait()
から呼び出される新しい待機メカニズムです。startProcess
内で、子プロセスが起動されると、そのPIDと関連付けられたチャネルがグローバルなマップprocs.waits
に登録されます。このチャネルは、子プロセスの終了メッセージ(waitmsg
)を伝達するために使用されます。startProcess
内の固定されたゴルーチンは、子プロセスが終了するまでsyscall.Await
を呼び出して待機します。子プロセスが終了すると、そのwaitmsg
をチャネルに送信します。WaitProcess
は、指定されたPIDに対応するチャネルからwaitmsg
を受け取ることで、子プロセスの終了を待機します。これにより、os/Process.wait()
は直接OSのwait
システムコールを呼び出すのではなく、Goランタイムが管理する内部キューを通じて待機する形になります。
-
PIDの記録とデキュー:
- 子プロセスのPIDは
fork
時に記録され、procs.waits
マップに登録されます。 - 子プロセスの
waitmsg
が読み取られ、チャネルを通じてWaitProcess
に渡されると、そのPIDはprocs.waits
マップから削除(デキュー)されます。これにより、システムが不要な待機情報を保持しないようにします。
- 子プロセスのPIDは
このアプローチにより、GoのランタイムはPlan 9のOSスレッドアフィニティの要件を満たしつつ、Goの並行性モデルを維持することができます。os/exec
パッケージは、この下位レベルの変更を意識することなく、引き続きクロスプラットフォームなプロセス実行APIを提供できます。
コアとなるコードの変更箇所
このコミットで変更された主要なファイルとコードブロックは以下の通りです。
src/pkg/os/exec_plan9.go
Process.wait()
メソッドが大幅に簡素化されました。- 変更前は、
syscall.Await
をループで呼び出し、waitmsg.Pid
が現在のプロセスのPIDと一致するかを確認していました。 - 変更後は、新しく追加された
syscall.WaitProcess(p.Pid, &waitmsg)
を直接呼び出すようになりました。これにより、待機ロジックがsyscall
パッケージに委譲され、より抽象化されました。
--- a/src/pkg/os/exec_plan9.go
+++ b/src/pkg/os/exec_plan9.go
@@ -75,20 +75,12 @@ func (p *Process) wait() (ps *ProcessState, err error) {
if p.Pid == -1 {
return nil, ErrInvalid
}
-
- for true {
- err = syscall.Await(&waitmsg)
-
- if err != nil {
- return nil, NewSyscallError("wait", err)
- }
-
- if waitmsg.Pid == p.Pid {
- p.setDone()
- break
- }
- }
-
+ err = syscall.WaitProcess(p.Pid, &waitmsg)
+ if err != nil {
+ return nil, NewSyscallError("wait", err)
+ }
+
p.setDone()
ps = &ProcessState{
pid: waitmsg.Pid,
src/pkg/syscall/exec_plan9.go
このファイルが変更の大部分を占めており、新しいロジックが追加されています。
procs
構造体:sync.Mutex
とmap[int]chan *waitErr
を含むグローバル変数procs
が追加されました。これは、実行中の子プロセスとその待機チャネルを管理するためのものです。
startProcess
関数:ForkExec
のラッパーとして新しく導入されました。- 内部で新しいゴルーチンを起動し、そのゴルーチン内で
runtime.LockOSThread()
を呼び出してOSスレッドに固定します。 - 固定されたゴルーチン内で実際の
forkExec
を実行し、その結果をforkc
チャネルを通じて親ゴルーチンに返します。 - 子プロセスが正常に起動した場合、そのPIDと関連付けられた
waitc
チャネルをprocs.waits
マップに登録します。 - その後、
syscall.Await
を呼び出して子プロセスの終了を待機し、結果をwaitc
チャネルに送信します。
StartProcess
関数の変更:os
パッケージから呼び出されるsyscall.StartProcess
が、直接forkExec
を呼び出す代わりに、新しく追加されたstartProcess
を呼び出すように変更されました。
WaitProcess
関数:os/Process.wait()
から呼び出される新しい待機関数です。procs.waits
マップから指定されたPIDに対応する待機チャネルを取得します。- チャネルから
waitErr
を受け取り、子プロセスの終了を待ちます。 - 待機メッセージを受け取った後、
procs.waits
マップから該当エントリを削除します。
--- a/src/pkg/syscall/exec_plan9.go
+++ b/src/pkg/syscall/exec_plan9.go
@@ -7,6 +7,7 @@
package syscall
import (
+ "runtime"
"sync"
"unsafe"
)
@@ -499,9 +500,68 @@ func ForkExec(argv0 string, argv []string, attr *ProcAttr) (pid int, err error)
return forkExec(argv0, argv, attr)
}
+type waitErr struct {
+ Waitmsg
+ err error
+}
+
+var procs struct {
+ sync.Mutex
+ waits map[int]chan *waitErr
+}
+
+// startProcess starts a new goroutine, tied to the OS
+// thread, which runs the process and subsequently waits
+// for it to finish, communicating the process stats back
+// to any goroutines that may have been waiting on it.
+//
+// Such a dedicated goroutine is needed because on
+// Plan 9, only the parent thread can wait for a child,
+// whereas goroutines tend to jump OS threads (e.g.,
+// between starting a process and running Wait(), the
+// goroutine may have been rescheduled).
+func startProcess(argv0 string, argv []string, attr *ProcAttr) (pid int, err error) {
+ type forkRet struct {
+ pid int
+ err error
+ }
+
+ forkc := make(chan forkRet, 1)
+ go func() {
+ runtime.LockOSThread()
+ var ret forkRet
+
+ ret.pid, ret.err = forkExec(argv0, argv, attr)
+ // If fork fails there is nothing to wait for.
+ if ret.err != nil || ret.pid == 0 {
+ forkc <- ret
+ return
+ }
+
+ waitc := make(chan *waitErr, 1)
+
+ // Mark that the process is running.
+ procs.Lock()
+ if procs.waits == nil {
+ procs.waits = make(map[int]chan *waitErr)
+ }
+ procs.waits[ret.pid] = waitc
+ procs.Unlock()
+
+ forkc <- ret
+
+ var w waitErr
+ w.err = Await(&w.Waitmsg)
+ waitc <- &w
+ close(waitc)
+ }()
+ ret := <-forkc
+ return ret.pid, ret.err
+}
+
// StartProcess wraps ForkExec for package os.
func StartProcess(argv0 string, argv []string, attr *ProcAttr) (pid int, handle uintptr, err error) {
- pid, err = forkExec(argv0, argv, attr)
+ pid, err = startProcess(argv0, argv, attr)
return pid, 0, err
}
@@ -548,3 +608,35 @@ func Exec(argv0 string, argv []string, envv []string) (err error) {
return e1
}
+
+// WaitProcess waits until the pid of a
+// running process is found in the queue of
+// wait messages. It is used in conjunction
+// with StartProcess to wait for a running
+// process to exit.
+func WaitProcess(pid int, w *Waitmsg) (err error) {
+ procs.Lock()
+ ch := procs.waits[pid]
+ procs.Unlock()
+
+ var wmsg *waitErr
+ if ch != nil {
+ wmsg = <-ch
+ procs.Lock()
+ if procs.waits[pid] == ch {
+ delete(procs.waits, pid)
+ }
+ procs.Unlock()
+ }
+ if wmsg == nil {
+ // ch was missing or ch is closed
+ return NewError("process not found")
+ }
+ if wmsg.err != nil {
+ return wmsg.err
+ }
+ if w != nil {
+ *w = wmsg.Waitmsg
+ }
+ return nil
+}
コアとなるコードの解説
このコミットの核心は、src/pkg/syscall/exec_plan9.go
に追加されたstartProcess
関数とWaitProcess
関数、そしてそれらを連携させるためのprocs
グローバル変数です。
-
procs
グローバル変数:var procs struct { sync.Mutex; waits map[int]chan *waitErr }
- これは、Goランタイム全体で共有されるデータ構造です。
sync.Mutex
は、waits
マップへの並行アクセスを保護するためのミューテックスです。waits map[int]chan *waitErr
は、子プロセスのPIDをキーとし、その子プロセスの終了メッセージを送信するためのチャネルを値とするマップです。これにより、Goランタイムは、どのゴルーチンがどのPIDの終了を待っているかを追跡できます。
-
startProcess
関数:- この関数は、
os.StartProcess
(最終的にはsyscall.StartProcess
)から呼び出されます。 go func() { runtime.LockOSThread(); ... }()
: ここが最も重要な部分です。新しいゴルーチンを起動し、そのゴルーチン内でruntime.LockOSThread()
を呼び出します。これにより、このゴルーチンは、子プロセスをfork-exec
し、その終了をAwait
するまで、同じOSスレッドに固定されます。Plan 9のwait
システムコールの制約を満たすために不可欠な処理です。ret.pid, ret.err = forkExec(argv0, argv, attr)
: 固定されたOSスレッド上で、実際の子プロセス生成(fork-exec
)が行われます。procs.Lock(); procs.waits[ret.pid] = waitc; procs.Unlock()
: 子プロセスが正常に起動した場合、そのPIDと、子プロセスの終了メッセージを送信するための新しいチャネルwaitc
をprocs.waits
マップに登録します。w.err = Await(&w.Waitmsg); waitc <- &w; close(waitc)
: 固定されたゴルーチンは、syscall.Await
を呼び出して子プロセスの終了を待ちます。Await
はPlan 9のネイティブな待機システムコールをラップしたものです。子プロセスが終了すると、そのWaitmsg
とエラー情報を含むwaitErr
構造体をwaitc
チャネルに送信し、チャネルを閉じます。
- この関数は、
-
WaitProcess
関数:- この関数は、
os/Process.wait()
から呼び出され、特定のPIDの子プロセスの終了を待機します。 procs.Lock(); ch := procs.waits[pid]; procs.Unlock()
:procs.waits
マップから、待機したいPIDに対応するチャネルch
を取得します。wmsg = <-ch
:ch
チャネルからwaitErr
構造体を受け取ります。これにより、WaitProcess
を呼び出したゴルーチンは、startProcess
内の固定されたゴルーチンが子プロセスの終了を検知し、メッセージを送信するまでブロックされます。delete(procs.waits, pid)
: 待機メッセージを受け取った後、procs.waits
マップから該当エントリを削除します。これにより、リソースが解放され、不要な情報が保持されなくなります。
- この関数は、
このメカニズムにより、GoのゴルーチンがOSスレッド間で自由に移動できるという特性を維持しつつ、Plan 9のOSスレッドアフィニティの要件を満たすことが可能になりました。fork
とwait
の間の競合状態は、startProcess
内の単一のOSスレッド固定ゴルーチンが両方の操作を処理し、その結果を内部チャネルを通じて他のゴルーチンに安全に伝達することで解消されています。
関連リンク
- Go言語の公式ドキュメント: https://go.dev/
- Plan 9 From Bell Labs: https://9p.io/plan9/
- Goの
os/exec
パッケージ: https://pkg.go.dev/os/exec - Goの
syscall
パッケージ: https://pkg.go.dev/syscall - Goの
runtime.LockOSThread
に関するドキュメント: https://pkg.go.dev/runtime#LockOSThread
参考にした情報源リンク
- Goのコミットレビューページ: https://golang.org/cl/6545051 (コミットメッセージに記載されているリンク)
- Plan 9の
wait
システムコールに関する情報 (一般的なOSのドキュメントや論文) - Goのスケジューラに関する情報 (Goの公式ブログや設計ドキュメント)
- 競合状態に関する一般的なプログラミングの概念
- Unix系OSにおける
fork
,exec
,wait
の動作に関する情報