SchOS/kernel/dev/procfs.c
Nexoniarz 20cc673a41 net: a network *class*, so the stack stops knowing what an e1000 is
The TCP/IP stack reached straight into the first driver anyone wrote.
net.c called e1000_present(), the lwIP glue was named netif_e1000.c and
called e1000_transmit(), and include/defs.h declared the driver's
internals to the whole kernel. The second NIC driver would therefore
have arrived with a near-identical copy of the glue file and a patch to
the protocol stack, which is the arrangement include/device.h was
written to prevent -- the e1000 even said so, with `.ops = 0, // no
class vtable yet; there is no net class`.

There is now. A driver publishes a struct netdev_ops -- transmit,
receive, mac, link_up -- and is finished: it never mentions lwIP, and
nothing above it learns its name. The display class was already this
shape; this is the same shape for networking, and the two now read the
same way in kernel/dev/device.c.

What that made possible immediately:

  - One netif per bound device instead of one per driver. lwIP's glue is
    netif_eth.c, it takes a struct device, and it holds up to NETIF_MAX
    interfaces. net.c attaches every network device it finds and starts
    DHCP on each, because which interface the network answers on is not
    ours to decide.
  - /proc/netif lists all of them, one block of fields each, with the
    interface name and the driver that claimed it.
  - The ARP self-test moved from the e1000 to the class. Nothing in it
    was chip-specific, and it is the first thing anyone will want to run
    against the next driver. Still `echo n > /proc/sysrq`.
  - Private headers live with their driver: e1000.h was in include/,
    where exactly one file could use it. The driver build now adds the
    driver's own directory to the include path.

The e1000 also drives more than one card. Its probe() refused the second
outright, because there was a single static instance -- so a machine
with two of them drove one and silently ignored the other. Instances are
an array now, and the instance is hung on dev->priv by a new optional
`attached` hook that the bus calls once the device exists. probe() runs
before device_add(), so there was previously nowhere to put it; that is
the whole reason the hook exists rather than a line inside probe.

QEMU gets an explicit NIC on every target (QEMU_NET). The default device
happens to be an e1000 on this machine type today, which is exactly the
sort of thing that stays true until a QEMU release changes it and the
network stops being exercised without anyone noticing.

Verified:
  - bootcheck -Net passes, 48 [ok], no failures -- including the moved
    self-test and two new checks that /proc/netif names both the
    interface and the driver behind it, which together say the stack
    went through the class rather than through a driver it was compiled
    to know.
  - Two NICs attached by hand (-device e1000 twice, second with its own
    MAC): eth0 and eth1 both attach, each reports its own MAC in
    /proc/netif, and each gets its own DHCP lease. Before this commit
    the same machine produced one interface.
  - make schfs-check still passes in full.

One trap worth recording: moving a header broke the build for anyone
with stale .d files -- make stopped with "No rule to make target
'include/e1000.h'". CFLAGS gains -MP, which emits a phony target per
header so a moved or deleted one can never wedge the build again.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-15 16:42:34 +02:00

492 lines
16 KiB
C

