Sixty-five headers sat side by side in include/, so `#include "irq.h"` said nothing about where irq.h was or what it belonged to, and errno.h, stat.h and fcntl.h at the top shadowed the system's for any host tool built with -I./include. They are in folders now -- arch/ core/ fs/ dev/ gfx/ net/ uapi/ (and ldls/ for the Linux driver layer) -- and every include names the folder, as Linux's do: "core/proc.h". Done mechanically and checked the hard way: the kernel built from this tree disassembles identically to the one built from the tree before it. The full image boots, and userland is the same set of programs. Paths in docs and comments follow; docs/guides/layout.md has a table of what is where, and tools/fix-includes.py rewrites old-style includes from the folders in the tree, for code written before this. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01S1giNvrHTrPMPw7V6x1kQ4
644 lines
24 KiB
C
644 lines
24 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 "core/types.h"
|
|
#include "core/defs.h"
|
|
#include "core/klog.h"
|
|
#include "core/irq.h"
|
|
#include "arch/x86.h"
|
|
#include "dev/device.h"
|
|
#include "gfx/display.h"
|
|
#include "dev/netdev.h"
|
|
#include "core/param.h"
|
|
#include "arch/memlayout.h"
|
|
#include "core/hwinfo.h"
|
|
#include "arch/mmu.h"
|
|
#include "core/spinlock.h"
|
|
#include "core/sleeplock.h"
|
|
#include "fs/fs.h"
|
|
#include "fs/buf.h"
|
|
#include "fs/file.h"
|
|
#include "core/proc.h"
|
|
#include "core/uaccess.h"
|
|
#include "uapi/errno.h"
|
|
#include "fs/fsstat.h"
|
|
#include "fs/schfs.h"
|
|
#include "core/sbuf.h"
|
|
#include "core/kmem.h"
|
|
#include "core/dbg.h"
|
|
#include "fs/blkdev.h"
|
|
#include "fs/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/core/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");
|
|
// The buffer cache, which grows to 1/32 of RAM and keeps it: without this
|
|
// line that showed up in free(1) as memory "used" by nothing.
|
|
uint nbuf, maxbuf;
|
|
bcache_size(&nbuf, &maxbuf);
|
|
sb_kv(sb, "Buffers: ", (uint64)nbuf * (BSIZE / 1024), "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]);
|
|
}
|