pipe

Table of Contents

less

以前たまたまkshのfstatをした時、/dev/ttyがfd 10に居た。これは何するものぞって 聞いてみたら、ターミナル・アプリでは汎用で使えるので、事前に開いておくって事だった。 tmux,viなども同様らしい。調べてみたいけどソースの山に埋没しそうなんで、lessどうよ?

ad$ fstat -p 54433
USER     CMD          PID   FD MOUNT        INUM  MODE         R/W    SZ|DV
sakae    less       54433 text /         3499304  -r-xr-xr-x     r   150224
sakae    less       54433   wd /tmp        38592  drwxr-xr-x     r     1024
sakae    less       54433    0 /         5806577  crw--w----    rw    ttyp1
sakae    less       54433    1 /         5806577  crw--w----    rw    ttyp1
sakae    less       54433    2 /         5806577  crw--w----    rw    ttyp1
sakae    less       54433    3 /         5807247  crw-rw-rw-    rp      tty
sakae    less       54433    4 /tmp        38594  -rw-r--r--    rp      110

うん、幸いな事にless君もttyを開いてくれたぞ。ちなみにfd 4を使って開かれている ファイルは、下記のようにして、調べられる。ls -i | grep 38594より多少楽かな。

ad$ find /tmp -inum 38594
/tmp/myless/Makefile

何処でttyをオープンしてるか調べると、

