[インデックス 1680] ファイルの概要
このコミットは、Go言語の初期ランタイムにおいて、外部プロセスの実行と管理のための基本的な機能、具体的には os.ForkExec
、os.Exec
、os.Wait
、および exec.OpenCmd
を追加するものです。特に、fork
/exec
システムコールが関わる際のファイルディスクリプタの継承に関するスレッドセーフティの問題に対処するための ForkLock
というRWMutexのスタブ実装も導入されています。これにより、Goプログラムから外部コマンドを安全かつ効率的に実行するための基盤が築かれました。
コミット
commit 91ceda5c18fdf7c7512b0a36725d9d5cf1c2b23f
Author: Russ Cox <rsc@golang.org>
Date: Sun Feb 15 19:35:52 2009 -0800
add os.ForkExec, os.Exec, os.Wait, exec.OpenCmd.
as thread-safe as possible, given the surrounding system.
add stub RWLock implementation.
R=r
DELTA=852 (834 added, 6 deleted, 12 changed)
OCL=25046
CL=25053
GitHub上でのコミットページへのリンク
https://github.com/golang/go/commit/91ceda5c18fdf7c7512b0a36725d9d5cf1c2b23f
元コミット内容
os.ForkExec
、os.Exec
、os.Wait
、exec.OpenCmd
を追加。
周囲のシステムを考慮し、可能な限りスレッドセーフに。
スタブの RWLock
実装を追加。
変更の背景
このコミットが行われた2009年2月は、Go言語がまだ公開されて間もない、非常に初期の段階でした。当時のGoは、システムプログラミング言語としての地位を確立しようとしており、そのために外部プロセスとの連携は不可欠な機能でした。Unix系のシステムでは、新しいプロセスを生成するために fork
と exec
という2つのシステムコールを組み合わせて使用するのが一般的です。
しかし、fork
と exec
をマルチスレッド環境で安全に使用することは、特にファイルディスクリプタの継承に関して複雑な問題を引き起こします。fork
は親プロセスのメモリ空間とファイルディスクリプタを子プロセスに複製しますが、この際に意図しないファイルディスクリプタ(例えば、他のゴルーチンが開いているソケットやパイプの書き込み側など)が子プロセスに継承されてしまうと、様々な問題が発生する可能性があります。例えば、親プロセスがパイプの読み込み側を閉じても、子プロセスが書き込み側を保持していると、読み込み側はEOFを受け取らず、親プロセスがハングアップする可能性があります。
このコミットの主な目的は、Goプログラムが外部コマンドを起動し、その入出力を制御し、終了を待機するための堅牢でスレッドセーフなメカニズムを提供することでした。特に、fork
と exec
の間の競合状態を回避し、子プロセスが意図したファイルディスクリプタのみを継承するようにするための対策が求められていました。
前提知識の解説
1. fork()
システムコール
fork()
は、現在のプロセス(親プロセス)のほぼ完全なコピーである新しいプロセス(子プロセス)を作成するUnix系システムコールです。子プロセスは親プロセスのメモリ空間、開いているファイルディスクリプタ、シグナルハンドラなどを継承します。fork()
は親プロセスでは子プロセスのPIDを返し、子プロセスでは0を返します。
2. exec()
システムコール群
exec()
システムコール群(execve
, execl
, execv
など)は、現在のプロセスイメージを新しいプログラムイメージで置き換えます。つまり、exec()
が成功すると、現在のプロセスは新しいプログラムを実行し始め、元のプログラムは終了します。PIDは変更されません。通常、fork()
の後に exec()
が呼び出され、新しいプログラムを実行する子プロセスを作成します。
3. wait()
/ waitpid()
システムコール
wait()
や waitpid()
は、親プロセスが子プロセスの終了を待機するためのシステムコールです。子プロセスが終了すると、その終了ステータスを親プロセスに通知し、子プロセスは「ゾンビプロセス」状態から完全に消滅します。これにより、システムリソースのリークを防ぎます。
4. ファイルディスクリプタ (File Descriptor, FD)
ファイルディスクリプタは、Unix系システムにおいてファイルやI/Oリソース(ソケット、パイプなど)を識別するために使用される非負の整数です。プロセスは、開いているファイルディスクリプタのテーブルを保持しています。
5. O_CLOEXEC
フラグ
O_CLOEXEC
(Close-on-exec) は、ファイルを開く際に指定できるフラグです。このフラグが設定されたファイルディスクリプタは、exec()
システムコールが成功した際に自動的に閉じられます。これは、子プロセスに意図しないファイルディスクリプタが継承されるのを防ぐための重要なメカニズムです。しかし、open()
と fcntl(F_SETFD, FD_CLOEXEC)
の間に fork()
が発生すると、競合状態が発生し、子プロセスが O_CLOEXEC
が設定される前のファイルディスクリプタを継承してしまう可能性があります。
6. sync.RWMutex
(Read-Write Mutex)
sync.RWMutex
は、Go言語の標準ライブラリ sync
パッケージで提供される読み書きロックです。複数の読み取り操作は同時に許可されますが、書き込み操作は排他的に行われます。このコミットでは、まだスタブ実装ですが、fork
処理中のファイルディスクリプタの安全な管理のために導入されています。
7. スレッドセーフティ
マルチスレッド環境において、複数のスレッドが同時にアクセスしても、データ構造やプログラムの動作が正しく保たれることを指します。fork
/exec
の文脈では、他のゴルーチン(Goの軽量スレッド)がファイルディスクリプタを操作している最中に fork
が発生しても、子プロセスが安全な状態を保つことが重要です。
技術的詳細
このコミットの核心は、fork
/exec
のスレッドセーフな実行を保証するための syscall.ForkLock
の導入と、forkAndExecInChild
関数の実装です。
syscall.ForkLock
syscall.ForkLock
は sync.RWMutex
型のグローバル変数です。このロックは、新しいファイルディスクリプタの作成と fork
システムコールの実行を同期するために使用されます。
- 読み取りロック (RLock): 新しいファイルディスクリプタを作成するシステムコール(
Pipe
,Socket
,Accept
(非ブロッキングモード),Dup
)は、ForkLock
を読み取りモードで取得します。これにより、複数のファイルディスクリプタ作成操作が同時に行われることを許可しつつ、fork
が発生するのを防ぎます。 - 書き込みロック (Lock):
fork
システムコールを呼び出す直前には、ForkLock
を書き込みモードで取得します。これにより、fork
実行中に他のファイルディスクリプタ作成操作が割り込むのを防ぎます。
このメカニズムの目的は、ファイルディスクリプタが作成されてから O_CLOEXEC
フラグが設定されるまでの短い期間に fork
が発生し、意図しないファイルディスクリプタが子プロセスに継承されるという競合状態を回避することです。
ただし、このコミットのコメントにもあるように、open
のようなブロッキングする可能性のあるシステムコールでは ForkLock
を取得することは現実的ではありません。そのため、Linuxでは O_CLOEXEC
フラグを open
に直接渡すことでこの問題を回避し、それ以外の場合は競合状態を許容するというトレードオフがなされています。
syscall.forkAndExecInChild
関数
この関数は、fork
後の子プロセス内で実行されるロジックをカプセル化しています。この関数は、fork
後の子プロセスでは、親プロセスのロック状態を継承している可能性があるため、いかなるロックも取得せず、メモリ割り当ても行わず、スケジューリングも発生させないという非常に厳しい制約の下で動作します。これは、fork
後の子プロセスが exec
を呼び出すまでの間、親プロセスのロック状態を引き継いでいる可能性があり、デッドロックを避けるためです。
この関数は以下の主要なステップを実行します。
RawSyscall(SYS_FORK, ...)
: 実際のfork
システムコールを呼び出します。- ファイルディスクリプタの整理:
- パス1:
fd[i] < i
となるファイルディスクリプタ(つまり、新しいFDが既存のFD番号よりも小さい場合)を、len(fd)
よりも大きい一時的なFD番号にDUP2
します。これは、後続のパス2でFD番号をi
にDUP2
する際に、まだ必要なFDを上書きしてしまうのを防ぐためです。一時的なFDにはFD_CLOEXEC
フラグを設定します。 - パス2:
fd[i]
をi
にDUP2
します。これにより、子プロセスは指定されたFDを正しい番号で継承します。この際、FD_CLOEXEC
フラグをクリアします。 - 不要なFDのクローズ:
len(fd)
よりも大きいFD(ただし、標準入力/出力/エラーを除く)を閉じます。
- パス1:
RawSyscall(SYS_EXECVE, ...)
: 新しいプログラムをロードして実行します。- エラーハンドリング:
exec
が失敗した場合、エラーコードをパイプ(親プロセスとの通信用)に書き込み、子プロセスはSYS_EXIT
で終了します。
os.ForkExec
と exec.OpenCmd
os.ForkExec
:syscall.ForkExec
のラッパーで、Goのos.Error
型に変換して返します。exec.OpenCmd
:os.ForkExec
を利用して、外部コマンドを起動し、その標準入出力と標準エラー出力をGoのos.FD
オブジェクトとして提供します。これにより、Goプログラムから外部プロセスのI/Oを簡単に制御できるようになります。DevNull
,Passthru
,Pipe
,MergeWithStdout
といったモードを定義し、入出力のリダイレクトを柔軟に設定できます。
os.Wait
os.Wait
は、指定されたPIDの子プロセスの終了を待機し、そのステータスを os.Waitmsg
構造体で返します。syscall.Wait4
を内部的に使用し、rusage
(リソース使用量) の情報も取得できるように設計されています。
sync.RWMutex
のスタブ実装
このコミットでは、sync.RWMutex
がまだ本格的に実装されておらず、RLock
と RUnlock
が単に Mutex
の Lock
と Unlock
を呼び出すだけのスタブ実装となっています。これは、機能のプロトタイプを先に導入し、後でより効率的な実装に置き換えるという開発アプローチを示しています。
コアとなるコードの変更箇所
src/lib/exec.go
(新規ファイル)
package exec
import (
"os";
"syscall";
)
const (
DevNull = iota;
Passthru;
Pipe;
MergeWithStdout;
)
type Cmd struct {
Stdin *os.FD;
Stdout *os.FD;
Stderr *os.FD;
Pid int;
}
func OpenCmd(argv0 string, argv, envv []string, stdin, stdout, stderr int) (p *Cmd, err *os.Error) {
// ... (入出力FDの準備ロジック) ...
p.Pid, err = os.ForkExec(argv0, argv, envv, fd);
// ... (FDのクローズロジック) ...
return p, nil;
}
func (p *Cmd) Wait(options uint64) (*os.Waitmsg, *os.Error) {
// ... (os.Waitの呼び出し) ...
}
func (p *Cmd) Close() *os.Error {
// ... (FDのクローズロジック) ...
}
src/lib/os/exec.go
(新規ファイル)
package os
import (
"os";
"syscall";
)
func ForkExec(argv0 string, argv []string, envv []string, fd []*FD) (pid int, err *Error) {
// ... (FDをint64に変換) ...
p, e := syscall.ForkExec(argv0, argv, envv, intfd);
return int(p), ErrnoToError(e);
}
func Exec(argv0 string, argv []string, envv []string) *Error {
e := syscall.Exec(argv0, argv, envv);
return ErrnoToError(e);
}
type Waitmsg struct {
Pid int;
syscall.WaitStatus;
Rusage *syscall.Rusage;
}
func Wait(pid int, options uint64) (w *Waitmsg, err *Error) {
// ... (syscall.Wait4の呼び出し) ...
}
src/lib/syscall/exec.go
(新規ファイル)
package syscall
import (
"sync";
"syscall";
"unsafe";
)
var ForkLock sync.RWMutex
func CloseOnExec(fd int64) {
Fcntl(fd, F_SETFD, FD_CLOEXEC);
}
func forkAndExecInChild(argv0 *byte, argv []*byte, envv []*byte, fd []int64, pipe int64) (pid int64, err int64) {
// ... (forkシステムコール呼び出し) ...
// ... (子プロセスでのFD整理ロジック) ...
// ... (execveシステムコール呼び出し) ...
// ... (エラーハンドリング) ...
}
func ForkExec(argv0 string, argv []string, envv []string, fd []int64) (pid int64, err int64) {
// ... (パイプの準備) ...
ForkLock.Lock(); // ForkLockの取得
pid, err = forkAndExecInChild(argv0p, argvp, envvp, fd, p[1]);
ForkLock.Unlock(); // ForkLockの解放
// ... (子プロセスからのエラー読み取り) ...
}
func Exec(argv0 string, argv []string, envv []string) (err int64) {
// ... (execveシステムコール呼び出し) ...
}
src/lib/sync/mutex.go
(変更)
type RWMutex struct {
Mutex;
}
func (m *RWMutex) RLock() {
m.Lock(); // スタブ実装
}
func (m *RWMutex) RUnlock() {
m.Unlock(); // スタブ実装
}
コアとなるコードの解説
src/lib/exec.go
このファイルは、Goプログラムから外部コマンドを実行するための高レベルなインターフェース exec.Cmd
を定義しています。
OpenCmd
関数は、実行するコマンドのパス、引数、環境変数、そして標準入出力・エラー出力のリダイレクト方法(DevNull
, Passthru
, Pipe
, MergeWithStdout
)を受け取ります。内部的には os.ForkExec
を呼び出してプロセスを起動し、そのプロセスIDと入出力用の os.FD
を Cmd
構造体に格納して返します。
Cmd.Wait
メソッドは、起動したプロセスの終了を待機し、その終了ステータスを返します。
Cmd.Close
メソッドは、関連するファイルディスクリプタを閉じます。
src/lib/os/exec.go
このファイルは、exec
パッケージが利用する低レベルなOSレベルのプロセス管理機能を提供します。
ForkExec
関数は、syscall.ForkExec
のラッパーであり、Goの os.Error
型にエラーを変換します。
Exec
関数は、現在のプロセスを新しいプログラムで置き換える syscall.Exec
のラッパーです。
Wait
関数は、syscall.Wait4
を利用して子プロセスの終了を待機し、Waitmsg
構造体で詳細な終了ステータス(PID、終了ステータス、リソース使用量など)を返します。
src/lib/syscall/exec.go
このファイルは、fork
/exec
/wait
のシステムコールを直接ラップし、特にマルチスレッド環境での安全性を確保するためのロジックを含んでいます。
ForkLock
は、前述の通り、fork
処理中のファイルディスクリプタの競合状態を回避するための重要なメカニズムです。
forkAndExecInChild
関数は、fork
後の子プロセスで実行される非常にデリケートな部分です。この関数は、親プロセスのロック状態に影響されないように、メモリ割り当てやスケジューリングを伴う操作を避けています。主な役割は、子プロセスが継承すべきファイルディスクリプタを適切に設定し、不要なものを閉じ、最終的に execve
システムコールを呼び出して新しいプログラムを実行することです。DUP2
システムコールを巧みに利用して、ファイルディスクリプタの番号を調整し、O_CLOEXEC
フラグを適切に設定/解除しています。
ForkExec
関数は、ForkLock
を取得した上で forkAndExecInChild
を呼び出し、子プロセスからのエラーをパイプ経由で受け取ります。
src/lib/sync/mutex.go
このコミット時点では、sync.RWMutex
はまだ本格的な実装ではなく、RLock
と RUnlock
が単に基底の Mutex
の Lock
と Unlock
を呼び出すだけのスタブとなっています。これは、読み取りロックと書き込みロックのセマンティクスは満たすものの、読み取りの並行性を活用できない非効率な実装であることを意味します。これは、機能のプロトタイプを先に導入し、後でパフォーマンスを最適化する一般的な開発手法です。
関連リンク
- Go言語の
os/exec
パッケージの現在のドキュメント: https://pkg.go.dev/os/exec - Go言語の
syscall
パッケージの現在のドキュメント: https://pkg.go.dev/syscall - Go言語の
sync
パッケージの現在のドキュメント: https://pkg.go.dev/sync
参考にした情報源リンク
fork(2)
man page (Linux): https://man7.org/linux/man-pages/man2/fork.2.htmlexecve(2)
man page (Linux): https://man7.org/linux/man-pages/man2/execve.2.htmlwait(2)
man page (Linux): https://man7.org/linux/man-pages/man2/wait.2.htmlfcntl(2)
man page (Linux): https://man7.org/linux/man-pages/man2/fcntl.2.htmlO_CLOEXEC
の重要性に関する記事 (例: "The trouble withfork()
"): https://lwn.net/Articles/542629/ (LWN.netの記事は有料購読が必要な場合がありますが、概念は広く議論されています)- Go言語の初期の設計に関する議論やメーリングリストのアーカイブ (Goの公式リポジトリのコミット履歴や関連するIssue/CLを参照)
- Unix/Linuxプログラミングに関する書籍やオンラインリソース (例: "Advanced Programming in the UNIX Environment" by W. Richard Stevens)