[インデックス 14973] ファイルの概要
このコミットは、Go言語のsyscall
パッケージにおけるPlan 9オペレーティングシステム向けのfork-exec/wait
処理の不整合(レースコンディション)を修正するものです。具体的には、ForkExec
関数がstartProcess
関数を使用するように変更し、startProcess
およびWaitProcess
におけるWaitmsg
データの受け渡しが、ForkExec
やStartProcess
以外の場所からのフォークによって引き起こされる可能性のある干渉から保護されるように改善されています。
コミット
commit 7f0d1652a46df1d15d7e80fb34f88b5ebb4ff5f6
Author: Akshat Kumar <seed@mail.nanosouffle.net>
Date: Tue Jan 22 22:42:44 2013 -0500
syscall: fix fork-exec/wait inconsistencies for Plan 9
Fixes the fork-exec/wait race condition for ForkExec
as well, by making it use startProcess. This makes the
comment for StartProcess consistent as well.
Further, the passing of Waitmsg data in startProcess
and WaitProcess is protected against possible forks
from outside of ForkExec and StartProcess, which might
cause interference with the Await call.
R=rsc, rminnich, npe, ality
CC=golang-dev
https://golang.org/cl/7128059
GitHub上でのコミットページへのリンク
https://github.com/golang/go/commit/7f0d1652a46df1d15d7e80fb34f88b5ebb4ff5f6
元コミット内容
syscall: fix fork-exec/wait inconsistencies for Plan 9
ForkExec
のfork-exec/wait
レースコンディションを修正し、startProcess
を使用するようにします。これにより、StartProcess
のコメントも一貫性を持つようになります。
さらに、startProcess
およびWaitProcess
におけるWaitmsg
データの受け渡しは、ForkExec
およびStartProcess
以外の場所からのフォークによって引き起こされる可能性のある干渉から保護されます。
変更の背景
このコミットの主な目的は、Go言語のsyscall
パッケージがPlan 9オペレーティングシステム上でプロセスを生成し、その終了を待機する際の既知のレースコンディションを解決することです。特に、ForkExec
関数とstartProcess
関数の間で発生する可能性のあるfork-exec/wait
の不整合が問題でした。
従来のForkExec
の実装では、startProcess
とは異なるロジックでプロセスを生成・管理しており、これがwait
メッセージの処理において競合状態を引き起こす可能性がありました。具体的には、子プロセスが終了した際に親プロセスがその終了ステータス(Waitmsg
)を適切に取得できない、あるいは誤ったWaitmsg
を取得してしまうといった問題が発生し得ました。これは、複数のプロセスが同時にAwait
システムコールを呼び出す可能性があり、どのWaitmsg
がどのプロセスに対応するのかを正確に判断するのが困難であったためです。
また、ForkExec
やStartProcess
といったGoのAPIを介さずに、外部から直接fork
システムコールが呼び出された場合、その子プロセスからのWaitmsg
が、Goの内部的なAwait
呼び出しと干渉し、予期せぬ動作を引き起こす可能性も考慮されていました。このコミットは、これらの問題を解決し、Plan 9上でのプロセス管理の堅牢性と正確性を向上させることを目指しています。
前提知識の解説
Plan 9 from Bell Labs
Plan 9は、ベル研究所で開発された分散オペレーティングシステムです。Unixの設計思想をさらに推し進め、すべてのリソース(ファイル、デバイス、ネットワーク接続など)をファイルシステムとして表現するという「すべてはファイルである」という原則を徹底しています。プロセス間通信やネットワーク通信もファイル操作として抽象化されており、これによりシステム全体の一貫性とシンプルさが実現されています。Go言語の開発者の一部はPlan 9の開発にも携わっており、Go言語の設計思想にはPlan 9の影響が色濃く見られます。
fork, exec, wait システムコール
Unix系OS(およびPlan 9も同様の概念を持つ)におけるプロセス生成と実行の基本的なメカニズムです。
fork
: 既存のプロセス(親プロセス)の複製(子プロセス)を作成します。子プロセスは親プロセスのメモリ空間、ファイルディスクリプタなどを継承しますが、それぞれ独立したプロセスとして実行されます。fork
は、新しいプロセスを作成する唯一の方法です。exec
: 現在のプロセスイメージを、指定された新しいプログラムで置き換えます。exec
が成功すると、現在のプロセスのコードとデータは新しいプログラムのものに上書きされ、新しいプログラムが実行を開始します。プロセスIDは変更されません。通常、fork
で子プロセスを作成した後、その子プロセス内でexec
を呼び出して別のプログラムを実行します。wait
(またはawait
in Plan 9): 親プロセスが子プロセスの終了を待機するためのシステムコールです。子プロセスが終了すると、その終了ステータスなどの情報(Plan 9ではWaitmsg
)が親プロセスに返されます。これにより、親プロセスは子プロセスの実行結果を知ることができます。
レースコンディション (Race Condition)
複数のプロセスやスレッドが共有リソースに同時にアクセスしようとした際に、そのアクセス順序によって結果が非決定的に変化してしまう状態を指します。このコミットの文脈では、子プロセスが終了してWaitmsg
が生成されるタイミングと、親プロセスがAwait
を呼び出すタイミング、そして複数の子プロセスが存在する場合にどのWaitmsg
がどのプロセスに対応するのかを識別するロジックの間でレースコンディションが発生していました。
Go言語のsyscall
パッケージ
Go言語のsyscall
パッケージは、オペレーティングシステムの低レベルなシステムコールへのインターフェースを提供します。これにより、GoプログラムからOSの機能(ファイル操作、プロセス管理、ネットワーク通信など)を直接呼び出すことができます。このコミットで扱われているForkExec
やStartProcess
は、Goプログラムから新しいプロセスを生成し、実行するための高レベルなラッパー関数です。
ForkExec
:fork
とexec
の組み合わせを抽象化した関数で、新しいプロセスを生成し、指定されたプログラムを実行します。StartProcess
:os
パッケージから呼び出される、より高レベルなプロセス開始関数で、内部的にForkExec
またはそれに類する機能を使用します。Await
: Plan 9におけるwait
システムコールに相当するもので、子プロセスの終了を待ち、そのWaitmsg
(終了メッセージ)を取得します。Waitmsg
: Plan 9において、子プロセスの終了に関する情報(プロセスID、終了ステータスなど)を含むデータ構造です。
技術的詳細
このコミットは、Plan 9におけるfork-exec/wait
の不整合を、主に以下の2つのアプローチで解決しています。
-
ForkExec
のstartProcess
への統合: 以前のForkExec
関数は、forkExec
という内部関数を直接呼び出していました。しかし、startProcess
関数は、プロセス生成後のWaitmsg
の処理に関してより堅牢なロジックを持っていました。このコミットでは、ForkExec
が直接startProcess
を呼び出すように変更されました。これにより、ForkExec
とStartProcess
の両方が同じ、より信頼性の高いプロセス開始およびWaitmsg
処理メカニズムを共有することになり、コードの一貫性が保たれ、レースコンディションの可能性が減少しました。 -
Await
呼び出しの堅牢化とWaitmsg
の保護:startProcess
関数内のゴルーチンでAwait
を呼び出す部分に重要な変更が加えられました。以前は、Await
を一度呼び出し、その結果をwaitc
チャネルに送信していました。しかし、これでは、Await
が返したWaitmsg
が、startProcess
によって生成された子プロセスのものであることを保証できませんでした。特に、ForkExec
やStartProcess
を介さずに外部からfork
されたプロセスが終了した場合、そのWaitmsg
が誤って取得されてしまう可能性がありました。新しい実装では、
Await
呼び出しがループ内に配置され、w.err == nil && w.Pid != ret.pid
という条件が満たされるまでAwait
を繰り返し呼び出すようになりました。w.err == nil
:Await
がエラーなく成功したことを確認します。w.Pid != ret.pid
: 取得したWaitmsg
のプロセスID (w.Pid
) が、現在startProcess
が待機している子プロセスのID (ret.pid
) と異なる場合、それは目的の子プロセスのものではないと判断し、再度Await
を呼び出します。
このループにより、
startProcess
は、自身が生成した子プロセスに対応するWaitmsg
が取得されるまでAwait
を継続的に呼び出すことが保証されます。これにより、外部からのフォークや、他のプロセスからのWaitmsg
が誤って処理されることを防ぎ、Waitmsg
データの受け渡しにおける干渉を排除します。
これらの変更により、Goのsyscall
パッケージはPlan 9上でより安定してプロセスを管理できるようになり、特に並行処理環境下でのfork-exec/wait
の信頼性が向上しました。
コアとなるコードの変更箇所
src/pkg/syscall/exec_plan9.go
ファイルにおいて、以下の変更が行われました。
--- a/src/pkg/syscall/exec_plan9.go
+++ b/src/pkg/syscall/exec_plan9.go
@@ -495,11 +495,6 @@ func forkExec(argv0 string, argv []string, attr *ProcAttr) (pid int, err error)\
return pid, nil
}
-// Combination of fork and exec, careful to be thread safe.
-func ForkExec(argv0 string, argv []string, attr *ProcAttr) (pid int, err error) {
- return forkExec(argv0, argv, attr)
-}
-
type waitErr struct {
Waitmsg
err error
@@ -551,7 +546,9 @@ func startProcess(argv0 string, argv []string, attr *ProcAttr) (pid int, err err
forkc <- ret
var w waitErr
- w.err = Await(&w.Waitmsg)
+ for w.err == nil && w.Pid != ret.pid {
+ w.err = Await(&w.Waitmsg)
+ }
waitc <- &w
close(waitc)
}()
@@ -559,6 +556,11 @@ func startProcess(argv0 string, argv []string, attr *ProcAttr) (pid int, err err
return ret.pid, ret.err
}
+// Combination of fork and exec, careful to be thread safe.
+func ForkExec(argv0 string, argv []string, attr *ProcAttr) (pid int, err error) {
+ return startProcess(argv0, argv, attr)
+}
+
// StartProcess wraps ForkExec for package os.
func StartProcess(argv0 string, argv []string, attr *ProcAttr) (pid int, handle uintptr, err error) {
pid, err = startProcess(argv0, argv, attr)
@@ -612,8 +614,8 @@ func Exec(argv0 string, argv []string, envv []string) (err error) {
// 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.\n
+// with ForkExec/StartProcess to wait for a
+// running process to exit.
func WaitProcess(pid int, w *Waitmsg) (err error) {
procs.Lock()
ch := procs.waits[pid]
コアとなるコードの解説
-
ForkExec
関数の変更:- 元の
ForkExec
関数(497-499行目)が削除され、新しいForkExec
関数(559-561行目)が追加されました。 - 新しい
ForkExec
関数は、以前のようにforkExec
を直接呼び出すのではなく、startProcess
関数を呼び出すように変更されました。 - これにより、
ForkExec
とStartProcess
(564行目でstartProcess
を呼び出している)の両方が、プロセス生成とWaitmsg
処理の共通かつ堅牢なロジックを使用するようになります。これは、コードの重複を避け、一貫性を保つ上で重要です。
- 元の
-
startProcess
内のAwait
呼び出しのループ化:startProcess
関数内のゴルーチン(551行目から始まる)において、Await
システムコールの呼び出し方法が変更されました。- 変更前:
w.err = Await(&w.Waitmsg)
- 変更後:
for w.err == nil && w.Pid != ret.pid { w.err = Await(&w.Waitmsg) }
- この
for
ループは、Await
がエラーを返さず (w.err == nil
)、かつ取得したWaitmsg
のプロセスID (w.Pid
) が、現在startProcess
が待機している子プロセスのID (ret.pid
) と一致しない限り、Await
を繰り返し呼び出します。 - これにより、
startProcess
は、自身が生成した子プロセスに対応するWaitmsg
が確実に取得されるまで待機し、他のプロセスからのWaitmsg
による干渉を防ぎます。これは、fork-exec/wait
のレースコンディションを解決するための核心的な変更です。
-
WaitProcess
のコメント修正:WaitProcess
関数のコメントが修正されました。- 変更前:
// with StartProcess to wait for a running // process to exit.
- 変更後:
// with ForkExec/StartProcess to wait for a // running process to exit.
- これは、
ForkExec
もstartProcess
を使用するようになったため、WaitProcess
がForkExec
とStartProcess
の両方と連携して動作することを明確にするためのドキュメントの更新です。
これらの変更は、Plan 9環境におけるGoのプロセス管理の信頼性と正確性を大幅に向上させます。
関連リンク
- Go CL (Code Review) 7128059: https://golang.org/cl/7128059
参考にした情報源リンク
- Plan 9 from Bell Labs: https://9p.io/plan9/
- Unix
fork
,exec
,wait
man pages (一般的な概念理解のため): - Go
syscall
package documentation: https://pkg.go.dev/syscall - Go
os/exec
package documentation (関連する高レベルAPI): https://pkg.go.dev/os/exec - Race condition (Wikipedia): https://en.wikipedia.org/wiki/Race_condition