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

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

このコミットは、Go言語のsyscallパッケージにおけるPlan 9オペレーティングシステム向けのfork-exec/wait処理の不整合(レースコンディション)を修正するものです。具体的には、ForkExec関数がstartProcess関数を使用するように変更し、startProcessおよびWaitProcessにおけるWaitmsgデータの受け渡しが、ForkExecStartProcess以外の場所からのフォークによって引き起こされる可能性のある干渉から保護されるように改善されています。

コミット

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

ForkExecfork-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がどのプロセスに対応するのかを正確に判断するのが困難であったためです。

また、ForkExecStartProcessといった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の機能(ファイル操作、プロセス管理、ネットワーク通信など)を直接呼び出すことができます。このコミットで扱われているForkExecStartProcessは、Goプログラムから新しいプロセスを生成し、実行するための高レベルなラッパー関数です。

  • ForkExec: forkexecの組み合わせを抽象化した関数で、新しいプロセスを生成し、指定されたプログラムを実行します。
  • StartProcess: osパッケージから呼び出される、より高レベルなプロセス開始関数で、内部的にForkExecまたはそれに類する機能を使用します。
  • Await: Plan 9におけるwaitシステムコールに相当するもので、子プロセスの終了を待ち、そのWaitmsg(終了メッセージ)を取得します。
  • Waitmsg: Plan 9において、子プロセスの終了に関する情報(プロセスID、終了ステータスなど)を含むデータ構造です。

技術的詳細

このコミットは、Plan 9におけるfork-exec/waitの不整合を、主に以下の2つのアプローチで解決しています。

  1. ForkExecstartProcessへの統合: 以前のForkExec関数は、forkExecという内部関数を直接呼び出していました。しかし、startProcess関数は、プロセス生成後のWaitmsgの処理に関してより堅牢なロジックを持っていました。このコミットでは、ForkExecが直接startProcessを呼び出すように変更されました。これにより、ForkExecStartProcessの両方が同じ、より信頼性の高いプロセス開始およびWaitmsg処理メカニズムを共有することになり、コードの一貫性が保たれ、レースコンディションの可能性が減少しました。

  2. Await呼び出しの堅牢化とWaitmsgの保護: startProcess関数内のゴルーチンでAwaitを呼び出す部分に重要な変更が加えられました。以前は、Awaitを一度呼び出し、その結果をwaitcチャネルに送信していました。しかし、これでは、Awaitが返したWaitmsgが、startProcessによって生成された子プロセスのものであることを保証できませんでした。特に、ForkExecStartProcessを介さずに外部から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]

コアとなるコードの解説

  1. ForkExec関数の変更:

    • 元のForkExec関数(497-499行目)が削除され、新しいForkExec関数(559-561行目)が追加されました。
    • 新しいForkExec関数は、以前のようにforkExecを直接呼び出すのではなく、startProcess関数を呼び出すように変更されました。
    • これにより、ForkExecStartProcess(564行目でstartProcessを呼び出している)の両方が、プロセス生成とWaitmsg処理の共通かつ堅牢なロジックを使用するようになります。これは、コードの重複を避け、一貫性を保つ上で重要です。
  2. 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のレースコンディションを解決するための核心的な変更です。
  3. WaitProcessのコメント修正:

    • WaitProcess関数のコメントが修正されました。
    • 変更前: // with StartProcess to wait for a running // process to exit.
    • 変更後: // with ForkExec/StartProcess to wait for a // running process to exit.
    • これは、ForkExecstartProcessを使用するようになったため、WaitProcessForkExecStartProcessの両方と連携して動作することを明確にするためのドキュメントの更新です。

これらの変更は、Plan 9環境におけるGoのプロセス管理の信頼性と正確性を大幅に向上させます。

関連リンク

参考にした情報源リンク