lsystem(const char *cmd, const char *donemsg) {
           :
        /*
         * Force standard input to be the user's terminal
         * (the normal standard input), even if less's standard input
         * is coming from a pipe.
         */
        inp = dup(0);
        (void) close(0);
        if (open("/dev/tty", O_RDONLY) == -1)
                (void) dup(inp);

標準入力をパイプに切り替えている。gdbで途中まで進めてからfstatすると確かにtty に過げ替えられていた。

(gdb) bt
#0  lsystem (cmd=0xbb5135f430 <message> "vi +5 Makefile", donemsg=0x0)
    at ../lsystem.c:86
#1  0x000000bb513405a3 in commands () at ../command.c:1422
#2  0x000000bb51337ca4 in main (argc=-1, argv=0x73789cafca28) at ../main.c:269

但しこの経路は、lessを使ってて、そこからvでviを起動した場合だ。もう一つの経路が 存在してる。

(gdb) bt
#0  open_getchr () at ../ttyin.c:26
#1  0x00000b84d2fe2b41 in main (argc=-1, argv=0x71eeca2a1668) at ../main.c:224
(gdb) n
33              tty = open("/dev/tty", O_RDONLY);
(gdb) p tty
$1 = 3

どうやら、安全策のようだ。

/*
 * Open keyboard for input.
 */
void
open_getchr(void)
{
        /*
         * Try /dev/tty.
         * If that doesn't work, use file descriptor 2,
         * which in Unix is usually attached to the screen,
         * but also usually lets you read from the keyboard.
         */
        tty = open("/dev/tty", O_RDONLY);
        if (tty == -1)
                tty = STDERR_FILENO;
}
USER     CMD          PID   FD MOUNT        INUM  MODE         R/W    SZ|DV
sakae    less       23204 text /         3499304  -r-xr-xr-x     r   150224
sakae    less       23204   wd /tmp            2  drwxrwxrwt     r      512
sakae    less       23204    0 pipe 0xfffffd8053900aa8 state: E
sakae    less       23204    1 /         5806577  crw--w----    rw    ttyp1
sakae    less       23204    2 /         5806577  crw--w----    rw    ttyp1
sakae    less       23204    3 /         5807247  crw-rw-rw-    rp      tty

cat hogefile | less した場合。これが期待される利用方法。

sakae@fb:/tmp $ gdb -q --args less -
Reading symbols from less...
Reading symbols from /usr/lib/debug//usr/bin/less.debug...
(gdb) b main
Breakpoint 1 at 0x41142a: file /usr/src/contrib/less/main.c, line 256.
(gdb) r < file
Starting program: /usr/bin/less < file

Breakpoint 1, main (argc=1, argv=<optimized out>)
    at /usr/src/contrib/less/main.c:256
warning: Source file is more recent than executable.
256             progname = *argv++;

動作を追跡したい場合、こんな風にやればいいかな。

pipe

久しぶりにパイプが出てきたので、配管工になってみる。一番基本的な、親子の一方 通行伝送路です。親側でパイプを作成。forkでパイプも含めて複製して子を成す。 子は言いたい事を言ってから消滅。それを親が聞き取ると言う悲しいシチエーションです。

#include <unistd.h>
#include <string.h>

int main() {
  int pipe_fd[2];
  ssize_t n;
  pid_t child_pid;
  char buf[256];

  pipe(pipe_fd);                             // {read_fd, write_fd}
  child_pid = fork();

  if (child_pid == 0) {
    close(pipe_fd[0]);                       // no use, so close
    const char *msg = "Hello from PIPE\n";
    write(pipe_fd[1], msg, strlen(msg));
    _exit(0);
  }

  close(pipe_fd[1]);                         // no use, so close
  n = read(pipe_fd[0], buf, sizeof buf);
  write(1, buf, n);
  return 0;
}

set follow-fork-mode child を使うと子供側を追跡できる。所で、使わないパイプを 閉じているけど、これって単なる作法なの? コメントにしちゃっても動くんですけど。 例によって、あの人に聞いてみる。

とても良い質問です。この `close()` の扱いについては、**単なる作法ではなく、実際に正しく動作させるための重要な意味があります**。以下に詳しく説明します。

## 🔧 `close()` を行う理由:深淵な理由

### 1. EOF(End Of File)を伝えるため

`read()` は、読み込み元のファイルディスクリプタ(この例では ` pipe_fd[0] `)に対して、**すべての書き込み側がクローズされて初めて EOF を返す**ようになっています。

  • 親が ` pipe_fd[1] ` をクローズしなければ、子プロセスが終了して書き込みをやめても、`read()` はブロックされたままになります。
    • なぜならカーネルは「まだ書き込むプロセスがいるかもしれない」と判断するからです。

つまり:

close( pipe_fd[1] ); // これがないと read() が EOF にならず、ブロックし続ける可能性がある

### 2. リソースリーク防止

パイプもファイルディスクリプタで管理されるリソースです。不要な FD を開いたままにしておくと、次のような問題が発生します:

  • ファイルディスクリプタ枯渇(特に大量のプロセスや並列処理時)
  • デバッグ時に分かりにくくなる(「なぜまだパイプが開いているのか?」と混乱)

### 3. 意図を明確にする(これは作法の側面)

このようなクローズは、コードリーディング時に「このプロセスはこの FD を使わない」と明示する意味でも重要です。 これにより、バグの防止やメンテナンス性向上につながります。

## ✅ まとめ:`close()` は必要?

観点 必要性 理由
-------------- ---- -----------------
正しく EOF を検出させる 必須 読み手がブロックされないようにする
リソース管理 強く推奨 FD 枯渇や意図しない動作の防止
コードの可読性 推奨 誰が何を使うかを明確にする

## 💡補足:マルチプロセス・マルチスレッドでは特に重要

複数のプロセス(やスレッド)で同じパイプの FD を保持している場合、「どこでクローズするか」が非常に重要になります。 **一つでも FD が残っていると EOF が返らない**という性質は、複雑な処理ではバグの温床になります。

ご質問の通り「コメントにしても動く場合がある」のは、たまたま親子それぞれで終了タイミングやバッファが小さいなどの条件が合っているからです。しかし、それは**動作が保証された状況ではない**ため、正しい実装として `close()` は必要です。

gdb useing pipe

パイプがカーネル側でどうなってるか追跡しようとしたんだ。そしたらgdbの操作をする 度に(ex: next等)、パイプが使われていた。どれが目的なパイプ・コールか分らなくなってしまったぞ。

(gdb) info breakpoints
Num     Type           Disp Enb Address    What
1       breakpoint     keep y   0xd028eee0 in pipe_read at /usr/src/sys/kern/sys_pipe.c:382
        breakpoint already hit 15 times
2       breakpoint     keep y   0xd028f260 in pipe_write at /usr/src/sys/kern/sys_pipe.c:482

腹癒せにgdbがどんなパイプを張り巡らせているか確認しる。

qe$ ps
  PID TT  STAT        TIME COMMAND
 2820 p1  I+       0:05.14 gdb -q a.out (egdb)
 6348 p1  TX       0:00.09 /tmp/a.out
qe$ doas fstat -p 2820
USER     CMD          PID   FD MOUNT        INUM  MODE         R/W    SZ|DV
sakae    egdb        2820 text /          260802  -rwxr-xr-x     r 10560588
sakae    egdb        2820   wd /tmp            2  drwxrwxrwt     r      512
sakae    egdb        2820    0 /          416487  crw--w----    rw    ttyp1
sakae    egdb        2820    1 /          416487  crw--w----    rw    ttyp1
sakae    egdb        2820    2 /          416487  crw--w----    rw    ttyp1
sakae    egdb        2820    3 pipe 0xd4609adc state:
sakae    egdb        2820    4 pipe 0xd4609adc state:
sakae    egdb        2820    5 pipe 0xd4609c5c state:
sakae    egdb        2820    6 pipe 0xd4609c5c state:
sakae    egdb        2820    7 pipe 0xd4609d1c state:
sakae    egdb        2820    8 pipe 0xd4609d1c state:
sakae    egdb        2820    9 pipe 0xd4609a1c state:
sakae    egdb        2820   10 pipe 0xd4609a1c state:
sakae    egdb        2820   11 /tmp            4  -rwxr-xr-x    re     7684

この結果では、4本のパイプが施設されている様に見える。それはいいんだけど、 piep_read/pipe_write 回数のアンバランスさが解せない。読むのが圧倒的に 多いのは何故? 理解に苦しむぞ。それに、どんなデータが流れるのかも不明だ。

パイプと言えば、皆人体内に長大なパイプを施設してるね。そう血管だ。こちらは 循環器内科の領分。十分に理解が進んでいると思われる。燃料パイプ。ガス管。 下水管。人体防衛軍の通路。放熱管。まだまだ機能が有ったと思うけど、にわかに 思い出せない。

まあそんな横道的な事はどうでも良くて、 このパイプの利用状況を確認するには、gdbでgdbすればいいじゃん。待て、そんな 再帰を許してくれるか? 大体リエントラント(再入可能)な作りには、なっていない だろう! 一旦休止して、頭を冷やしてくる。

OS by Rust

そんな訳でネットをうろうろしてたら、面白い記事を発見したぞ。 Rust で Unix ライクな 自作 OS

丁寧な解説がされていて嬉しいけど、取りあえず、論よりrunします。

sakae@lu:octox$ cargo run --target riscv64gc-unknown-none-elf
   Compiling octox v0.1.0 (/var/my/srcs/octox)
warning: octox@0.1.0: nmeta 70 (boot, super, log blocks 30 inode blocks 13, bitmap blocks 25) blocks 199930 total 200000
warning: octox@0.1.0: mkfs:balloc: first 2908 blocks have benen allocated
warning: octox@0.1.0: mkfs:balloc: write bitmap block at sector 45
    Finished `dev` profile [unoptimized + debuginfo] target(s) in 2.58s
     Running `qemu-system-riscv64 ... -kernel target/riscv64gc-unknown-none-elf/debug/octox`

octox kernel is booting

bd: memory sz is 535654400 bytes; allocate an size array of length 26
bd: 12584176 meta bytes for managing 536870912 bytes of memory
bd: 1216512 bytes unavailable
hart 3 starting
hart 1 starting
hart 2 starting
init: starting sh

riscv64の4coreで起動してきました。psの変わりにCtl + Pが実装されてるそうです。 shutdownは、qemuのモニターモードで、いきなり電源OFFのようです。

$  ;;; Ctl + p
pid: PId(0) state: SLEEPING name: "init", chan: 2161285136
pid: PId(1) state: SLEEPING name: "sh", chan: 2147837440
QEMU: Terminated   ;;; Ctl + a x
sakae@lu:octox$

lsコマンドでユーザーランドを探検。

$ ls
dev            Dir      2 64
bin            Dir      3 272
lib            Dir      4 32
etc            Dir      5 48
README.org     File     6 5254
init           File     7 218432
initcode       File    23 99456
$ ls dev
console        Device  25 0
null           Device  26 0
$ ls bin
cat            File     8 112800
wc             File     9 122336
ln             File    10 109808
clear          File    11 108928
head           File    12 167232
rm             File    13 109000
sleep          File    14 119784
ls             File    15 191368
touch          File    16 103880
jell           File    17 570272
mkdir          File    18 115296
echo           File    19 109568
sh             File    20 282136
kill           File    21 119776
grep           File    22 203632

知らないコマンド君が居るなあ。ググっても出てこないし。jell君って何者? ソースが有るから参照せい。どうも括弧言語みたい。

$ jell
user=> (+ 1 2 3 4)
10

user=> (= 3 4)
false

user=> (= 3 3)
true

user=>
panicked at src/user/bin/jell.rs:481:37:
called `Result::unwrap()` on an `Err` value: LexicalError("\u{3}")

終了はCtl + c RETで、エラーのどさくさに紛れて脱出するのがセオリーっぽい。 ソースの最後の方にテストコードが掲載されてた。

EvaluatorTestCase::new(
    "(let [fib
       (fn [n]
         (if (<= n 2)
             1
             (+ (fib (- n 1))
                (fib (- n 2)))))]
       (fib 10))",
    Environment::new(),
    Expr::Int(55),
    false,

