噂のpidfdをRustで試してみた

## はじめに

[Python3.9の新機能 (まとめ)](https://qiita.com/ksato9700/items/d5df9d36147796c86c77)という記事を書く中でpidfdというものを知りました。まだあまり良い解説記事がないのですが、最近の5.x系Linuxカーネルに新たに導入されたプロセスを識別するための仕掛けです。この機能がRust言語でCrate（ライブラリのようなもの）として実装されていたので試してみました。

## pidfdとは

これまでプロセスはPID(Process ID)によって指し示されていました。PIDはプロセスを一意に指し示すための識別子で、例えばシグナルなどを送ったりするのに使われます。

PIDは32bitの整数で、プロセスが生成される時にカーネルが割り振りますが、システム全体で共有しているので枯渇しちゃいます。32bitならそこそこあるように思いますが、実はPIDの最大値はもっと小さく設定されています。Linuxの場合は `cat /proc/sys/kernel/pid_max` で見ることができますが、手元のUbuntuだと `32768` でした。これを越えるとどうなるか？ 過去につかったPIDを再利用することになります。

ところがこれはセキュリティ的な問題をはらんでいます。例えば、PID=12345が自分の信頼できるプロセスだと思っていたら、いつの間にか同じIDなのに違うプロセスになっていた、ということが起こりうるからです。そもそもIDっていうのに再利用しちゃうってまずいですよね。

人の世界では例えば電話番号とかでも同じことが起きますが、電話の場合は相手が出たら声で「こいつ違うな」ってわかりますが、Unixのプロセスの世界ではそう簡単にはわからないかも知れない。

それでこの問題を避けるために pidfd というものが導入されました。これにより、これまでシステム全体で一つの番号体系だったものをプロセスごとの番号体系になります。もちろんPIDはなくならないけど、親プロセスと子プロセスのやり取りに関してはこちらを使ったほうが安全で確実です。

そして、プロセスのために新たなID体系を作るのではなく、既にUnixにあるものを流用しています。File Descriptorですね。pidfdの"fd"はここから取られているみたいです。Unixに昔からある [Everything is a file](https://en.wikipedia.org/wiki/Everything_is_a_file) の考え方をプロセスまで拡張した感じですかね。

pidfdを得るには3つくらい方法があります。

1. `/proc/[pid]` ディレクトリを`open()`する
2. `pidfd_open()` システムコールを使う
3. `clone()` か `clone3()` を`CLONE_PIDFD`フラグをつけて呼び出す

そして、得られた pidfdを 

```c
pidfd_send_signal(int pidfd, int sig, siginfo_t *info, unsigned int flags);
```

に与えるとシグナルを送れたりします。ファイル感覚でプロセスを扱えるってなんか新鮮ですね。

仕様をみているだけだとつまらないのでちょっと試してみたくなったのですが、C言語で書くというのもあまり芸がない。と思っていたらRustの実装があったのでそちらを使ってみました。



## 試してみたこと

### 環境の準備

新しいLinuxカーネルが必要とのことでなにかのディストリビューションを入れたあとにカーネルアップグレードしなきゃかなと思ったのですが、Ubuntuの最新版である 19.10 (Eoan Ermine)がLinux 5.3を採用している（2020-01-19時点で apt upgradeすると linux-image-5.3.0-26）ということがわかり、これで試しています。

なお、久しぶりにUbuntuのインストール(server版)をやりましたが、インストーラも進化してますね。中でも、Githubから公開鍵[^1]をサクッと取ってきてくれたりするのは結構うれしかったです。

[^1]: Githubの公開鍵は`https://github.com/<user-name>.keys`で公開されているのでした 

### pidfd版

pidfdのCrateは https://crates.io/crates/pidfd にあります。`cargo new`でプロジェクトを作り、`Cargo.toml`に最新のpidfdと一緒に使うfuturesを依存関係として入れておきます。

```
[dependencies]
pidfd = "0.2.4"
futures = "0.3.1"
```

テストに使ったソースはこのような感じです。ほぼほぼサンプルプログラムのままですが、途中でpidfdの値を取りたかったので少しだけ改造してあります。

```rust
use pidfd::PidFd;
use std::{io, process::Command};
use std::os::unix::io::AsRawFd;

fn main() {
    futures::executor::block_on(async move {
        futures::try_join!(
            spawn_sleeper("1", "5"),
            spawn_sleeper("2", "4"),
            spawn_sleeper("3", "3"),
            spawn_sleeper("4", "2"),
            spawn_sleeper("5", "1"),
        )
        .unwrap();
    })
}

async fn spawn_sleeper(id: &str, timeout: &str) -> io::Result<()> {
    println!("started job {}", id);

    let pidfd = Command::new("/bin/sleep")
        .arg(timeout)
        .spawn()
        .map(PidFd::from)
        .unwrap();

    println!("pidfd={:?}", pidfd.as_raw_fd());

    let exit_status = pidfd.into_future()
        .await?;

    println!("finished job {}: {}", id, exit_status);
    Ok(())
}
```

動きを少しだけ解説しておくと、`main`は`spawn_sleeper`という非同期関数を5回呼び出して `try_join!`でそれが終わるのを待つだけです。`spawn_sleeper`は、`id`と`timeout`という2つの引数を取り、まずは`id`をプリントアウトしたあと、`/bin/sleep`を呼び出して与えられた`timeout`秒だけ待って終了という関数です。

pidfdは`Command:new`で子プロセスを作ったあとにそれを`PidFd::from`に渡して取得しています。それのraw_id（OSから渡された生のID）を表示したあと、`Future`に変換して`await`します。`timeout`後に子プロセスが終了したら、再び`id`と終了コードを表示して完了という流れです。

これを実行してみるとこうなります。

```
% cargo run
started job 1
pidfd=3
started job 2
pidfd=6
started job 3
pidfd=7
started job 4
pidfd=8
started job 5
pidfd=9
finished job 5: exit code: 0
finished job 4: exit code: 0
finished job 3: exit code: 0
finished job 2: exit code: 0
finished job 1: exit code: 0
```

動きをお見せできないのは残念ですが、`pidfd=9`までは一気に表示され、あとは一秒ごとに`finished job ...`が表示されます。というのも、`spawn_sleepers`の`awit`の手前までは一気に進み、そこまで行くと処理がyieldされて次の`spawn_sleeper`呼び出しがされるから。これを5回繰り返して待ち状態になります。そして`timeout`の指定が逆順になっているので最後に呼び出した`id=5`から順次逆順で処理を完了していきます。

これだけだと通常の非同期プログラミングのデモになっちゃうので、`pidfd=` の行を見ていただきたいです。繰り返し実行するとわかるのですが、これらの番号は基本的に変わりません。0, 1, 2 は標準入力、標準出力、標準エラーに割りあたっているので、3番から振られるのは普通のファイルと変わりません。途中の4, 5 が抜けているのは別のファイルオープンとかがあったのかもです。

### tokio版

これだけだと何なので、pidfdを使わない実装というのもやってみました。tokioというCrateを使っています。tokio は "A runtime for writing reliable asynchronous applications with Rust." （信頼できる非同期アプリをRustで書くための実行環境）で、実際Rustでは人気のCrateの一つです。

これを使ってみるとこんな風になりました。

```rust
use tokio::process::Command;
use futures::try_join;

#[tokio::main]
async fn main() -> Result<(), Box<dyn std::error::Error>> {
    try_join! (
        spawn_sleeper("1", "5"),
        spawn_sleeper("2", "4"),
        spawn_sleeper("3", "3"),
        spawn_sleeper("4", "2"),
        spawn_sleeper("5", "1"),
    ).unwrap();

    Ok(())
}

async fn spawn_sleeper(id: &str, timeout: &str) -> Result<(), Box<dyn std::error::Error>> {
    println!("started job {}", id);

    let child = Command::new("/bin/sleep")
        .arg(timeout)
        .spawn()
        .unwrap();

    println!("pid = {}", child.id());

    let exit_status = child.await?;

    println!("finished job {}: {}", id, exit_status);

    Ok(())
}

```



`spawn_sleeper` の中身を見てみると、`Command:new`から`arg()`、`spawn()`するまでは同じコード。そのあと `unwrap()`して `id`を表示させてから、`await?`で終了を待ちます。ここで `Child`をそのまま `await`で待つのは不思議だなと一瞬思いますが、冒頭の use文を見てみると `use tokio::process::Command;` となっていて標準の`use std::process::Command;`とは違うものになっています。それ以外はごく自然なコードですね。

結果はこのようになります。

```
started job 1
pid = 16850
started job 2
pid = 16851
started job 3
pid = 16852
started job 4
pid = 16853
started job 5
pid = 16854
finished job 5: exit code: 0
finished job 4: exit code: 0
finished job 3: exit code: 0
finished job 2: exit code: 0
finished job 1: exit code: 0
```

pidfd版と同じで10行目まで一気に表示し、その後は一秒ごとに一行ずつ表示されます。pidfd版との違いはidの部分で、pidが5桁の数字で表現されています。何回か実行するとわかりますが、この数字は実行するたびに変わります。そして上限の32768を越えるとまた若い番号に戻るはずです。

なお、ご参考までに今回のこの例は以下のリポジトリに入れてあります。
https://github.com/ksato9700/pidfd_rust_test

## まとめ

Linuxに新たに導入されたpidfdを調べて、そして使ってみました。まだ日本語の解説記事とか少ないと思いますが、英語だと[このlwn.netの記事](https://lwn.net/Articles/801319/)なんかは良くまとまっているかなと思います。

そして、Pythonでは asyncioの子プロセスを監視するための仕掛けとして 3.9から導入されるようですが、そのうち Subprocess モジュールとかでも使われるようになるのかも知れません。Linux限定というところが微妙ではあるのですが...