std::process::Command::spawn (UNIX)
Rustでプロセス起動を行いたい場合、std::process::Command
を使うことになると思います。
use std::process::Command;
let output = Command::new("sh")
.arg("-c")
.arg("echo hello")
.output()
.expect("failed to execute proces");
let hello = output.stdout;
その処理の中で中核となる spawn
関数を追ってみます。UNIX側の処理しか追っていません。
対象となる部分のソースコードは こちら です。
まず setup_io
関数の処理が目に入ります。ここは標準入出力のredirectの指定を行います。この時引数に needs_stdin
というフラグがあり、ここがtrueの場合は標準入力のredirect先を /dev/null
に指定しています。
次の処理からはOSごとに処理の内容が大きく分岐します。Linux(かつglibcのバージョンが2.24以上)/MacOS/FreeBSDでは、条件が揃っていればプロセス起動に posix_spawn
を使っています。posix_spawnはfork-execまでを一気にやってくれるAPIで、Linuxではcloneシステムコール、MacOS/FreeBSDではvforkを使うためメモリ使用量が抑えられパフォーマンスの向上が見込めます。Linuxでglibcのバージョンを要求しているのは、posix_spawnの返り値としてENOENTが返ってくるようになるのがこのバージョン以降となるためです。posix_spawnが使えるためには、uidやgidの情報を持っていることやgetcwdがディレクトリを返すことなどの条件があります。
実際にposix_spawnで処理を行うところをみてみると、まず標準入出力をdup2して0,1,2番のファイルディスクリプタに複製しています。
次に posix_spawnattr_setsigmask
でposix_spawnを呼び出すスレッドはシグナルのマスクを行っています。これは直後で行っている sigaddset
と posix_spawnattr_setsigdefault
を使ってSIGPIPEのシグナルハンドラにSIG_DFLをセットしていますが、この変更をposix_spawn内で行う前にシグナルハンドラが呼び出されてしまうとrace conditionが発生してしまいます。なのでその前の段階でシグナルのマスクを行ってこれを防いでいます。
後の処理は属性を渡してこれらの処理をposix_spawn時に行うことを伝えて posix_spawnp
を呼び出しているだけです。
posix_spawnを使えない場合、fork-execを自前で行うことになります。
まず anon_pipe
関数でパイプ作っています。このパイプは子側で execvp(3)
で失敗した場合に親側にプロセス間通信で失敗した理由(およびCLOEXECのvalidation)を送ってるのでそのために使われているようです。ここはLinuxの場合可能なら pipe2(2)
を使うといったことを行っているのですが、古いLinuxカーネルでも動かせるようにweak!マクロを用いたHackを行っています。weak!マクロは dlsym(3)
でシンボルの存在チェックを行い、見つからなかった場合にフォールバックを行う、というマクロです。weak!マクロの全体の実装は このように なっています。
余談ですが、RustはI/Oデザインとして3番以降のすべてのfdにデフォルトでCLOEXECを設定してfd継承を避けるようにしています。これは ulrich drepperも言っているように リソース消費とセキュリティリスクの問題からでしょう。
fork(2)
後の処理を親側、子側でみていきます。
親側をみると、当たり前ですがpipe fdのいらないほうをcloseする処理があります。
その後input側をreadして終了ステータスをみる処理があります。
// loop to handle EINTR
loop {
match input.read(&mut bytes) {
Ok(0) => return Ok((p, ours)),
Ok(8) => {
assert!(combine(CLOEXEC_MSG_FOOTER) == combine(&bytes[4.. 8]),
"Validation on the CLOEXEC pipe failed: {:?}", bytes);
let errno = combine(&bytes[0.. 4]);
assert!(p.wait().is_ok(),
"wait() should either return Ok or panic");
return Err(Error::from_raw_os_error(errno))
}
Err(ref e) if e.kind() == ErrorKind::Interrupted => {}
Err(e) => {
assert!(p.wait().is_ok(),
"wait() should either return Ok or panic");
panic!("the CLOEXEC pipe failed: {:?}", e)
},
Ok(..) => { // pipe I/O up to PIPE_BUF bytes should be atomic
assert!(p.wait().is_ok(),
"wait() should either return Ok or panic");
panic!("short read on the CLOEXEC pipe")
}
}
}
成功した場合はそのまま子のpidと終了ステータスの組を返します。
失敗した場合、条件はごにょごにょあるのですがwaitによる待ちを親が行ってエラーを返したりしてくれるようです。エラー処理の中では前述したpipeのCLOEXECのvalidationチェックを行うコードがあります。
成功したときに read(2)
でブロックしそうに見えますが、子でwrite側のfdが execvp(3)
が成功したときにCLOEXECされるために親の read(2)
側ではEOFを検知して0を返すようになっているのでここでブロックすることはありません。
子側もみてみます。
do_exec
関数内で標準入出力を必要に応じてそれぞれ0,1,2番に複製し、posix_spawnの処理と同様にシグナルマスクとSIGPIPEのシグナルハンドラをSIG_DFLにセットしてから execvp(3)
を呼んでコマンドを実行しています。
失敗したらerrnoとCLOEXEC validationをパイプを用いて親に渡して _exit(2)
で即時終了します。このときexitを使ってしまうとatexitで登録したハンドラが動いてしまうので必ず _exit
によって終了する必要があります。
注釈
ソースコードの引用は以下の条文に基づき行われます。