fs: devtmpfs, so /dev is generated rather than written to a disk
A fresh boot spent 2600 ms in schinit's bootstrap() before printing its first
line, and the whole of it was device nodes. Measured rather than guessed: an
instrumented bootstrap reported 2600 ms on a fresh image and 0 ms on the next
boot, because the nodes already existed by then and mknod failed fast. The cost
was therefore invisible to anyone not building images -- which is to say,
invisible to everyone except whoever works on this.
Each node was a real inode on the root filesystem, and SchFS commits every
mutation as its own transaction group: a copy-on-write rewrite of the path to
the root, then an uberblock behind a cache flush. Twenty of those is seconds.
The duplication mattered more than the time. Major and minor numbers are the
kernel's -- devsw[] in kernel/dev/cdev.c is what they dispatch on -- and schinit
held a second copy of that mapping with nothing checking the two agreed. Adding
a character device meant editing a userspace program, and a wrong minor produced
a node that opened successfully and did nothing.
kernel/fs/devfs.c puts the table next to the thing it describes and serves /dev
behind the same fs_ops vtable procfs uses. The namespace is all it provides:
fileread and filewrite see T_DEV and go through devsw themselves, so a read
never reaches it. It is read-only, and dirlink and ialloc answer EROFS rather
than pretending -- a node exists because the kernel has the device, or it does
not exist.
This also makes /proc/filesystems true. It has advertised "nodev devtmpfs" for
as long as it has existed, against a kernel that had no such filesystem.
Two things found while measuring what remained:
* The thirteen mknod() calls for /proc ran *after* the procfs mount, so every
one was resolving a path into a read-only synthetic filesystem and failing.
procfs.c's own note called them a fallback to be cleaned up later; they have
been dead for longer than that.
* The four mkdir() calls for mount points were the rest of it. They are made
by the image builder now (sysroot-install), and the ones in init are a
fallback -- a mkdir that fails because the directory exists costs a path
walk and no writes, where creating one costs a transaction.
bootstrap() is now 0 ms on a fresh image, down from 2600.
init opens /dev/console after the mount rather than mknod'ing a node called
"console" into the root directory, which is what used to leave a stray /console
in `ls /`. The old path is kept as a fallback: if the mount fails there is no
/dev/console and no way to report it.
socktest passes, /proc still lists per-process directories, ps -e still
enumerates.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01VQ7Qx8E1q1M1bbCq8QLHS2
This commit is contained in:
parent
d97dd9ed90
commit
c874811679
8 changed files with 392 additions and 59 deletions
|
|
@ -132,6 +132,47 @@ Three things worth knowing:
|
|||
length until it is generated, and generating every one on every `stat()` to
|
||||
fill in a number almost nothing reads would be absurd.
|
||||
|
||||
## devtmpfs
|
||||
|
||||
`/dev` is generated, not stored. `kernel/fs/devfs.c`, behind the same `fs_ops`
|
||||
vtable, mounted by init with `mount("dev", "/dev", "devtmpfs", 0, 0)`.
|
||||
|
||||
It used to be twenty `mknod()` calls in `cmd/schinit`, which was wrong twice
|
||||
over:
|
||||
|
||||
**The cost.** Every node was a real inode on the root filesystem, and SchFS
|
||||
commits each mutation as its own transaction group — a copy-on-write rewrite of
|
||||
the path to the root, then an uberblock behind a cache flush. Measured at
|
||||
**2600 ms of a first boot**, spent before init printed its first line. On later
|
||||
boots the nodes already existed and the same code took 0 ms, so the cost was
|
||||
invisible to anyone not building fresh images — which is to say, invisible to
|
||||
everyone except whoever works on the system.
|
||||
|
||||
**The duplication, which matters more.** Major and minor numbers are the
|
||||
kernel's: they are what `devsw[]` in `kernel/dev/cdev.c` dispatches on. init
|
||||
held a second copy of that mapping and nothing checked the two agreed. Adding a
|
||||
character device meant editing a userspace program, and a wrong minor there
|
||||
produced a node that opened successfully and did nothing.
|
||||
|
||||
The table now sits next to the thing it describes. The namespace is all devfs
|
||||
provides — reads and writes never reach it, because `fileread`/`filewrite` see
|
||||
`T_DEV` and dispatch through `devsw` themselves.
|
||||
|
||||
It is read-only: `dirlink` and `ialloc` answer `EROFS` rather than pretending.
|
||||
A node exists because the kernel has the device, or it does not exist.
|
||||
|
||||
### What is there
|
||||
|
||||
`console`, `null`, `keyboard`, `zero`, `full`, `random`, `urandom`, `tty`,
|
||||
`stdin`, `stdout`, `stderr`, `fb0`, `mouse`, `hda`, `hdb`, and `input/event0`
|
||||
and `input/event1` in one subdirectory. Minors under major 1 follow Linux's
|
||||
mem-major layout where it costs nothing.
|
||||
|
||||
The raw disks are created whether or not a drive is present: opening one with
|
||||
nothing behind it fails with `ENXIO`, which is a better answer than a missing
|
||||
path — and with no node at all, `echo x > /dev/hdb` silently creates an ordinary
|
||||
file of that name and appears to work.
|
||||
|
||||
### What is there
|
||||
|
||||
Per-process directories, plus the global files: `meminfo`, `uptime`, `cpuinfo`,
|
||||
|
|
|
|||
Loading…
Reference in a new issue