これは、Rustその2 Advent Calendar 2017 23日目の記事です。
Rust はシステムプログラミング言語なので当然ですが、システムプログラミングができます。が、この話題に関して探してみると思った以上に日本語文献が少ないなと思ったので、今後の Rust の普及のためにもシステムプログラミング観点からの記事を残しておきます。[*1]
ご存知の方も多いかとは思いますが、改めて、今回は nix
というライブラリを使って、 システムコールの fork
、 wait
、 exec
を呼び出す簡単なプログラムを書きます。
更新情報
- 2021-06-06: nix のバージョン 0.19.0 より fork は unsafe 関数となっていることを確認しました。せっかくなので、現時点のバージョンである 0.21.0 を使用し、改めて fork-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 を利用するモチベーションだと言えるでしょう。
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.21.0"
main.rs
use nix::sys::wait::*; use nix::unistd::{execve, fork, ForkResult}; use std::env; use std::ffi::CString; fn main() { // a) fork match unsafe { 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日目にも同様の話題を書いておられますので、そちらもぜひご覧ください。かぶってしまい申し訳ありません。