SchOS/kernel/fs/devfs.c
Nexoniarz 2a441af3c6 ldls: a network, in a process
docs/plan/ldls.md L3a -- the class bridge, and the first of the three pieces
L3 turned out to be.

/dev/netdev is /dev/pci with the arrows reversed. That hands a device to a
process; this lets a process be one. The kernel side is netdev_ops and nothing
else -- transmit, receive, mac, link_up, the same four entries e1000
publishes -- so a process claims the device, supplies a MAC, and from then on
read() gives it frames the stack wants sent and write() hands it frames that
arrived.

cmd/netecho proves it and is deliberately not a driver: no hardware, none
claimed, every frame answered by the program itself. DHCP so the interface
gets an address at all, ARP so the stack can find it, ICMP so it can be
pinged. All built by hand from RFCs 826, 791, 792 and 2131.

    # netecho &
    netuser: ldls0 is a process now, 2:4c:44:4c:53:1
    netecho: I am 10.9.0.1 and will offer 10.9.0.2
    netecho: answered DHCP, offering an address

    # cat /proc/netif
    interface ldls0   driver netuser   address 10.9.0.2   gateway 10.9.0.1

    # ping 10.9.0.1 3
    netecho: answered an ARP
    netecho: answered a ping
    3 packets transmitted, 3 received, 0% packet loss

The interface got its address by DHCP from a process and then answered ping
from one. lwIP never learns that the thing behind netdev_ops is not a driver,
which is the whole property the bridge exists to have.

It dies the way it should, too. kill the process and link_up() answers false
immediately, so the stack refuses to send -- "ping: sendto: -5" -- rather than
swallowing packets into a driver that is not there. Start another and the
*same* interface comes back, keeping its address. That came free:
netuser_release_all() sits beside pciuser_release_all() in exit() for exactly
the same reason, and the reason is that a process holding a device is one that
may crash.

One thing the kernel needed: netinit() starts DHCP on the devices that exist
when it runs, so an interface created later -- which is how an LDLS-hosted
driver arrives -- would have come up with no address and no way to get one.
net_attach_late() fixes that, and lives in kernel/net/net.c because that is
the only file allowed to include lwIP's headers.

Recorded as not done, because it is not: this proves the bridge, not a driver.
Nothing in that test loads a module. That is why it was done first -- if ping
had not worked here the bridge would be wrong, and now a failure with a real
driver can only be the driver's side. L3b is the SchOS-side host, L3c is the
fifteen symbols for real, and L3 is the two joined.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01VQ7Qx8E1q1M1bbCq8QLHS2
2026-09-14 08:31:06 +02:00

300 lines
9.8 KiB
C

// 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 },
/* /dev/tty is the caller's *controlling* terminal, which is a different
thing from the console and used to be the same thing only because there
was one terminal. Minor 10 resolves through proc->ctty; see
vt_for_minor() in kernel/core/console.c. */
{ "tty", 1, 10, D_ROOT },
{ "tty1", 1, 11, D_ROOT },
{ "tty2", 1, 12, D_ROOT },
{ "tty3", 1, 13, D_ROOT },
{ "tty4", 1, 14, D_ROOT },
{ "tty5", 1, 15, D_ROOT },
{ "tty6", 1, 16, D_ROOT },
{ "stdin", 1, 1, D_ROOT },
{ "stdout", 1, 1, D_ROOT },
{ "stderr", 1, 1, D_ROOT },
{ "fb0", 2, 0, D_ROOT },
{ "pci", 9, 0, D_ROOT },
{ "dbg", 10, 0, D_ROOT },
{ "netdev", 11, 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);
}