これは、Rustその2 Advent Calendar 2017 23日目の記事です。
Rust はシステムプログラミング言語なので当然ですが、システムプログラミングができます。が、この話題に関して探してみると思った以上に日本語文献が少ないなと思ったので、今後の Rust の普及のためにもシステムプログラミング観点からの記事を残しておきます。[*1]
ご存知の方も多いかとは思いますが、改めて、今回は nix というライブラリを使って、 システムコールの fork、 wait、 exec を呼び出す簡単なプログラムを書きます。
Rust で UNIX システムプログラミングをする
システムコールするには libc などといった手段がありますが、今回はそれを nix というライブラリを使って書いていきます。
nix は、 unsafe なシステムコール API を提供する libc に対して、 safety なシステムコール API を提供する、libc をラップしたライブラリです。nix を用いる利点としては、下記のように libc だと unsafe な API を提供しているものを、nix の unsafe でない API を用いてシステムプログラミングをすることができる、といったところでしょうか。
// libc api (unsafe, requires handling return code/errno) pub unsafe extern fn gethostname(name: *mut c_char, len: size_t) -> c_int; // nix api (returns a nix::Result<CStr>) pub fn gethostname<'a>(buffer: &'a mut [u8]) -> Result<&'a CStr>;
libc 側は unsafe により関数を呼び出しており、さらに返り値も c_int と C ライクです。一方で、 nix 側は unsafe がついていることもなく、また返り値も Result になっており、より Rust な書き方を後続処理でできるように配慮された作りです。
nix で fork -> wait までを実装してみる
実際の実装例を見てみましょう。ここでは、
- プロセスを
fork - 子プロセスで新しいプログラムを
exec - 親プロセスは子プロセスを
wait - プログラムの実行結果を出力
- 正常に終了した場合は完了メッセージを出し、失敗した場合はその旨をメッセージに出す
という、よくある「プログラムを実行して結果を待つ」操作を実行できるサンプルを作ります[*2]。
実行結果:
./sample /bin/echo OK OK exit!: pid=Pid(52366), status=0
第1引数に実行したいプログラムのあるディレクトリを指定し、第2引数にそのプログラムが必要とする引数を与えます。 このサンプルでは、第1引数で echo コマンドを呼び出して、第2引数で渡された文字列をコンソールに出力しています。
サンプルコード
Cargo.toml
[dependencies] nix = "0.9.0"
main.rs
extern crate nix; use std::env; use std::ffi::CString; use nix::sys::wait::*; use nix::unistd::*; fn main() { // a) fork match fork().expect("fork failed") { ForkResult::Parent { child } => { // b) waitpid match waitpid(child, None).expect("wait_pid failed") { WaitStatus::Exited(pid, status) => { println!("exit!: pid={:?}, status={:?}", pid, status) } WaitStatus::Signaled(pid, status, _) => { println!("signal!: pid={:?}, status={:?}", pid, status) } _ => println!("abnormal exit!"), } } ForkResult::Child => { // 引数の値を取得します。 let args: Vec<String> = env::args().collect(); let dir = CString::new(args[1].to_string()).unwrap(); let arg = CString::new(args[2].to_string()).unwrap(); // c) execv execv( &dir, &[ dir.clone(), arg, ], ).expect("execution failed."); } } }
簡単な解説
a) fork
fork(2) によって、カーネルにプロセスを複製させ、プロセスを2つに分裂させます。元から存在しているプロセスが「親プロセス」で、複製して作られたほうが「子プロセス」ですね。nix の fork の rustdoc にも、今説明した挙動についてとても詳しく載っておりますので、そちらもぜひご覧ください。
さて、 nix の fork ですが、きちんと Result 型で返ってきます。今回は面倒なので expect でエラーハンドリングをしていますが、パターンマッチで書くこともできます。
Result の中の ForkResult は次のような enum です:
#[derive(Clone, Copy)] pub enum ForkResult { Parent { child: Pid }, Child, }
したがって、後続でパターンマッチングを行うことができます。ForkResult::Parent が親で、ForkResult::Child が子です。
b) waitpid
親プロセスの方は、子プロセスが処理を完了するまで待ちます。これを wait(2) によって実現しています。
waitpid の第2引数には、ブロックを防いだり、どのプロセスを待つかを制御するなどのオプションを設定することも可能ですが、今回は単純なサンプルですのでそこに関してはスルーして None を定義しています。
WaitStatus は、 wait/waitpid すると返ってくる enum です。つまり、パターンマッチできます。今回は、正常終了したかシグナルを捕捉したもののみを取り扱いました。
その他のステータスについては、ソースコード内のコメントにかなり詳しく載っており親切ですのでそちらをご覧ください:
c) execv
execv(3) は、これもまたシステムコールの1つで、自プロセスを新しいプログラムで上書きする機能を持っています。プログラムの実行時に使用する常套手段です。今回は子プロセスに処理を実行させるので、子プロセス内で execv しています。
まとめ
libcでもシステムプログラミングはできますが、nixを使うとより Rust らしいシステムプログラミングができます。nixの rustdoc はとても丁寧に書かれており、API を使いこなしながら rustdoc にも目を通すと、それだけで結構システムプログラミングの勉強になります。
こちらもご覧ください
- nix crate - Qiita: 17日目にも同様の話題を書いておられますので、そちらもぜひご覧ください。かぶってしまい申し訳ありません。