mkfifoでプロセス間通信を試す (Rust)

はじめに

mkfifoという名前付きパイプを作成するシステムコール(を実行する関数)を試しました。名前付きパイプはプロセス間通信の手段の1つです。 例えばcat file | grep hogeで使うのは名前無しパイプの方です。名前付きパイプではファイルを作成してそれを読み書きすることでプロセス間の通信を実現します。

読み出し用にオープンすると、他のプロセスによって書き込み用にオープンされるまでブロックされます。逆も同様です。

試したソースコードgkuga/rust-examplesにおいてあります。今回はRustで試しましたがLinuxの機能なのでCでもGoでもRubyでもできます。

mkfifoを試す

nixというライブラリを使っています。以前に書いたnixでプロセスをforkしてみる (Rust)ソースコードを流用していますので、プロセスをフォークするところまではこちらを参照してください。

今回のプログラムの流れは以下です。

  1. 名前付きパイプを作成
  2. プロセスをフォーク
    • 子プロセスで名前付きパイプから読み込み標準出力に出力する
    • 親プロセスで"Hello World!"を書き込む
  3. 子プロセスが終了したら親プロセスも終了

では早速プログラムの中身を見ていきます。mkfifo()はnixのunistdモジュールにあります。作成は簡単でmkfifo("名前付きパイプのパス", "権限を設定するフラグ")を実行するだけです。以下では成功した場合にその作成したパスを表示します。

match unistd::mkfifo(&fifo_path, stat::Mode::S_IRWXU) {
    Ok(_) => println!("created {:?}", fifo_path),
    Err(err) => println!("Error creating fifo: {}", err),
}

実行すると下記のようになります。

$ cargo run
   Compiling mkfifo v0.1.0 (/home/vagrant/dev/src/github.com/gkuga/rust-examples/mkfifo)
    Finished dev [unoptimized + debuginfo] target(s) in 0.61s
     Running `target/debug/mkfifo`
Main(10823) stared
created "/tmp/test_fifo.N9iLh4CIh1iv/foo.pipe"

foo.pipeというのは自分で適当につけた名前です。

その後プロセスをフォークします。そして子プロセスではfile.read_to_string(&mut contents).unwrap();というようにファイルの中身を読み込んでいます。

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());
        sleep(Duration::from_secs(3));
        let mut file = OpenOptions::new().read(true).open(&fifo_path).unwrap();
        let mut contents = String::new();
        file.read_to_string(&mut contents).unwrap();
        println!("{}", contents);
        exit(0);
    }
    Err(_) => panic!("fork() failed"),
};

この記事のはじめにのところに書きましたが、

読み出し用にオープンすると他のプロセスによって書き込み用にオープンされるまでブ ロックされます。逆も同様です。

というように書き込みされるまで読み出し部分でブロックされます。上記のコードのOpenOptions::new().read(true).open(&fifo_path).unwrap();この部分でプロセスがブロックされています。そのあとにprintln!("{}", contents);と内容を表示して、exit(0);と子プロセスは終了します。

親プロセスで以下のように"Hello, world!"を書き込んでいます。これによって子プロセスのブロックが解除され"Hello, world!"を表示してくれるはずです。

file.write_all(b"Hello, world!").unwrap();

実行結果だけ掲載すると以下のようになります。

$ cargo run
    Finished dev [unoptimized + debuginfo] target(s) in 0.02s
     Running `target/debug/mkfifo`
Main(10878) stared
created "/tmp/test_fifo.kh9OV0xAZXxY/foo.pipe"
Main(10878) forked a child(10880)
Child(10880) started. PPID is 10878.
Hello, world!
Child exited (Exited(Pid(10880), 0)).

"Hello, world!"が表示されて子プロセスが終了しています。

おわりに

プロセス間通信の仕組みがいまいちよくわかっていませんが、今でもよく使われているのでしょうか。今回はruncのコードでコンテナをスタートさせる時に使われていたので試した次第です。名前付きパイプは「読み込みがされているのか書き込みがされているのか」の状態管理が大変そうです。何かもっと良いやり方はないのでしょうか?

また、今回親プロセス側で以下のようにブロックで囲まないとなぜかうまくいきませんでした。

{
    let mut file = OpenOptions::new().write(true).open(&fifo_path).unwrap();
    file.write_all(b"Hello, world!").unwrap();
}

仕組みがいまいちよくわかっていないので、分かり次第追記します。