lambdaの代わりにfnを使うのか。

user=> (def bai (fn (x) (* 2 x)))
error: RuntimeError("fn takes vector as variable")

user=> (def bai (fn [x] (* 2 x)))
nil

user=> (bai 3)
6

user=> bai
<lambda>

user=> (conj (quote (3 4)) 5)
(5 3 4)

何となくscheme系じゃなくてclojure系と思うんだけど、どうよ。 clojureと言えばjavaの上に構築された新種のLisp。一部の人にはjavaがガラスの天井 となってしまうので不人気だったりするそうだ。その点、今回の実装はjava lessなので 天井知らずの突き抜けたLispに成長する可能性を秘めているな。kernelが仮想マシンと なって、shellの代替えを実現したりして(scshみたいな奴)。。 そうなったら、octoxの暖簾分けになるのかな。

一番気になるconjを見ると、cons cell なんて旧式な機構は使っていない。Vectorで 実現されてる。これがRust流な実装なのか。それからLispと言えばガベコレだけど、 Rustが上手に隠蔽してくれるので、心配の必要は無い。本当か?

let mut joined = elems.as_ref().clone();
for arg in args[1..].iter() {
    let value = self.evaluate(arg.clone(), env)?;
    joined.push(value);
}
Ok(Expr::Vector(Rc::new(joined)))