// /proc — synthesised kernel status files.
//
// Each file is a character device node under major PROCFS_MAJOR, distinguished
// by its minor number, created by init at boot. Reading one renders the whole
// text into a scratch buffer and returns the slice starting at the caller's
// file offset, so a normal read-until-zero loop terminates.
//
// SCOPE: these are the *global* files. Per-process directories (/proc/<pid>/…)
// are not here, because they need something this kernel does not have yet:
// synthetic directories, i.e. a directory whose entries are generated at
// lookup and getdents time rather than read from disk. That is a real VFS
// feature touching iget/ilock/dirlookup/getdents, and it is the natural
// companion to the SchFS work rather than a bolt-on to the current fs.c.
// The namespace (which files exist, including one directory per process) is
// kernel/fs/procfs.c; this file only generates the *content* of the global
// files. `ps` and `top` work through the former.
#include "types.h"
#include "defs.h"
#include "irq.h"
#include "x86.h"
#include "device.h"
#include "display.h"
#include "netdev.h"
#include "param.h"
#include "memlayout.h"
#include "mmu.h"
#include "spinlock.h"
#include "sleeplock.h"
#include "fs.h"
#include "file.h"
#include "proc.h"
#include "uaccess.h"
#include "errno.h"
#include "fsstat.h"
#define PROC_BUFSZ 2048
// --- tiny formatting helpers (no snprintf in the kernel) -------------------
struct sbuf {
char *p;
int len;
int cap;
};
static void
sb_putc(struct sbuf *sb, char c)
{
if (sb->len < sb->cap - 1)
sb->p[sb->len++] = c;
}
static void
sb_puts(struct sbuf *sb, const char *s)
{
while (*s)
sb_putc(sb, *s++);
}
// A string in a fixed-width column, space-padded. cprintf has no width
// specifier and neither does this buffer, so tables build their own columns.
// Over-long values are allowed to run past their column rather than be
// truncated: a misaligned row is easier to read than a mangled name.
static void
sb_pad(struct sbuf *sb, const char *s, int width)
{
int n = 0;
while (s[n]) {
sb_putc(sb, s[n]);
n++;
}
while (n++ < width)
sb_putc(sb, ' ');
}
// Four lowercase hex digits, zero-padded. PCI and USB identifiers are always
// written this way, and a lookup table keyed on "8086:100e" does not match
// "8086:100E" or "8086:10e".
static void
sb_puthex4(struct sbuf *sb, uint32 v)
{
static const char hex[] = "0123456789abcdef";
for (int i = 3; i >= 0; i--)
sb_putc(sb, hex[(v >> (i * 4)) & 0xF]);
}
static void
sb_putu(struct sbuf *sb, uint64 v)
{
char tmp[24];
int i = 0;
if (v == 0) { sb_putc(sb, '0'); return; }
while (v && i < (int)sizeof(tmp)) { tmp[i++] = '0' + (v % 10); v /= 10; }
while (i--) sb_putc(sb, tmp[i]);
}
// "<label><value> <unit>\n" — the shape almost every /proc line takes.
static void
sb_kv(struct sbuf *sb, const char *label, uint64 v, const char *unit)
{
sb_puts(sb, label);
sb_putu(sb, v);
if (unit && *unit) { sb_putc(sb, ' '); sb_puts(sb, unit); }
sb_putc(sb, '\n');
}
// --- the files -------------------------------------------------------------
// PHYSTOP is the top of usable physical memory; kalloc hands out everything
// above the kernel image. Reporting the total honestly and leaving "free" as a
// share of it is better than the invented constants sysinfo() used to return.
#define TOTAL_KB (PHYSTOP / 1024)
static void
gen_meminfo(struct sbuf *sb)
{
uint64 total = TOTAL_KB;
uint64 freek = kalloc_free_bytes() / 1024;
sb_kv(sb, "MemTotal: ", total, "kB");
sb_kv(sb, "MemFree: ", freek, "kB");
sb_kv(sb, "MemAvailable: ", freek, "kB");
sb_kv(sb, "Buffers: ", 0, "kB");
sb_kv(sb, "Cached: ", 0, "kB");
sb_kv(sb, "SwapTotal: ", 0, "kB");
sb_kv(sb, "SwapFree: ", 0, "kB");
}
static void
gen_uptime(struct sbuf *sb)
{
uint t = uptime(); // timer ticks, 100 Hz
sb_putu(sb, t / 100);
sb_putc(sb, '.');
sb_putu(sb, (t % 100) / 10);
sb_putu(sb, (t % 100) % 10);
sb_puts(sb, " 0.00\n"); // idle time: not tracked
}
static void
gen_cpuinfo(struct sbuf *sb)
{
// Online CPUs, not present ones -- the same rule Linux follows, and the
// reason matters: something reading this to size a thread pool would
// otherwise be told a VM with four processors has four to schedule on, when
// three of them are enumerated and idle. "cpu cores" stays the present count,
// which is what that field means.
// Real values, from CPUID. These used to read "SchOS" and "SchOS virtual
// CPU" -- a thing to print when you have not asked the processor, and
// actively unhelpful, because identifying the machine is the one question
// /proc/cpuinfo exists to answer.
//
// Leaf 0 returns the vendor string in EBX:EDX:ECX, in that order, which is
// not the order anyone expects. Leaves 0x80000002..4 hold the 48-character
// brand string. Leaf 0x80000000 reports the highest extended leaf, and asking
// for one the CPU does not implement returns the highest supported leaf's
// value rather than an error -- so checking first is not optional.
char vendor[13], brand[49];
uint32 a, b, c, d;
cpuid_leaf(0, &a, &b, &c, &d);
*(uint32 *)&vendor[0] = b;
*(uint32 *)&vendor[4] = d;
*(uint32 *)&vendor[8] = c;
vendor[12] = '\0';
brand[0] = '\0';
cpuid_leaf(0x80000000, &a, 0, 0, 0);
if (a >= 0x80000004) {
for (int leaf = 0; leaf < 3; leaf++) {
cpuid_leaf(0x80000002 + leaf, &a, &b, &c, &d);
*(uint32 *)&brand[leaf * 16 + 0] = a;
*(uint32 *)&brand[leaf * 16 + 4] = b;
*(uint32 *)&brand[leaf * 16 + 8] = c;
*(uint32 *)&brand[leaf * 16 + 12] = d;
}
brand[48] = '\0';
}
// Intel pads the brand string with leading spaces, which read as a broken
// field rather than a name.
const char *model = brand;
while (*model == ' ')
model++;
if (*model == '\0')
model = "unknown";
for (int i = 0; i < ncpu_online; i++) {
sb_kv(sb, "processor\t: ", i, 0);
sb_puts(sb, "vendor_id\t: ");
sb_puts(sb, vendor);
sb_putc(sb, '\n');
sb_puts(sb, "model name\t: ");
sb_puts(sb, model);
sb_putc(sb, '\n');
sb_kv(sb, "cpu cores\t: ", ncpu, 0);
sb_putc(sb, '\n');
}
if (ncpu_online < ncpu) {
sb_kv(sb, "cpus present\t: ", ncpu, 0);
sb_puts(sb, "note\t\t: additional processors are enumerated but not "
"started; see docs/roadmap.md\n");
}
}
static void
gen_version(struct sbuf *sb)
{
// The kernel's name, not the system's -- same as "Linux version 6.x" here.
sb_puts(sb, "SchIFT version 0.1.0 (schift) #1 SMP x86_64\n");
}
static void
gen_stat(struct sbuf *sb)
{
uint t = uptime();
sb_puts(sb, "cpu ");
sb_putu(sb, t); sb_puts(sb, " 0 0 0 0 0 0 0 0 0\n");
// Online, for the same reason as cpuinfo: top and friends count the cpuN
// lines here to decide how many cores to draw.
for (int i = 0; i < ncpu_online; i++) {
sb_puts(sb, "cpu");
sb_putu(sb, i);
sb_putc(sb, ' ');
sb_putu(sb, t); sb_puts(sb, " 0 0 0 0 0 0 0 0 0\n");
}
sb_kv(sb, "processes ", (uint64)proc_num(), 0);
sb_kv(sb, "procs_running ", 1, 0);
sb_kv(sb, "btime ", 0, 0);
}
// Everything attached, whatever bus found it. The one place that answers "what
// is in this machine" across PCI, USB and platform devices at once -- which is
// the question a device manager is, so this is where one would read from.
//
// Columns rather than key/value: this is a table of like things, and a reader
// scanning for an unclaimed device wants the driver column to line up.
static void
gen_devices(struct sbuf *sb)
{
sb_puts(sb, "name bus class driver id device\n");
for (int i = 0; i < device_count(); i++) {
struct device *d = device_at(i);
if (!d)
break;
// 14 for the name and driver columns: "virtio-gpu0" is eleven characters
// and overran a ten-wide column, which sb_pad allows on purpose but which
// made the whole table unreadable.
sb_pad(sb, d->name, 14);
sb_pad(sb, dev_bus_name(d->bus), 10);
sb_pad(sb, dev_class_name(d->class), 10);
// An unclaimed device is the interesting row, so it gets a visible marker
// rather than an empty column.
sb_pad(sb, d->driver ? d->driver : "(none)", 14);
// vendor:product, the way lspci and lsusb print it. The kernel has no
// business resolving these to names -- that is what /usr/share/usb.ids is
// for, and it belongs to a userspace tool. What the kernel owes is the
// number, in the form a lookup expects.
if (d->vendor || d->product) {
sb_puthex4(sb, d->vendor);
sb_putc(sb, ':');
sb_puthex4(sb, d->product);
sb_putc(sb, ' ');
} else {
sb_puts(sb, " ");
}
sb_puts(sb, d->desc[0] ? d->desc : "-");
sb_putc(sb, '\n');
}
}
static void
gen_loadavg(struct sbuf *sb)
{
// No load tracking; report zeros but a truthful process count.
sb_puts(sb, "0.00 0.00 0.00 1/");
sb_putu(sb, (uint64)proc_num());
sb_puts(sb, " 1\n");
}
// df and mountpoint parse this.
//
// It used to be three fixed lines, which was true while there was one
// filesystem and no way to mount a second. Now `mount -t schfs /dev/hdb /mnt`
// works, and a mount missing from here is invisible to df -- it printed a
// header and no row, because df looks the path up in this list before it calls
// statfs at all.
static void
gen_mounts(struct sbuf *sb)
{
char path[MAXPATH], source[MAXPATH], type[16];
uint fsdev;
sb_puts(sb, "devtmpfs /dev devtmpfs rw 0 0\n");
for (int i = 0; i < 8; i++) {
if (mount_get(i, path, source, type, &fsdev) != 0)
continue;
// "/" is emitted last, below, so skip it if it somehow appears here.
if (path[0] == '/' && path[1] == '\0')
continue;
sb_puts(sb, source[0] ? source : "none");
sb_puts(sb, " ");
sb_puts(sb, path);
sb_puts(sb, " ");
sb_puts(sb, type[0] ? type : "schfs");
// A mounted snapshot is genuinely read-only, and saying so is what stops
// anything trying to write to one and reporting a confusing EROFS later.
sb_puts(sb, SCHFS_IS_SNAP_DEV(fsdev) ? " ro 0 0\n" : " rw 0 0\n");
}
// Root must be last: df keeps the last mount point that prefixes the path
// it was asked about, so listing / first made `df /` report /proc instead.
sb_puts(sb, "/dev/root / schfs rw 0 0\n");
}
// Where the time goes in the filesystem and block paths.
//
// Cycles, not seconds: the numbers are only ever compared against each other,
// and converting would mean knowing the TSC frequency, which this kernel does
// not. What matters is the ratios and the miss counts.
static void
gen_fsstat(struct sbuf *sb)
{
sb_puts(sb, "bread_calls "); sb_putu(sb, fsstat.bread_calls);
sb_puts(sb, "\nbread_miss "); sb_putu(sb, fsstat.bread_miss);
sb_puts(sb, "\nbget_evict_dirty "); sb_putu(sb, fsstat.bget_evict_dirty);
sb_puts(sb, "\ndisk_reads "); sb_putu(sb, fsstat.disk_reads);
sb_puts(sb, "\ndisk_writes "); sb_putu(sb, fsstat.disk_writes);
sb_puts(sb, "\ndisk_kcycles "); sb_putu(sb, fsstat.disk_cycles / 1000);
sb_puts(sb, "\nread_calls "); sb_putu(sb, fsstat.read_calls);
sb_puts(sb, "\nread_bytes "); sb_putu(sb, fsstat.read_bytes);
sb_puts(sb, "\nread_kcycles "); sb_putu(sb, fsstat.read_cycles / 1000);
sb_puts(sb, "\ngetdents_calls "); sb_putu(sb, fsstat.getdents_calls);
sb_puts(sb, "\ngetdents_walked "); sb_putu(sb, fsstat.getdents_walked);
sb_puts(sb, "\ngetdents_kcycles "); sb_putu(sb, fsstat.getdents_cycles / 1000);
sb_puts(sb, "\ndescents "); sb_putu(sb, fsstat.descents);
sb_puts(sb, "\nexec_calls "); sb_putu(sb, fsstat.exec_calls);
sb_puts(sb, "\nexec_kcycles "); sb_putu(sb, fsstat.exec_cycles / 1000);
sb_puts(sb, "\n");
}
static void
gen_filesystems(struct sbuf *sb)
{
sb_puts(sb, "\tschfs\n");
sb_puts(sb, "nodev\tproc\n");
sb_puts(sb, "nodev\tdevtmpfs\n");
}
// --- dispatch --------------------------------------------------------------
#define PROC_MEMINFO 1
#define PROC_UPTIME 2
#define PROC_CPUINFO 3
#define PROC_VERSION 4
#define PROC_STAT 5
#define PROC_LOADAVG 6
#define PROC_MOUNTS 7
#define PROC_FILESYSTEMS 8
#define PROC_SCHFS 9
#define PROC_SYSRQ 10
#define PROC_PCI 11
#define PROC_INTERRUPTS 12
#define PROC_DEVICES 13
#define PROC_FSSTAT 14
#define PROC_NETIF 15
// Render global file `id` (a PROC_* constant) into `buf`. Returns its length.
//
// Split out of procread() so the /proc *filesystem* (kernel/fs/procfs.c) can
// use the same generators: that file owns the namespace, this one owns the
// content, and they have no other business with each other.
int
proc_gen_global(int id, char *buf, int cap)
{
struct sbuf sb = { buf, 0, cap };
switch (id) {
case PROC_MEMINFO: gen_meminfo(&sb); break;
case PROC_UPTIME: gen_uptime(&sb); break;
case PROC_CPUINFO: gen_cpuinfo(&sb); break;
case PROC_VERSION: gen_version(&sb); break;
case PROC_STAT: gen_stat(&sb); break;
case PROC_LOADAVG: gen_loadavg(&sb); break;
case PROC_MOUNTS: gen_mounts(&sb); break;
case PROC_FSSTAT: gen_fsstat(&sb); break;
case PROC_FILESYSTEMS: gen_filesystems(&sb); break;
// SchFS renders straight into the buffer rather than through sbuf, because
// it lives in kernel/fs/schfs and must not depend on this file's helpers.
case PROC_SCHFS: sb.len = schfs_kinfo(buf, PROC_BUFSZ); break;
// Same arrangement as SchFS above: pci.c renders its own lines so that it
// does not depend on this file's helpers.
case PROC_PCI: sb.len = pci_info(buf, PROC_BUFSZ); break;
// Likewise irq.c: it owns the table, so it renders it.
case PROC_INTERRUPTS: sb.len = irq_info(buf, PROC_BUFSZ); break;
// And likewise net.c, which is the only file here allowed to include lwIP's
// headers -- reaching into the stack from this one would put them on every
// kernel file's include path.
case PROC_NETIF: sb.len = net_info(buf, PROC_BUFSZ); break;
case PROC_DEVICES: gen_devices(&sb); break;
default: return 0;
}
return sb.len;
}
int
procread(struct inode *ip, uint64 off, char *dst, int n)
{
char buf[PROC_BUFSZ];
int len = proc_gen_global(ip->minor, buf, PROC_BUFSZ);
if (len <= 0 || (int)off >= len)
return 0; // EOF
int avail = len - (int)off;
if (n > avail)
n = avail;
memmove(dst, buf + off, n);
return n;
}
// /proc is read-only, with one deliberate exception.
//
// /proc/sysrq is the same idea as Linux's sysrq-trigger: a way to make the
// kernel do something drastic on purpose. Right now the only command is 'c',
// crash — which exists because the crash screen is otherwise the one part of
// the system that can only be tested by having a bug. A screen you cannot look
// at until something goes wrong is a screen nobody has ever actually read.
// One sysrq command byte. Shared by the device path (procwrite, below) and
// the filesystem path (kernel/fs/procfs.c), so the two cannot drift.
static void
proc_sysrq_cmd(char cmd)
{
switch (cmd) {
case 'c':
panic("sysrq: crash requested from /proc/sysrq");
case 'u':
xhci_dump();
break;
case 's':
usb_storage_dump();
break;
case 'd':
display_selftest();
break;
case 'n':
netdev_selftest();
break;
case 't':
text_selftest();
break;
default:
cprintf("sysrq: unknown command '%c' "
"(known: c = crash, u = dump USB, s = read the USB disk, "
"d = display self-test)\n",
cmd);
break;
}
}
// `user_src` says whether `src` is a user address that still needs copying in;
// the filesystem hands one over, the device path has already copied.
int
proc_write_sysrq(int user_src, uint64 src, uint n)
{
char c;
if (n < 1)
return -1;
if (user_src) {
if (copy_from_user(&c, src, 1) < 0)
return -EFAULT;
} else {
c = *(char *)src;
}
proc_sysrq_cmd(c);
return (int)n;
}
// The character-device path: /proc/sysrq reached through the old mknod'd node
// rather than through the /proc filesystem. `src` is already a kernel buffer
// here, which is why it passes user_src = 0.
int
procwrite(struct inode *ip, uint64 off, char *src, int n)
{
(void)off;
if (ip->minor != PROC_SYSRQ)
return -1;
return proc_write_sysrq(0, (uint64)src, (uint)n);
}