Rustでプロセスを作りたくなったので試した。変なとこがあれば教えてください。
Rust でシステム周りのプログラミングをする場合、nixというクレートが便利。システムコール呼び出しを Result<T, E>
で返してくれるようになって、極めて扱いやすくなる。
プロセス周りについては、 fork(2)
と wait(2)/waitpid(2)
のラッパーが存在するのでまずは素直に使う。
単純な例
use nix::sys::wait::waitpid; use nix::unistd::{fork, ForkResult}; use std::thread::sleep; use std::time::Duration; fn main() -> Result<(), Box<dyn std::error::Error>> { println!("Hello, world!"); match unsafe { fork()? } { ForkResult::Parent { child } => { println!("Child Pid: {}", child); let status = waitpid(child, None)?; println!("Reaped: {:?}", status); } ForkResult::Child => { println!("I'm a new child process"); sleep(Duration::from_secs(5)); println!("Exit!!1"); } } Ok(()) }
fork()
はnix上でも unsafe であり、返り値は Result で包まれた ForkResult
というenum。これを振り分けることで親子プロセスの判定ができる。
上記のコードをLinux上で動かすとたしかによくあるプロセスツリーが作成される。
vagrant 14661 4.7 0.0 3132 860 pts/0 S+ 10:38 0:00 | \_ target/debug/fork-wait-example vagrant 14663 0.0 0.0 3132 132 pts/0 S+ 10:38 0:00 | \_ target/debug/fork-wait-example
正常終了時にはこう。
Hello, world! Child Pid: 15207 I'm a new child process Exit!!1 Reaped: Exited(Pid(15207), 0)
シグナルで子プロセスをkillするとこういう感じ。なお waitpid
の返り値もenumである。第2引数では、 WNOHANG
など man waitpid にあるオプションを受け付ける模様。
Hello, world! Child Pid: 15224 I'm a new child process Reaped: Signaled(Pid(15224), SIGTERM, false)
複数ワーカを作る
ワーカを作る場合は単純に fork()
を呼んで、 waitpid(None, None)
で待ち受ければOK。なお、 nix::wait::wait
というショートカットもある。
use nix::sys::wait::waitpid; use nix::unistd::{fork, ForkResult}; use std::thread::sleep; use std::time::Duration; fn main() -> Result<(), Box<dyn std::error::Error>> { println!("Hello, world!"); for _ in 0..5 { match unsafe { fork()? } { ForkResult::Parent { child } => { println!("Added child. Pid: {}", child); } ForkResult::Child => { println!("I'm a new child process"); sleep(Duration::from_secs(30)); println!("Exit!!1"); std::process::exit(0) } } sleep(Duration::from_secs(3)) } while let Ok(status) = waitpid(None, None) { println!("Reaped child. Status: {:?}", status); } Ok(()) }
走らせるとこう。
vagrant 15761 1.2 0.0 3132 1752 pts/0 S+ 10:59 0:00 | \_ target/debug/fork-wait-example vagrant 15763 0.0 0.0 3132 136 pts/0 S+ 10:59 0:00 | \_ target/debug/fork-wait-example vagrant 15768 0.0 0.0 3132 136 pts/0 S+ 10:59 0:00 | \_ target/debug/fork-wait-example vagrant 15771 0.0 0.0 3132 136 pts/0 S+ 10:59 0:00 | \_ target/debug/fork-wait-example vagrant 15774 0.0 0.0 3132 136 pts/0 S+ 10:59 0:00 | \_ target/debug/fork-wait-example vagrant 15778 0.0 0.0 3132 136 pts/0 S+ 10:59 0:00 | \_ target/debug/fork-wait-example
一つ一つの終了ステータスを確認できる。while let のループで扱いが可能になり、極めて便利。
Hello, world! I'm a new child process Added child. Pid: 15763 Added child. Pid: 15768 I'm a new child process Added child. Pid: 15771 I'm a new child process Added child. Pid: 15774 I'm a new child process Added child. Pid: 15778 I'm a new child process Exit!!1 Reaped child. Status: Exited(Pid(15763), 0) Exit!!1 Reaped child. Status: Exited(Pid(15768), 0) Exit!!1 Reaped child. Status: Exited(Pid(15771), 0) Exit!!1 Reaped child. Status: Exited(Pid(15774), 0) Exit!!1 Reaped child. Status: Exited(Pid(15778), 0)
forkの留意点
マルチスレッドの場合、子プロセスにおいては async-signal-safe な関数のみを呼び出すべきとしている 。なので、今回のサンプルプログラムのように親子共シングルスレッドなら気にしなくてもいいとは思う*1が、デーモンを書くような場合マルチスレッドのライブラリを内部で使うこともありうる。基本的には即座に execve()
するような目的が良いかもしれない。例えば自分自身を execve
して $0
で動作を変えるようなパターンは良さそう。
特に、書いてある通り memory allocation may not be async-signal-safe
なので、複雑な構造体を生成したりすべきではないだろう。
今度は、プロセス間通信を試す。 socketpair とか。
*1:psのSTATからわかる。