カーネルを含めてユーザーランドをgdbの遡上に載せるには、どうしたらいいのだろう? Rustを真面目にやった事が無いから作法が分からない。

仮想マシンの設定

octoxの土台も興味が有るので確認。

.carogo/config.toml

qemu-system-riscv64 -machine virt \
 -bios none \
 -m 524M \
 -smp 4 \
 -nographic \
 -serial mon:stdio \
 -global virtio-mmio.force-legacy=false \
 -drive file=target/fs.img,if=none,format=raw,id=x0 \
 -device virtio-blk-device,drive=x0,bus=virtio-mmio-bus.0 \
 -kernel  (target/riscv64gc-unknown-none-elf/debug/octox)

括弧内は、実稼動時に生成? されるのかな。-bios noneって、完全なbearなマシン。 気合が入っているな。

対比として、OpenBSDの起動設定。kvmってqemu-system-amd64を高速化する専用の エイリアスだな。

kvm -m 1536 \
  -nographic \
  -no-fd-bootchk \
  -s \
  -net nic \
  -net user,hostfwd=tcp::2022-:22 \
  -hda disk

biosはデフォの物を利用してんだな。カーネルやシリアルはdiskの中に格納され ちゃってるから、特に指定はしてない。シリアルラインをconsoleとして扱かう様 にOSを設定してる。で、外部からssh接続できるように、NICを生やしている。 -sを指定してるんで、カーネルをgdbに アタッチ出来る。しかもユーザーランドはソースからコンパイルしてgdb可能。

Others

README

口笛のはなし なんて本を読んだ。

口笛 武田裕煕 とかで検索すると著者の演奏を聞く事ができる。 これが口笛か。人体楽器。オペラの歌手も凄いけど、こちらも負けず劣らず凄い。

この口笛がどうして出てくるのは、正確に解明されていないそうな。そんなのFFTすれば いいんでないかい。で、分析したら割りと純音に近いそうな。そんなものですかい。 まあ、細かい事は言わずに単純に楽しむのが良さそうです。

口笛言語 興味深いな。 口笛教室 これ楽しいな。


This year's Index

Home