SchOS/kernel/dev/procfs.c
Nexoniarz c1af31f75a smp: 64 processors, and say how many there are
NCPU was 16, under what a desktop has today. 64 costs 48 more TSS
I/O bitmaps and scheduler stacks, about 800 KB of BSS. Past 254 the
APIC would need x2APIC mode, which is a separate change.

With more than 16 there were two other ceilings. sched_getaffinity
wrote "CPU 0" and returned 0, and musl clears every byte past the
count returned -- so the mask came back empty and nproc, make -j and
sysconf(_SC_NPROCESSORS_ONLN) all said 1, on any machine. It returns
the mask of processors that started, and its size, now. And /proc
rendered every file into 2 KB, which /proc/cpuinfo ran out of at the
eighteenth processor without a word; /proc/pci, /proc/interrupts and
/proc/mounts were each one bigger machine from the same cut. There is
one 64 KB buffer for it now behind a sleeplock, since there is no
allocator for contiguous pages, and the generators that write raw
are given the real capacity instead of a constant.

Checked under QEMU with -smp 24: nproc 24, /proc/cpuinfo whole.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01S1giNvrHTrPMPw7V6x1kQ4
2026-10-23 13:50:08 +02:00

640 lines
23 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 "klog.h"
#include "irq.h"
#include "x86.h"
#include "device.h"
#include "display.h"
#include "netdev.h"
#include "param.h"
#include "memlayout.h"
#include "hwinfo.h"
#include "mmu.h"
#include "spinlock.h"
#include "sleeplock.h"
#include "fs.h"
#include "buf.h"
#include "file.h"
#include "proc.h"
#include "uaccess.h"
#include "errno.h"
#include "fsstat.h"
#include "schfs.h"
#include "sbuf.h"
#include "kmem.h"
#include "dbg.h"
#include "blkdev.h"
#include "fat.h"
// Every generated file is rendered whole into one buffer and then sliced, so
// this is the largest any of them can be. It was 2048, which a 24-processor
// /proc/cpuinfo passed at the eighteenth CPU, and /proc/pci, /proc/mounts and
// /proc/interrupts were each one larger machine away from the same quiet cut.
//
// One buffer, not one per read: 64 KB is sixteen pages and there is no
// allocator for contiguous pages. The lock serialises /proc reads, which are
// short and not a hot path -- top reads a few files a second.
#define PROC_BUFSZ (64 * 1024)
static char genbuf[PROC_BUFSZ] __attribute__((aligned(4096)));
static struct sleeplock genlock = {
.lk = { .name = "procgen" }, .name = "procgen",
};
char *
proc_genbuf_get(int *cap)
{
acquiresleep(&genlock);
*cap = PROC_BUFSZ;
return genbuf;
}
void
proc_genbuf_put(void)
{
releasesleep(&genlock);
}
// The formatting helpers these files are built out of now live in
// include/sbuf.h, because the debugger's introspection registry needs exactly
// the same thing and two copies would have drifted. See docs/architecture/debugging.md.
// --- the files -------------------------------------------------------------
static void
gen_meminfo(struct sbuf *sb)
{
// Usable RAM, not PHYSTOP: that is an address and counts the PCI hole, so a
// 6 GB machine reported 8.
uint64 total = ram_total_bytes() / 1024;
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");
}
void gen_schinit(struct sbuf *sb); /* defined at the end of this file */
static void
gen_uptime(struct sbuf *sb)
{
// From the TSC, not from the tick count -- see monotonic() in
// kernel/core/time.c. A tick lost while interrupts were disabled is lost
// forever, so anything derived from ticks runs slow under load and never
// catches up. This is the number people check when they suspect that.
uint64 sec, usec;
monotonic(&sec, &usec);
sb_putu(sb, (uint)sec);
sb_putc(sb, '.');
sb_putu(sb, (uint)(usec / 100000));
sb_putu(sb, (uint)((usec / 10000) % 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/plan/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)
{
// USER_HZ, like times() -- everything that parses /proc/stat divides by 100.
uint t = (uint)((uint64)uptime() * USER_HZ / HZ);
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;
// Every mount is in the table, /dev included. A fixed devtmpfs line here
// listed it twice once init mounted it the ordinary way.
for (int i = 0; i < 32; 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.
int ro = SCHFS_IS_SNAP_DEV(fsdev) || (FAT_IS_DEV(fsdev) && fat_readonly(fsdev));
sb_puts(sb, ro ? " 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);
// Occupancy. The peaks are the diagnostic: a peak that climbs across a
// session and never falls back after the commands finish is a reference
// leak, and which line it is on says where to look.
sb_puts(sb, "\ninode_used "); sb_putu(sb, fsstat.inode_used);
sb_puts(sb, "\ninode_peak "); sb_putu(sb, fsstat.inode_peak);
sb_puts(sb, " / "); sb_putu(sb, inode_slots());
sb_puts(sb, "\nfile_used "); sb_putu(sb, fsstat.file_used);
sb_puts(sb, "\nfile_peak "); sb_putu(sb, fsstat.file_peak);
sb_puts(sb, " / "); sb_putu(sb, file_slots());
sb_puts(sb, "\nbuf_pinned "); sb_putu(sb, fsstat.buf_pinned);
{
uint nb, maxb;
bcache_size(&nb, &maxb);
sb_puts(sb, "\nbuf_pinned_peak "); sb_putu(sb, fsstat.buf_pinned_peak);
sb_puts(sb, " / "); sb_putu(sb, nb);
sb_puts(sb, "\nbuf_cached "); sb_putu(sb, nb);
sb_puts(sb, " / "); sb_putu(sb, maxb);
}
// The SchFS bracket. `locked 1` with `owner 0` is the wedge: the volume lock
// is held by something that is not in a transaction, so nothing will ever
// release it and every later filesystem call sleeps in acquiresleep.
for (int i = 0; i < SCHFS_VOL_SLOTS; i++) {
int locked, owner, depth;
schfs_lockinfo(i, &locked, &owner, &depth);
if (locked < 0)
continue;
sb_puts(sb, "\nschfs"); sb_putu(sb, (uint64)i);
sb_puts(sb, " locked "); sb_putu(sb, (uint64)locked);
sb_puts(sb, " tx_owner_pid "); sb_putu(sb, (uint64)owner);
sb_puts(sb, " tx_depth "); sb_putu(sb, (uint64)depth);
}
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");
sb_puts(sb, "\tvfat\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
#define PROC_KMSG 17
#define PROC_RESOLV 18
#define PROC_BOOTMODE 19
#define PROC_BOOTDEBUG 30
#define PROC_SLAB 20
#define PROC_PARTITIONS 21
// 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;
// The slab allocator renders its own table, for the same reason SchFS and
// PCI do: it owns the data and must not depend on this file's helpers.
case PROC_SLAB: kmem_stats(&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, cap); 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, cap); break;
// Likewise irq.c: it owns the table, so it renders it.
case PROC_INTERRUPTS: sb.len = irq_info(buf, cap); break;
case 16: gen_schinit(&sb); 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, cap); break;
case PROC_RESOLV: sb.len = net_resolv(buf, cap); break;
// How this machine was booted, for the programs that start during one.
//
// schinit and the jobs it runs have the same problem the kernel does -- most
// of what they print is interesting once -- and the same answer only works
// if they can find out which kind of boot this is. One word, so reading it
// is `if (strcmp(buf, "verbose"))` and not a parser.
case PROC_BOOTMODE:
sb_puts(&sb, boot_verbose ? "verbose\n" : "quiet\n");
break;
// Whether the boot menu was asked for a network debugger. Its own file
// rather than a second line in bootmode, because bootmode's whole promise
// is that reading it is a string compare and not a parser.
case PROC_BOOTDEBUG:
sb_puts(&sb, boot_debug ? "on\n" : "off\n");
break;
case PROC_DEVICES: gen_devices(&sb); break;
// The block layer renders its own table, like PCI and SchFS above.
case PROC_PARTITIONS: sb.len = blkdev_info(buf, cap); break;
default: return 0;
}
return sb.len;
}
int
procread(struct inode *ip, uint64 off, char *dst, int n)
{
// The log is served straight out of its ring rather than regenerated into a
// scratch buffer like everything else here: it is 128 KB, and this function
// renders the whole file on every read into an array on a 4 KB kernel stack.
// See klog_read().
if (ip->minor == PROC_KMSG)
return klog_read(off, dst, n);
// **Not a local.** This is PROC_BUFSZ bytes -- a quarter of the kernel
// stack -- on a path that is several frames deep already, and the comment
// above has warned about exactly that since it was written. It stayed a
// local because a page from kalloc() for 2 KB felt worse; kmalloc is the
// answer that was missing.
int cap;
char *buf = proc_genbuf_get(&cap);
int len = proc_gen_global(ip->minor, buf, cap);
if (len <= 0 || (int)off >= len) {
proc_genbuf_put();
return 0; // EOF
}
int avail = len - (int)off;
if (n > avail)
n = avail;
memmove(dst, buf + off, n);
proc_genbuf_put();
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;
case 'i':
// Kill PID 1, on purpose.
//
// Signals cannot do this -- init_ignores() drops them, which is correct
// and is why the two-init scheme was solving a problem that did not exist.
// Setting `killed` directly is the kernel deciding, not userspace asking:
// init exits at its next trap return and proc_reincarnate() takes over.
//
// docs/architecture/init.md asks for this to be "tested by doing it rather than
// by reasoning", which is the only honest way to know the path works.
proc_kill_init();
cprintf("sysrq: pid 1 asked to exit -- it should come back\n");
break;
case 'g':
// Every registered view, straight to the console.
//
// This is docs/architecture/debugging.md's level 0: no process, no filesystem, no
// client program. If /dev/dbg cannot be opened -- because the filesystem
// is what is broken -- this still works, and on a machine with only a
// serial line it is the whole debugger.
dbg_dump_all();
break;
case 'w': {
// Wedge the supervisor: make schinitd hang, on purpose, so PID 1's
// heartbeat timeout is exercised instead of reasoned about. There is no
// way back from this; reboot afterwards.
int pid = proc_wedge("schinitd");
if (pid)
cprintf("sysrq: wedged schinitd (pid %d) -- pid 1 should notice "
"within %d seconds\n", pid, 10);
else
cprintf("sysrq: no process named schinitd to wedge\n");
break;
}
default:
cprintf("sysrq: unknown command '%c' "
"(known: c = crash, u = dump USB, s = read the USB disk, "
"d = display self-test, t = text self-test, n = net self-test, "
"g = dump every debug view, "
"w = wedge the init supervisor, i = kill pid 1)\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);
}
// ---------------------------------------------------------------------------
// /proc/schinit -- the job table
// ---------------------------------------------------------------------------
//
// docs/architecture/init.md, "State outside the process". PID 1 writes this
// whenever the table changes and
// anyone may read it; a supervisor that has just been restarted reads it back
// and knows what is running without having started any of it.
//
// The kernel holds the text and does not interpret it. That is deliberate: a
// procfs generator runs in the kernel, and walking PID 1's memory to answer a
// read would put this file's format into the kernel, where it would then have
// to be kept in step with a userspace program. Instead the kernel is a
// pinboard -- PID 1 knows the format and knows when it changed.
//
// procfs rather than a file on disk for four reasons: it survives a read-only
// root, it survives a full disk, it costs no I/O, and it is already the
// pattern this kernel uses for exactly this kind of question.
#define SCHINIT_TABLE_MAX 2048
static char schinit_table[SCHINIT_TABLE_MAX];
static int schinit_table_len;
int
proc_write_schinit(int user_src, uint64 src, uint n)
{
if (n > SCHINIT_TABLE_MAX)
n = SCHINIT_TABLE_MAX;
// A write replaces the table rather than appending to it. There is one
// writer and it always sends the whole thing, so append would only be a way
// to accumulate stale rows.
if (user_src) {
if (copy_from_user(schinit_table, src, n) < 0)
return -EFAULT;
} else {
memmove(schinit_table, (char *)src, n);
}
schinit_table_len = (int)n;
return (int)n;
}
void
gen_schinit(struct sbuf *sb)
{
if (schinit_table_len == 0) {
sb_puts(sb, "generation 0\nsupervisor 0\n");
return;
}
for (int i = 0; i < schinit_table_len; i++)
sb_putc(sb, schinit_table[i]);
}