Don't Repeat Yourself

Don't Repeat Yourself (DRY) is a principle of software development aimed at reducing repetition of all kinds. -- wikipedia

nix によるシステムプログラミング

これは、Rustその2 Advent Calendar 2017 23日目の記事です。

Rust はシステムプログラミング言語なので当然ですが、システムプログラミングができます。が、この話題に関して探してみると思った以上に日本語文献が少ないなと思ったので、今後の Rust の普及のためにもシステムプログラミング観点からの記事を残しておきます。[*1]

ご存知の方も多いかとは思いますが、改めて、今回は nix というライブラリを使って、 システムコールforkwaitexec を呼び出す簡単なプログラムを書きます。

更新情報

  • 2021-06-06: nix のバージョン 0.19.0 より fork は unsafe 関数となっていることを確認しました。せっかくなので、現時点のバージョンである 0.21.0 を使用し、改めて fork-exec のコード部分を書き換えておきました。

Rust で UNIX システムプログラミングをする

システムコールするには libc などといった手段がありますが、今回はそれを nix というライブラリを使って書いていきます。

github.com

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 までを実装してみる

実際の実装例を見てみましょう。ここでは、

  1. プロセスを fork
  2. 子プロセスで新しいプログラムを exec
  3. 親プロセスは子プロセスを wait
  4. プログラムの実行結果を出力
  5. 正常に終了した場合は完了メッセージを出し、失敗した場合はその旨をメッセージに出す

という、よくある「プログラムを実行して結果を待つ」操作を実行できるサンプルを作ります[*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つに分裂させます。元から存在しているプロセスが「親プロセス」で、複製して作られたほうが「子プロセス」ですね。nixfork の rustdoc にも、今説明した挙動についてとても詳しく載っておりますので、そちらもぜひご覧ください。

さて、 nixfork ですが、きちんと 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日目にも同様の話題を書いておられますので、そちらもぜひご覧ください。かぶってしまい申し訳ありません。

*1:なお、この記事では、ある程度 システムコールなどのシステムプログラミングに関する知識があることを前提にして書いておりますので、あらかじめご了承ください。そこまで難しい話は書いていないので、「そういうこともできるのか」くらいのスタンスでお読みいただければ幸いです。

*2:Mac OS X かつ Rust のバージョンは最新の nightly です。