From c874811679799b8e2e33e45db9aebc9fc5aaf845 Mon Sep 17 00:00:00 2001 From: Nexoniarz Date: Sat, 12 Sep 2026 20:18:30 +0200 Subject: [PATCH] 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 Claude-Session: https://claude.ai/code/session_01VQ7Qx8E1q1M1bbCq8QLHS2 --- Makefile | 13 +- cmd/schinit/main.c | 97 +++++------ docs/architecture/filesystem.md | 41 +++++ include/defs.h | 1 + include/file.h | 3 + kernel/fs/devfs.c | 287 ++++++++++++++++++++++++++++++++ kernel/fs/fs.c | 2 + kernel/sys/sys_fs.c | 7 + 8 files changed, 392 insertions(+), 59 deletions(-) create mode 100644 kernel/fs/devfs.c diff --git a/Makefile b/Makefile index 479e7ac..a0f9a9d 100644 --- a/Makefile +++ b/Makefile @@ -49,7 +49,7 @@ KERN_DRIVERS += $(BUILD)/kernel/drivers/usb/storage/usb_storage.o KERN_DRIVERS += $(BUILD)/kernel/drivers/usb/hub/usb_hub.o KERN_FS = $(addprefix $(BUILD)/kernel/fs/, \ - exec.o file.o fs.o log.o pipe.o procfs.o) + exec.o file.o fs.o log.o pipe.o procfs.o devfs.o) # SchFS. Every one of these is the very same source the host tools compile, so # the kernel and mkschfs cannot disagree about the on-disk format: there is one @@ -918,7 +918,18 @@ etc-install: $(SCHINIT_JOBS) $(ETC_PLAIN) $(LOGO_ASCII) @cp $(LOGO_ASCII) $(SYSROOT)/usr/share/schos/logo.txt @echo "installed $(words $(ETC_PLAIN)) /etc file(s) and $(words $(SCHINIT_JOBS)) schinit manifest(s)" +# The mount points. Created here rather than by init, because a mkdir on a +# fresh SchFS is a whole transaction -- a copy-on-write rewrite of the path to +# the root and an uberblock behind a cache flush -- and four of them were most +# of what remained of a 2.6 second first boot. Making them at image-build time +# costs nothing at all. +# +# Not committed as empty directories with a .gitkeep, because /sys and /run are +# not mounted over and the marker file would be visible in them. +SYSROOT_DIRS = dev proc sys run tmp + sysroot-install: $(addprefix $(SYSROOT)/bin/, $(CMDS)) etc-install + @mkdir -p $(addprefix $(SYSROOT)/, $(SYSROOT_DIRS)) ifdef WINHOST @echo "ports: skipped (Windows host) -- musl/mksh/toybox are used as already staged in $(SYSROOT)" @test -f $(SYSROOT)/bin/sh || { echo "ports: $(SYSROOT)/bin/sh is missing and cannot be built here; build ports on a Linux host once" >&2; exit 1; } diff --git a/cmd/schinit/main.c b/cmd/schinit/main.c index ac0ae2b..c67ec9e 100644 --- a/cmd/schinit/main.c +++ b/cmd/schinit/main.c @@ -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"); } diff --git a/docs/architecture/filesystem.md b/docs/architecture/filesystem.md index 9ba5eea..59328f3 100644 --- a/docs/architecture/filesystem.md +++ b/docs/architecture/filesystem.md @@ -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`, diff --git a/include/defs.h b/include/defs.h index bd9a93f..814e68b 100644 --- a/include/defs.h +++ b/include/defs.h @@ -296,6 +296,7 @@ int procwrite(struct inode*, uint64, char*, int); int proc_gen_global(int id, char *buf, int cap); int proc_write_sysrq(int user_src, uint64 src, uint n); void procfs_mount(void); +void devfs_mount(void); // input.c -- raw evdev event devices void inputinit(void); diff --git a/include/file.h b/include/file.h index c01e62d..e4fb461 100644 --- a/include/file.h +++ b/include/file.h @@ -149,6 +149,9 @@ struct inode { // The synthetic /proc. #define PROCFS_DEV 80 +// The synthetic /dev. See kernel/fs/devfs.c. +#define DEVFS_DEV 81 + // table mapping major device number to // device functions struct devsw { diff --git a/kernel/fs/devfs.c b/kernel/fs/devfs.c new file mode 100644 index 0000000..8ff2f8b --- /dev/null +++ b/kernel/fs/devfs.c @@ -0,0 +1,287 @@ +// SPDX-License-Identifier: Apache-2.0 +// +// devtmpfs: /dev, generated rather than stored. +// +// It used to be ~20 mknod() calls in cmd/schinit, which was wrong in two ways +// that are worth separating. +// +// *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: +// 2600 ms of a first boot spent writing device nodes to a disk, before schinit +// printed its first line. On later boots the nodes already existed and the same +// code took 0 ms, so the cost was invisible unless you built a fresh image -- +// which is exactly what anyone working on the system does every time. +// +// *The duplication, which matters more.* Major and minor numbers are the +// kernel's: they are what devsw[] in kernel/dev/cdev.c dispatches on. schinit +// held a second copy of that mapping and nothing checked the two agreed. A new +// character device meant editing a userspace program, and getting a minor wrong +// there produced a node that opened successfully and did nothing. +// +// So the table below sits next to the thing it describes, and /dev is a +// mount rather than a directory somebody populates. `mount("dev", "/dev", +// "devtmpfs", 0, 0)` is the whole of what init does now. +// +// The namespace is all this provides. Reads and writes never reach here -- +// fileread/filewrite see T_DEV and dispatch through devsw themselves -- which +// is why there is no read op below that does anything. + +#include "types.h" +#include "defs.h" +#include "param.h" +#include "stat.h" +#include "spinlock.h" +#include "sleeplock.h" +#include "proc.h" +#include "fs.h" +#include "file.h" +#include "errno.h" + +// Inode numbering. Two directories and a flat table of nodes, so the number is +// either a directory id or an index into devnodes[] offset past them. +#define DEVFS_ROOT 1ULL +#define DEVFS_INPUT 2ULL /* the /dev/input directory */ +#define DEVFS_FILE0 16ULL /* devnodes[0] is inode 16 */ + +// Which directory a node lives in. Only two exist and a third would want a real +// tree rather than a flag, which is the point at which this file should grow +// one instead of adding DEV_DIR_THIRD. +#define D_ROOT 0 +#define D_INPUT 1 + +struct devnode { + const char *name; + short major; + short minor; + short dir; +}; + +// The devices this kernel implements, in the order they appear in a listing. +// +// 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), which is worth keeping +// because it is one less arbitrary mapping for anyone reading both. +// +// The majors are devsw[] indices and nothing else; see kernel/dev/cdev.c. +static const struct devnode devnodes[] = { + { "console", 1, 1, D_ROOT }, + { "null", 1, 2, D_ROOT }, + { "keyboard",1, 3, D_ROOT }, + { "zero", 1, 5, D_ROOT }, + { "full", 1, 7, D_ROOT }, + { "random", 1, 8, D_ROOT }, + { "urandom", 1, 9, D_ROOT }, + { "tty", 1, 1, D_ROOT }, /* alias for the console */ + { "stdin", 1, 1, D_ROOT }, + { "stdout", 1, 1, D_ROOT }, + { "stderr", 1, 1, D_ROOT }, + { "fb0", 2, 0, D_ROOT }, + { "mouse", 3, 1, D_ROOT }, + + // Raw disks. Created whether or not a drive is present: opening one with no + // disk 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. + { "hda", 8, 0, D_ROOT }, + { "hdb", 8, 1, D_ROOT }, + + // Raw input events, Linux evdev layout. The minor is the device index, so + // event0 is the keyboard and event1 the mouse. + { "event0", 7, 0, D_INPUT }, + { "event1", 7, 1, D_INPUT }, +}; + +#define NDEVNODES ((int)(sizeof(devnodes) / sizeof(devnodes[0]))) + +static int +node_of(uint64 inum) +{ + if (inum < DEVFS_FILE0) + return -1; + uint64 i = inum - DEVFS_FILE0; + return i < (uint64)NDEVNODES ? (int)i : -1; +} + +static void +devfs_iload(struct inode *ip) +{ + int i = node_of(ip->inum); + + ip->major = ip->minor = 0; + ip->size = 0; + + if (ip->inum == DEVFS_ROOT || ip->inum == DEVFS_INPUT) { + ip->type = T_DIR; + ip->nlink = 2; + } else if (i >= 0) { + ip->type = T_DEV; + ip->nlink = 1; + ip->major = devnodes[i].major; + ip->minor = devnodes[i].minor; + } else { + ip->type = 0; /* no such node */ + ip->nlink = 0; + } + ip->valid = 1; +} + +static void devfs_istore(struct inode *ip) { (void)ip; } +static void devfs_ifree(struct inode *ip) { (void)ip; } +static void devfs_truncate(struct inode *ip, uint64 n) { (void)ip; (void)n; } + +// Never reached for a device node: fileread and filewrite see T_DEV and go +// through devsw. Present because fs_ops has the slots, and answering -EIO is +// better than a null pointer if something ever does arrive here. +static int devfs_read(struct inode *ip, int u, uint64 d, uint64 o, uint n) +{ (void)ip; (void)u; (void)d; (void)o; (void)n; return -1; } +static int devfs_write(struct inode *ip, int u, uint64 s, uint64 o, uint n) +{ (void)ip; (void)u; (void)s; (void)o; (void)n; return -1; } + +static int +streq(const char *a, const char *b) +{ + while (*a && *a == *b) { a++; b++; } + return *a == *b; +} + +static struct inode * +devfs_dirlookup(struct inode *dp, char *name, uint *poff) +{ + if (poff) + *poff = 0; + + if (streq(name, ".") || (streq(name, "..") && dp->inum == DEVFS_ROOT)) + return iget(DEVFS_DEV, dp->inum); + if (streq(name, "..")) + return iget(DEVFS_DEV, DEVFS_ROOT); /* out of /dev/input */ + + if (dp->inum == DEVFS_ROOT && streq(name, "input")) + return iget(DEVFS_DEV, DEVFS_INPUT); + + short want = dp->inum == DEVFS_INPUT ? D_INPUT : D_ROOT; + for (int i = 0; i < NDEVNODES; i++) + if (devnodes[i].dir == want && streq(name, devnodes[i].name)) + return iget(DEVFS_DEV, DEVFS_FILE0 + (uint64)i); + + return 0; +} + +// A read-only filesystem, and these say so rather than pretending. mknod on +// /dev returning EROFS is the correct answer: the node either exists because +// the kernel has the device, or it does not exist because it has not. +static int devfs_dirlink(struct inode *dp, char *name, uint64 inum) +{ (void)dp; (void)name; (void)inum; return -EROFS; } +static int devfs_dirunlink(struct inode *dp, char *name, uint off) +{ (void)dp; (void)name; (void)off; return -EROFS; } +static int devfs_dirempty(struct inode *dp) { (void)dp; return 0; } +static struct inode *devfs_ialloc(uint dev, short type) +{ (void)dev; (void)type; return 0; } + +struct linux_dirent64 { + uint64 d_ino; + int64 d_off; + uint16 d_reclen; + uchar d_type; + char d_name[]; +}; + +static int +emit(char *out, int cap, int *used, uint64 ino, uchar type, const char *name) +{ + int nl = (int)strlen(name); + int rec = (int)((sizeof(struct linux_dirent64) + nl + 1 + 7) & ~7); + + if (*used + rec > cap) + return -1; /* caller stops and resumes next call */ + + struct linux_dirent64 *d = (struct linux_dirent64 *)(out + *used); + d->d_ino = ino; + d->d_off = 0; + d->d_reclen = (uint16)rec; + d->d_type = type; + memmove(d->d_name, name, nl); + memset(d->d_name + nl, 0, rec - (int)sizeof(*d) - nl); + *used += rec; + return 0; +} + +// `off` is an ordinal over the entries of this directory: 0 is ".", 1 is "..", +// and the rest follow. An opaque cookie the way SchFS uses one buys nothing +// here, because the table is fixed and small enough to index directly. +static int +devfs_getdents(struct inode *dp, char *out, int cap, uint64 *poff) +{ + int used = 0; + uint64 n = *poff; + + if (dp->inum != DEVFS_ROOT && dp->inum != DEVFS_INPUT) + return -1; + + if (n == 0) { + if (emit(out, cap, &used, dp->inum, 4 /* DT_DIR */, ".") < 0) return -1; + n++; + } + if (n == 1) { + uint64 up = dp->inum == DEVFS_ROOT ? DEVFS_ROOT : DEVFS_ROOT; + if (emit(out, cap, &used, up, 4, "..") < 0) goto out; + n++; + } + + // /dev/input is a directory entry of the root, and it is emitted between the + // dots and the nodes so that its ordinal is stable. + if (dp->inum == DEVFS_ROOT && n == 2) { + if (emit(out, cap, &used, DEVFS_INPUT, 4, "input") < 0) goto out; + n++; + } + + short want = dp->inum == DEVFS_INPUT ? D_INPUT : D_ROOT; + uint64 base = dp->inum == DEVFS_ROOT ? 3 : 2; + + for (int i = 0; i < NDEVNODES; i++) { + if (devnodes[i].dir != want) + continue; + if (n > base) { base++; continue; } /* already emitted */ + if (emit(out, cap, &used, DEVFS_FILE0 + (uint64)i, 2 /* DT_CHR */, + devnodes[i].name) < 0) + goto out; + n++; + base++; + } + +out: + *poff = n; + return used; +} + +const struct fs_ops devfs_ops = { + .name = "devtmpfs", + .iload = devfs_iload, + .istore = devfs_istore, + .ifree = devfs_ifree, + .read = devfs_read, + .write = devfs_write, + .truncate = devfs_truncate, + .dirlookup = devfs_dirlookup, + .dirlink = devfs_dirlink, + .dirunlink = devfs_dirunlink, + .dirempty = devfs_dirempty, + .ialloc = devfs_ialloc, + .getdents = devfs_getdents, +}; + +// Graft it over /dev. Called by init, which has made the directory. +void +devfs_mount(void) +{ + struct inode *dp = namei("/dev"); + + if (dp == 0) { + cprintf("devfs: /dev does not exist; not mounted\n"); + return; + } + if (mount_add(dp->dev, dp->inum, DEVFS_DEV, DEVFS_ROOT, "/dev", "dev", + "devtmpfs") < 0) + cprintf("devfs: /dev already has something mounted on it\n"); + iput(dp); +} diff --git a/kernel/fs/fs.c b/kernel/fs/fs.c index cf374e1..8a749a4 100644 --- a/kernel/fs/fs.c +++ b/kernel/fs/fs.c @@ -279,6 +279,7 @@ static const struct fs_ops v1_ops = { extern const struct fs_ops schfs_ops; // Defined in fs/procfs.c -- the synthetic /proc. extern const struct fs_ops procfs_ops; +extern const struct fs_ops devfs_ops; // Is this filesystem read-only? // @@ -300,6 +301,7 @@ ops_for(uint dev) // kmount.c turns that device into the right volume. if (SCHFS_IS_ANY_DEV(dev)) return &schfs_ops; if (dev == PROCFS_DEV) return &procfs_ops; + if (dev == DEVFS_DEV) return &devfs_ops; return &v1_ops; } diff --git a/kernel/sys/sys_fs.c b/kernel/sys/sys_fs.c index a3cfad7..f97320c 100644 --- a/kernel/sys/sys_fs.c +++ b/kernel/sys/sys_fs.c @@ -1508,6 +1508,13 @@ addr_t sys_mount(void) return 0; } + // devtmpfs: the same, and the one init actually needs. /dev used to be + // twenty mknod() calls; see the note at the top of kernel/fs/devfs.c. + if (strncmp(fstype, "devtmpfs", 9) == 0 || strncmp(fstype, "dev", 4) == 0) { + devfs_mount(); + return 0; + } + // Only SchFS. Checking the name still matters: a caller that asked for // something else should be told no rather than quietly handed this. if (strncmp(fstype, "schfs", 6) != 0)