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
13
Makefile
13
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; }
|
||||
|
|
|
|||
|
|
@ -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.
|
||||
// /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");
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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`,
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
|
|
|
|||
|
|
@ -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 {
|
||||
|
|
|
|||
287
kernel/fs/devfs.c
Normal file
287
kernel/fs/devfs.c
Normal file
|
|
@ -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);
|
||||
}
|
||||
|
|
@ -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;
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
Loading…
Reference in a new issue