Open6

試して理解 Linuxのしくみ w/Rust

gomadoufugomadoufu

書籍ではUbuntuが指定されているがUbuntu機を持っていなかったので、Raspberry Pi 4にUbuntuをいれて使う。

gomadoufugomadoufu
$ cat /etc/os-release
PRETTY_NAME="Ubuntu 22.04.3 LTS"
...
$ cat /proc/cpuinfo
...
Hardware        : BCM2835
Revision        : c03112
Serial          : 10000000972ed649
Model           : Raspberry Pi 4 Model B Rev 1.2
gomadoufugomadoufu

システムコール発行の可視化

fn main() {
    println!("hello world");
}
$ rustc hello.rs
$ strace -o hello.log ./hello
hello world

straceの出力をチェック。

...
write(1, "hello world\n", 12)           = 12
...

ちゃんとwrite()システムコールが発行されているみたい。
12は何だと思って調べてみたら、バイト数とのこと。たしかに\nあわせて12ある。

gomadoufugomadoufu

システムコールを処理している時間の割合
Ubuntuにsarが入っていなかった。

sudo apt install sysstat

普通に無限ループさせてみる

fn main() {
    loop {}
}
$ taskset -c 0 ./inf-loop&
$ sar -P 0 1 1
Linux 5.15.0-1042-raspi (raspi4-ubuntu)         2023年11月28日  _aarch64_       (4 CPU)

14時09分47秒     CPU     %user     %nice   %system   %iowait    %steal     %idle
14時09分48秒       0    100.00      0.00      0.00      0.00      0.00      0.00
Average:          0    100.00      0.00      0.00      0.00      0.00      0.00

ユーザモードが100%になった。

今度は、システムコールgetppid()を無限ループさせてみる。
組み込みのstd::process::idを使えば十分だけど、Cのシステムコールと名前が対応していた方が見た目がわかりやすいので、外部ライブラリのnixを使ってみる。はじめて使う!
https://github.com/nix-rust/nix
外部ライブラリを使うにあたり、いちいちcargoでプロジェクトを作るのは面倒なので、補助としてrust-scriptも使う。
https://github.com/fornwall/rust-script

#!/usr/bin/env rust-script

//! ```cargo
//! [dependencies]
//! nix = { version = "0.27.1", features = ["process"]}
//! ```

use nix::unistd;

fn main() {
    loop {
        unistd::getppid();
    }
}
$ chmod +x syscall-inf-loop.rs
$ taskset -c 0 ./syscall-inf-loop.rs&
$ sar -P 0 1 1
Linux 5.15.0-1042-raspi (raspi4-ubuntu)         2023年11月28日  _aarch64_       (4 CPU)

15時07分04秒     CPU     %user     %nice   %system   %iowait    %steal     %idle
15時07分05秒       0     49.00      0.00     51.00      0.00      0.00      0.00
Average:          0     49.00      0.00     51.00      0.00      0.00      0.00

システムの占める割合が増えた。

gomadoufugomadoufu

同じプロセスを2つに分裂させるfork()関数
書籍のサンプルコードはPythonだけど、nixのfork()関数を使えば問題なくできそう。
https://docs.rs/nix/latest/nix/unistd/fn.fork.html
気になる点として、上記docs内にSafetyについての注記がある。

n a multithreaded program, only async-signal-safe functions like pause and _exit may be called by the child (the parent isn’t restricted). Note that memory allocation may not be async-signal-safe and thus must be prevented.
Those functions are only a small subset of your operating system’s API, so special care must be taken to only invoke code you can control and audit.

子プロセス内でasync-signal-safeでない関数を呼ばないでね、ということみたい。docsのサンプルコード中に、たとえばprintln!はダメだよと書いてある。以下の記事にも情報がある
https://zenn.dev/helloyuki/scraps/86384c534e63ac
async-signal-safeってなんぞや、と思って調べていたら、強火の記事を見かけたので貼っておく。ここでもasync-signal-safeでない関数としてmalloc()があげられていて、print系もダメそうだなという雰囲気がわかる。今回はシグナルハンドラの話ではないから、直接の関係はないのだと思うのだけれど。
https://qiita.com/rarul/items/090920b850acc4b7e910

Safetyの注記はあるが、docsのサンプルコードを利用して、問題なくfork()の動きを確認できた。注記を受け、unsafeな気持ちを込めて、println!unsafeで囲っておいた。気持ちが大事

#!/usr/bin/env rust-script

//! ```cargo
//! [dependencies]
//! nix = { version = "0.27.1", features = ["process"]}
//! ```

use nix::libc;
use nix::unistd;
use nix::unistd::ForkResult;

fn main() {
    match unsafe { unistd::fork() } {
        Ok(ForkResult::Parent { child, .. }) => {
            println!(
                "親プロセス : pid={}, 子プロセスのpid={}",
                unistd::getpid(),
                child
            );
            unsafe { libc::_exit(0) };
        }
        Ok(ForkResult::Child) => {
            #[allow(unused_unsafe)]
            unsafe {
                println!(
                    "子プロセス : pid={}, 親プロセスのpid={}",
                    unistd::getpid(),
                    unistd::getppid()
                )
            };
            unsafe { libc::_exit(0) };
        }
        Err(_) => println!("Fork failed"),
    }
}

ちょっと気になったのが、nixにはlibcのexit()のラッパが存在しないこと。exitしている時点でRustの安全管理の管轄外だから、nixとしてはexit()に関与しないよ、ということなのかな? サンプルコードでもlibc::_exit(0)が呼ばれていたので、それに倣った。

gomadoufugomadoufu

別のプログラムを起動するexecve()関数

#!/usr/bin/env rust-script

//! ```cargo
//! [dependencies]
//! nix = { version = "0.27.1", features = ["process"]}
//! ```

use nix::libc;
use nix::unistd;
use nix::unistd::ForkResult;
use std::ffi::CString;

fn main() {
    match unsafe { unistd::fork() } {
        Ok(ForkResult::Parent { child, .. }) => {
            println!(
                "親プロセス : pid={}, 子プロセスのpid={}",
                unistd::getpid(),
                child
            );
            unsafe { libc::_exit(0) };
        }
        Ok(ForkResult::Child) => {
            #[allow(unused_unsafe)]
            unsafe {
                println!(
                    "子プロセス : pid={}, 親プロセスのpid={}",
                    unistd::getpid(),
                    unistd::getppid()
                )
            };
            let _ = unistd::execve(
                CString::new("/bin/echo")
                    .expect("CString::new failed")
                    .as_c_str(),
                &[
                    CString::new("echo")
                        .expect("CString::new failed")
                        .as_c_str(),
                    CString::new(format!("pid={} からこんにちは", unistd::getpid()))
                        .expect("CString::new failed")
                        .as_c_str(),
                ],
                &[CString::new("").expect("CString::new failed").as_c_str()],
            );
            unsafe { libc::_exit(0) };
        }
        Err(_) => println!("Fork failed"),
    }
}
$ ./fork-and-exec.rs
親プロセス : pid=476199, 子プロセスのpid=476200
子プロセス : pid=476200, 親プロセスのpid=476199
pid=476200 からこんにちは