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

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

このコミットは、Go言語の初期ランタイムにおいて、外部プロセスの実行と管理のための基本的な機能、具体的には os.ForkExecos.Execos.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.ForkExecos.Execos.Waitexec.OpenCmd を追加。 周囲のシステムを考慮し、可能な限りスレッドセーフに。 スタブの RWLock 実装を追加。

変更の背景

このコミットが行われた2009年2月は、Go言語がまだ公開されて間もない、非常に初期の段階でした。当時のGoは、システムプログラミング言語としての地位を確立しようとしており、そのために外部プロセスとの連携は不可欠な機能でした。Unix系のシステムでは、新しいプロセスを生成するために forkexec という2つのシステムコールを組み合わせて使用するのが一般的です。

しかし、forkexec をマルチスレッド環境で安全に使用することは、特にファイルディスクリプタの継承に関して複雑な問題を引き起こします。fork は親プロセスのメモリ空間とファイルディスクリプタを子プロセスに複製しますが、この際に意図しないファイルディスクリプタ(例えば、他のゴルーチンが開いているソケットやパイプの書き込み側など)が子プロセスに継承されてしまうと、様々な問題が発生する可能性があります。例えば、親プロセスがパイプの読み込み側を閉じても、子プロセスが書き込み側を保持していると、読み込み側はEOFを受け取らず、親プロセスがハングアップする可能性があります。

このコミットの主な目的は、Goプログラムが外部コマンドを起動し、その入出力を制御し、終了を待機するための堅牢でスレッドセーフなメカニズムを提供することでした。特に、forkexec の間の競合状態を回避し、子プロセスが意図したファイルディスクリプタのみを継承するようにするための対策が求められていました。

前提知識の解説

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.ForkLocksync.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 を呼び出すまでの間、親プロセスのロック状態を引き継いでいる可能性があり、デッドロックを避けるためです。

この関数は以下の主要なステップを実行します。

  1. RawSyscall(SYS_FORK, ...): 実際の fork システムコールを呼び出します。
  2. ファイルディスクリプタの整理:
    • パス1: fd[i] < i となるファイルディスクリプタ(つまり、新しいFDが既存のFD番号よりも小さい場合)を、len(fd) よりも大きい一時的なFD番号に DUP2 します。これは、後続のパス2でFD番号を iDUP2 する際に、まだ必要なFDを上書きしてしまうのを防ぐためです。一時的なFDには FD_CLOEXEC フラグを設定します。
    • パス2: fd[i]iDUP2 します。これにより、子プロセスは指定されたFDを正しい番号で継承します。この際、FD_CLOEXEC フラグをクリアします。
    • 不要なFDのクローズ: len(fd) よりも大きいFD(ただし、標準入力/出力/エラーを除く)を閉じます。
  3. RawSyscall(SYS_EXECVE, ...): 新しいプログラムをロードして実行します。
  4. エラーハンドリング: exec が失敗した場合、エラーコードをパイプ(親プロセスとの通信用)に書き込み、子プロセスは SYS_EXIT で終了します。

os.ForkExecexec.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 がまだ本格的に実装されておらず、RLockRUnlock が単に MutexLockUnlock を呼び出すだけのスタブ実装となっています。これは、機能のプロトタイプを先に導入し、後でより効率的な実装に置き換えるという開発アプローチを示しています。

コアとなるコードの変更箇所

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.FDCmd 構造体に格納して返します。 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 はまだ本格的な実装ではなく、RLockRUnlock が単に基底の MutexLockUnlock を呼び出すだけのスタブとなっています。これは、読み取りロックと書き込みロックのセマンティクスは満たすものの、読み取りの並行性を活用できない非効率な実装であることを意味します。これは、機能のプロトタイプを先に導入し、後でパフォーマンスを最適化する一般的な開発手法です。

関連リンク

参考にした情報源リンク