rust-shell - 解説
実装の詳細
プロセス生成の流れ
1. ユーザー入力を読み取り
2. コマンドをパース
3. 組み込みコマンドか判定
- Yes: シェル内で直接実行
- No: 外部コマンドを実行
4. 外部コマンドの実行
- fork() で子プロセス生成
- リダイレクト設定
- exec() でプログラム実行
5. 親プロセスで wait()
6. 終了ステータスを記録
std::process::Command
// Rust の Command は fork/exec を抽象化
let output = Command::new("ls")
.arg("-la")
.current_dir("/tmp")
.env("MY_VAR", "value")
.stdin(Stdio::piped())
.stdout(Stdio::piped())
.stderr(Stdio::inherit())
.output()?;
println!("stdout: {}", String::from_utf8_lossy(&output.stdout));
パイプラインの実装
コマンド: ls | grep foo | wc -l
┌────┐ stdout ┌──────┐ stdout ┌────┐
│ ls │ ──────→ │ grep │ ──────→ │ wc │ → stdout
└────┘ pipe └──────┘ pipe └────┘
// パイプの作成と接続
let mut prev_stdout = None;
for cmd in commands {
let mut process = Command::new(&cmd.program);
// 前のコマンドの出力を入力に接続
if let Some(stdout) = prev_stdout.take() {
process.stdin(Stdio::from(stdout));
}
// 次のコマンド用に出力をパイプに
process.stdout(Stdio::piped());
let mut child = process.spawn()?;
prev_stdout = child.stdout.take();
}
よくある間違い
1. 組み込みコマンドの子プロセス実行
// 間違い: cd を子プロセスで実行
Command::new("cd").arg("/tmp").status();
// → 子プロセスのディレクトリが変わるだけで親には影響なし
// 正しい: シェルプロセス内で実行
env::set_current_dir("/tmp")?;
2. ゾンビプロセス
// 間違い: wait しない
let child = Command::new("sleep").arg("1").spawn()?;
// child は drop されても wait されない → ゾンビ
// 正しい: 明示的に wait
let mut child = Command::new("sleep").arg("1").spawn()?;
child.wait()?;
3. ファイルディスクリプタのリーク
// 間違い: リダイレクト用ファイルを閉じない
let file = File::create("out.txt")?;
process.stdout(Stdio::from(file));
// file は move されるので問題ないが、他の FD は注意
// パイプでは使わない側を閉じる必要がある
パフォーマンス考慮事項
fork() のコスト
// fork() は仮想メモリをコピー(CoW)
// 大きなプロセスでは一時的にメモリ使用増加
// 最適化: vfork() や posix_spawn()
// Rust の Command は内部で最適化を行う
パイプのバッファ
// パイプにはカーネルバッファがある(通常64KB)
// バッファが満杯になると write() がブロック
// バッファが空だと read() がブロック
// デッドロック例:
// 大量出力するコマンドの stdout を読まないと
// パイプバッファが満杯 → コマンドがブロック
発展トピック
シグナル処理
use signal_hook::{consts::SIGINT, iterator::Signals};
// Ctrl+C のハンドリング
let mut signals = Signals::new(&[SIGINT])?;
thread::spawn(move || {
for sig in signals.forever() {
match sig {
SIGINT => {
// 現在の子プロセスに SIGINT を送信
// または無視
}
_ => {}
}
}
});
ジョブ制御
// バックグラウンド実行
struct Job {
pid: u32,
command: String,
status: JobStatus,
}
enum JobStatus {
Running,
Stopped,
Done,
}
// & でバックグラウンド実行
if line.ends_with('&') {
let child = command.spawn()?;
jobs.push(Job {
pid: child.id(),
command: line.to_string(),
status: JobStatus::Running,
});
// wait せずに続行
}
ヒストリー
use std::fs::{File, OpenOptions};
use std::io::{BufRead, BufReader, Write};
struct History {
entries: Vec<String>,
file_path: PathBuf,
}
impl History {
fn add(&mut self, command: &str) {
self.entries.push(command.to_string());
// ファイルに追記
if let Ok(mut file) = OpenOptions::new()
.append(true)
.create(true)
.open(&self.file_path)
{
writeln!(file, "{}", command).ok();
}
}
fn get(&self, index: usize) -> Option<&str> {
self.entries.get(index).map(|s| s.as_str())
}
}