nixでプロセスをforkしてみる (Rust)

はじめに

Rustでシステムプログラミングに入門中です。

プロセスのforkには、Rustからlibcを呼べるlibcというunsafeなライブラリと、それsafeにしたnixというものが使えるようです。unsafeは扱いにくいのでnixを使いました。

Cargo.toml

[dependencies]
nix = "0.16.1"

今後rustのlibcを呼び出しているライブラリをlibcクレイトと呼ぶことにします。

この記事は私のRustの学習記録です。なのでRustでシステムプログラミングを始める初学者向けです。ただLinuxのプロセスの説明は省いています。

自分のプロセスIDを表示する

プログラムを起動して自分のプロセスIDを表示します。

src/main.rs

extern crate nix;

use nix::unistd::{getpid};

fn main() {
    println!("PID: {}", getpid());
}
$ cargo run
    Finished dev [unoptimized + debuginfo] target(s) in 0.02s
     Running `target/debug/fork`
PID: 4042

起動したプログラムのPIDが表示できました。

getpid()

nixのソースコードでは下記のようにlibcクレイトのgetpid()を呼び出しています。

/// Get the pid of this process (see
/// [getpid(2)](http://pubs.opengroup.org/onlinepubs/9699919799/functions/getpid.html)).
///
/// Since you are running code, there is always a pid to return, so there
/// is no error case that needs to be handled.
#[inline]
pub fn getpid() -> Pid {
    Pid(unsafe { libc::getpid() })
}

Pidはlibcクレイトで定義されているpid_t型をラップするタプル構造体のようです。

/// Process identifier
///
/// Newtype pattern around `pid_t` (which is just alias). It prevents bugs caused by accidentally
/// passing wrong value.
#[derive(Debug, Copy, Clone, Eq, PartialEq, Hash)]
pub struct Pid(pid_t);

impl Pid {
    /// Creates `Pid` from raw `pid_t`.
    pub fn from_raw(pid: pid_t) -> Self {
        Pid(pid)
    }

    /// Returns PID of calling process
    pub fn this() -> Self {
        getpid()
    }

    /// Returns PID of parent of calling process
    pub fn parent() -> Self {
        getppid()
    }

    /// Get the raw `pid_t` wrapped by `self`.
    pub fn as_raw(self) -> pid_t {
        self.0
    }
}

impl From<Pid> for pid_t {
    fn from(pid: Pid) -> Self {
        pid.0
    }
}

impl fmt::Display for Pid {
    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
        fmt::Display::fmt(&self.0, f)
    }
}

単純にラップしているように見えます。nixの中身についてはまたの機会にみていこうと思います。

下記でも同様にPIDを取得できるようです。どっちの書き方がいいのでしょうか。

extern crate nix;

use nix::unistd::{Pid};

fn main() {
    println!("PID: {}", Pid::this());
}

さっそくフォークしてみる

フォークの使い方はドキュメントにサンプルが載っています。

src/main.rs

extern crate nix;

use nix::unistd::{fork, getpid, getppid, ForkResult};

fn main() {
    println!("Main({}) stared", getpid());

    match fork() {
        Ok(ForkResult::Parent { child, .. }) => {
            println!("Main({}) forked a child({})", getpid(), child);
        }
        Ok(ForkResult::Child) => {
            println!("Child({}) started. PPID is {}", getpid(), getppid());
        }
        Err(_) => println!("Fork failed"),
    }
}
$ cargo run
   Compiling fork v0.1.0 (/home/vagrant/dev/src/github.com/gkuga/rust-examples/fork)
    Finished dev [unoptimized + debuginfo] target(s) in 0.34s
     Running `target/debug/fork`
Main(4901) stared
Main(4901) forked a child(4923)
Child(4923) started. PPID is 1

fork()でプロセスがコピーされて、元のプロセスではOk(ForkResult::Parent { child, .. })が実行されます。コピーされた子のプロセスではOk(ForkResult::Child)が実行されます。ここでChild(4923) started. PPID is 1となっていますが、本来はChild(4923) started. PPID is 4901を期待していました。これは親のプロセスが先に終了してしまったので、PIDが1のinitプロセスに付け替えられたようです。

waitpid()

親のプロセスで子のプロセスが終了するまで待ちます。それにはwaitpid()を使います。

extern crate nix;

use nix::sys::wait::waitpid;
use nix::unistd::{fork, getpid, getppid, ForkResult};
use std::process::exit;

fn main() {
    println!("Main({}) stared", getpid());

    let child_pid = match fork() {
        Ok(ForkResult::Parent { child, .. }) => {
            println!("Main({}) forked a child({})", getpid(), child);
            child
        }
        Ok(ForkResult::Child) => {
            println!("Child({}) started. PPID is {}.", getpid(), getppid());
            exit(0);
        }
        Err(_) => panic!("fork() failed"),
    };

    match waitpid(child_pid, None) {
        Ok(status) => println!("Child exited ({:?}).", status),
        Err(_) => println!("waitpid() failed"),
    }
}

fork()のmatchで子のプロセスIDを返すようにします。それをwaitpidに渡しました。実行結果は下記のようになりました。

$ cargo run
   Compiling fork v0.1.0 (/home/vagrant/dev/src/github.com/gkuga/rust-examples/fork)
    Finished dev [unoptimized + debuginfo] target(s) in 0.31s
     Running `target/debug/fork`
Main(5187) stared
Main(5187) forked a child(5212)
Child(5212) started. PPID is 5187.
Child exited (Exited(Pid(5212), 0)).

おわりに

何気なくプログラムはLinuxで動かしていましたが、nixという名前の通り様々なUnix系OSに対応しているようです。Macでも動きました。

Nix seeks to provide friendly bindings to various *nix platform APIs (Linux, Darwin, ...).

libcクレイトがrustが動作するプラットフォームで動くように作られているようです。

libc | Raw bindings to platform APIs for Rust