// /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//…) // 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 "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" #include "schfs.h" #include "sbuf.h" #include "dbg.h" #define PROC_BUFSZ 2048 // 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 ------------------------------------------------------------- // 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"); } 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; 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); // 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, NINODE); 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, NFILE); sb_puts(sb, "\nbuf_pinned "); sb_putu(sb, fsstat.buf_pinned); sb_puts(sb, "\nbuf_pinned_peak "); sb_putu(sb, fsstat.buf_pinned_peak); sb_puts(sb, " / "); sb_putu(sb, NBUF); // 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"); } // --- 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 // 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; 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, PROC_BUFSZ); break; case PROC_RESOLV: sb.len = net_resolv(buf, PROC_BUFSZ); 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; case PROC_DEVICES: gen_devices(&sb); 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); 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; 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]); }