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
|
|
@ -410,75 +410,56 @@ job_by_pid(int pid)
|
|||
static void
|
||||
bootstrap(void)
|
||||
{
|
||||
// Console first, as fd 0/1/2, so every message below has somewhere to go.
|
||||
if (open("console", O_RDWR) < 0) {
|
||||
mknod("console", 1, 1);
|
||||
open("console", O_RDWR);
|
||||
// /dev before the console, because the console now lives in it.
|
||||
//
|
||||
// The old order was the other way round -- open a console first so every
|
||||
// message below had somewhere to go -- and the fallback below is what keeps
|
||||
// that guarantee. If the mount fails there is no /dev/console and no way to
|
||||
// say so, so the original path stays: a node called `console` in the root
|
||||
// directory, which is where this used to put one.
|
||||
//
|
||||
// /dev is a filesystem now, not a pile of nodes. Every major and minor it
|
||||
// contains is the kernel's -- devsw[] in kernel/dev/cdev.c is what they
|
||||
// dispatch on -- and this program used to hold a second copy of that
|
||||
// mapping with nothing checking the two agreed.
|
||||
//
|
||||
// It was also slow in a way that only showed on a fresh image: each mknod
|
||||
// was one SchFS transaction, and the twenty of them cost 2600 ms of the
|
||||
// first boot before this program printed its first line. On a second boot
|
||||
// the nodes already existed and the same code took 0 ms, so the cost was
|
||||
// invisible to anyone not building images -- which is to say, invisible to
|
||||
// everyone except whoever works on this.
|
||||
mkdir("/dev");
|
||||
mount("dev", "/dev", "devtmpfs", 0, 0);
|
||||
|
||||
if (open("/dev/console", O_RDWR) < 0) {
|
||||
// No devtmpfs. Fall back to a real node on the root filesystem, which
|
||||
// is what every boot did before this.
|
||||
if (open("console", O_RDWR) < 0) {
|
||||
mknod("console", 1, 1);
|
||||
open("console", O_RDWR);
|
||||
}
|
||||
}
|
||||
dup(0);
|
||||
dup(0);
|
||||
|
||||
// Minor numbers under major 1 follow Linux's mem-major layout where it
|
||||
// costs nothing (null=2, zero=5, full=7, random=8, urandom=9).
|
||||
mkdir("/dev");
|
||||
mknod("/dev/console", 1, 1);
|
||||
mknod("/dev/null", 1, 2);
|
||||
mknod("/dev/keyboard", 1, 3);
|
||||
mknod("/dev/zero", 1, 5);
|
||||
mknod("/dev/full", 1, 7);
|
||||
mknod("/dev/random", 1, 8);
|
||||
mknod("/dev/urandom", 1, 9);
|
||||
mknod("/dev/fb0", 2, 0);
|
||||
mknod("/dev/mouse", 3, 1);
|
||||
mknod("/dev/tty", 1, 1); // alias for console -- must pass TCGETS
|
||||
mknod("/dev/ttyS0", 4, 64);
|
||||
mknod("/dev/stdin", 1, 1);
|
||||
mknod("/dev/stdout", 1, 1);
|
||||
mknod("/dev/stderr", 1, 1);
|
||||
|
||||
// Raw input events, Linux evdev layout (kernel/dev/input.c). /dev/keyboard
|
||||
// and /dev/mouse above are the old character-and-position interfaces;
|
||||
// these are the ones with key releases and button transitions, which is
|
||||
// what a game or a compositor needs and neither of the others express.
|
||||
mkdir("/dev/input");
|
||||
mknod("/dev/input/event0", 7, 0); // keyboard
|
||||
mknod("/dev/input/event1", 7, 1); // mouse
|
||||
|
||||
// Raw disks (kernel/dev/disk.c). The minor is the IDE device number, so
|
||||
// hda is device 0 and hdb device 1 -- the same numbering iderw() uses.
|
||||
//
|
||||
// Created unconditionally, like everything else here: opening one when no
|
||||
// drive is present fails with ENXIO, which is a better answer than a
|
||||
// missing path. It also matters that these exist even when absent -- with
|
||||
// no node, `echo x > /dev/hdb` silently creates an ordinary file of that
|
||||
// name and appears to succeed, which is a confusing way to discover the
|
||||
// node was never made.
|
||||
mknod("/dev/hda", 8, 0);
|
||||
mknod("/dev/hdb", 8, 1);
|
||||
|
||||
// /proc is a real filesystem (kernel/fs/procfs.c), not a handful of nodes:
|
||||
// it has to be, because one directory per process cannot be created ahead
|
||||
// of time. That is what makes ps and top work.
|
||||
//
|
||||
// The mknod'd global files still happen and are simply covered by the
|
||||
// mount, so a failure there degrades to the old behaviour rather than
|
||||
// losing /proc entirely.
|
||||
// The thirteen mknod() calls that used to follow this mount are gone. They
|
||||
// ran *after* it, so every one of them was resolving a path into a
|
||||
// read-only synthetic filesystem and failing -- dead weight kept as a
|
||||
// fallback for a mount that has not failed in a long time, exactly as the
|
||||
// note in procfs.c said it was.
|
||||
mkdir("/proc");
|
||||
mount("proc", "/proc", "proc", 0, 0);
|
||||
mknod("/proc/meminfo", 6, 1);
|
||||
mknod("/proc/uptime", 6, 2);
|
||||
mknod("/proc/cpuinfo", 6, 3);
|
||||
mknod("/proc/version", 6, 4);
|
||||
mknod("/proc/stat", 6, 5);
|
||||
mknod("/proc/loadavg", 6, 6);
|
||||
mknod("/proc/mounts", 6, 7);
|
||||
mknod("/proc/filesystems", 6, 8);
|
||||
mknod("/proc/schfs", 6, 9);
|
||||
mknod("/proc/sysrq", 6, 10); // echo c > here to crash on purpose
|
||||
mknod("/proc/pci", 6, 11);
|
||||
mknod("/proc/interrupts", 6, 12);
|
||||
mknod("/proc/devices", 6, 13);
|
||||
|
||||
// Mount points, and nothing mounts on them yet. They are created by the
|
||||
// image builder (see sysroot-install), so these are a fallback for an image
|
||||
// that does not have them -- a mkdir that fails because the directory is
|
||||
// already there costs a path walk and no writes, where creating one costs a
|
||||
// whole SchFS transaction.
|
||||
mkdir("/sys");
|
||||
mkdir("/run");
|
||||
}
|
||||
|
|
|
|||
Loading…
Reference in a new issue