// /proc as a real filesystem. // // It used to be a handful of character devices: init ran `mknod("/proc/uptime", // 6, 2)` and friends, so every file was a real inode on the root filesystem // with a major/minor pair behind it. That works for a fixed set of files and // cannot work at all for the thing /proc is mostly *for* -- one directory per // process. You cannot mknod a directory for a process that does not exist yet // and will not exist in a minute, which is why `ps` and `top` have never // worked: they call dirtree_flagread("/proc") and get a list with no pids in // it. // // So this is a synthetic filesystem behind the same `struct fs_ops` vtable // SchFS uses. Nothing is stored: every directory listing is generated from the // process table at the moment it is asked for, and every file's contents are // generated when it is read. // // The global files (meminfo, uptime, pci, ...) keep their existing generators // in kernel/dev/procfs.c; this layer only decides what exists and hands the // reads over. That split is deliberate -- the generators are about content and // this file is about namespace, and they change for different reasons. #include "types.h" #include "defs.h" #include "param.h" #include "memlayout.h" #include "mmu.h" #include "proc.h" #include "spinlock.h" #include "sleeplock.h" #include "fs.h" #include "file.h" #include "stat.h" #include "uaccess.h" #include "errno.h" // --------------------------------------------------------------------------- // Inode numbering // // An inode number has to survive being handed out, stored in a struct inode, // and given back later with no other context -- so it encodes the whole // identity of the object. // // 1 the root, /proc // PROCFS_GLOBAL | id a global file, id being the PROC_* minor that // kernel/dev/procfs.c already uses // pid << 8 the directory /proc/ // (pid << 8) | fileid a file inside it // // pid is bounded by NPROC and file ids by PF_MAX, so the ranges cannot // collide. Shifting by 8 rather than packing tightly keeps the numbers // readable in a debugger, which matters more than density here. // --------------------------------------------------------------------------- // Size of the scratch buffer every generator renders into. Must stay equal to // PROC_BUFSZ in kernel/dev/procfs.c: the SchFS and PCI generators there write // up to PROC_BUFSZ regardless of the cap they are handed, so a smaller value // here would be a buffer overrun rather than a truncation. #define PROCFS_GENBUF 2048 #define PROCFS_ROOT 1ULL #define PROCFS_GLOBAL 0x10000ULL #define PF_DIR 0 // the directory itself #define PF_STAT 1 #define PF_STATUS 2 #define PF_CMDLINE 3 #define PF_STATM 4 #define PF_EXE 5 #define PF_MAX 6 #define INO_PID(ino) ((int)((ino) >> 8)) #define INO_FILE(ino) ((int)((ino) & 0xFF)) #define INO_IS_GLOBAL(ino) (((ino) & PROCFS_GLOBAL) != 0) // The global files, by the name they appear under and the PROC_* id that // kernel/dev/procfs.c generates them from. struct global_file { const char *name; int id; }; static const struct global_file globals[] = { { "meminfo", 1 }, { "uptime", 2 }, { "cpuinfo", 3 }, { "version", 4 }, { "stat", 5 }, { "loadavg", 6 }, { "mounts", 7 }, { "filesystems", 8 }, { "schfs", 9 }, { "sysrq", 10 }, { "pci", 11 }, { "interrupts", 12 }, { "devices", 13 }, { "fsstat", 14 }, { "netif", 15 }, }; #define NGLOBALS ((int)(sizeof(globals) / sizeof(globals[0]))) // Per-process files. `statm` and `status` are here because ps reads them; a // missing file is not an error to it, but a present one is cheaper than the // fallback paths it takes without them. static const struct global_file pidfiles[] = { { "stat", PF_STAT }, { "status", PF_STATUS }, { "cmdline", PF_CMDLINE }, { "statm", PF_STATM }, { "exe", PF_EXE }, }; #define NPIDFILES ((int)(sizeof(pidfiles) / sizeof(pidfiles[0]))) // --------------------------------------------------------------------------- // Content // --------------------------------------------------------------------------- // A tiny append-only formatter. procfs files are small and generated whole, // so there is no reason for anything cleverer. struct sb { char *p; int n, cap; }; static void sbputc(struct sb *s, char c) { if (s->n < s->cap - 1) s->p[s->n++] = c; } static void sbputs(struct sb *s, const char *t) { while (*t) sbputc(s, *t++); } static void sbputi(struct sb *s, int64 v) { char tmp[24]; int i = 0, neg = 0; uint64 u; if (v < 0) { neg = 1; u = (uint64)(-v); } else u = (uint64)v; do { tmp[i++] = '0' + (int)(u % 10); u /= 10; } while (u); if (neg) sbputc(s, '-'); while (i--) sbputc(s, tmp[i]); } // Linux's /proc//stat, as far as anything here reads it. // // The field order is not negotiable: ps counts fields by position, so a // missing one silently shifts everything after it. Fields SchOS does not // track are zero, which is what Linux itself reports for things a given // process has never done. static char state_char(int st) { switch (st) { case RUNNING: return 'R'; case RUNNABLE: return 'R'; case SLEEPING: return 'S'; case ZOMBIE: return 'Z'; case EMBRYO: return 'R'; default: return '?'; } } static void gen_pid_stat(struct sb *s, const struct procinfo *pi) { int i; sbputi(s, pi->pid); sbputs(s, " ("); sbputs(s, pi->name); sbputs(s, ") "); sbputc(s, state_char(pi->state)); sbputc(s, ' '); sbputi(s, pi->ppid); // 4 ppid sbputc(s, ' '); sbputi(s, pi->pgid); // 5 pgrp sbputc(s, ' '); sbputi(s, pi->sid); // 6 session sbputs(s, " 0 -1 0"); // 7 tty_nr, 8 tpgid, 9 flags // 10..17: minflt cminflt majflt cmajflt utime stime cutime cstime. // No per-process accounting yet, so these are honestly zero rather than // invented -- a fabricated CPU time is worse than none, because top would // draw a plausible graph of a number nobody measured. for (i = 0; i < 8; i++) sbputs(s, " 0"); sbputs(s, " 20 0 1 0 0 "); // priority nice num_threads itrealvalue starttime sbputi(s, (int64)pi->sz); // 23 vsize, in bytes sbputc(s, ' '); sbputi(s, (int64)(pi->sz / PGSIZE)); // 24 rss, in pages sbputs(s, " 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0\n"); } static void gen_pid_status(struct sb *s, const struct procinfo *pi) { sbputs(s, "Name:\t"); sbputs(s, pi->name); sbputc(s, '\n'); sbputs(s, "State:\t"); sbputc(s, state_char(pi->state)); sbputs(s, "\nTgid:\t"); sbputi(s, pi->pid); sbputs(s, "\nPid:\t"); sbputi(s, pi->pid); sbputs(s, "\nPPid:\t"); sbputi(s, pi->ppid); sbputs(s, "\nUid:\t"); sbputi(s, pi->uid); sbputc(s, '\t'); sbputi(s, pi->uid); sbputc(s, '\t'); sbputi(s, pi->uid); sbputc(s, '\t'); sbputi(s, pi->uid); sbputs(s, "\nVmSize:\t"); sbputi(s, (int64)(pi->sz / 1024)); sbputs(s, " kB\nThreads:\t1\n"); } static void gen_pid_statm(struct sb *s, const struct procinfo *pi) { int64 pages = (int64)(pi->sz / PGSIZE); sbputi(s, pages); sbputc(s, ' '); sbputi(s, pages); sbputs(s, " 0 0 0 0 0\n"); } // argv is not kept after exec, so the best honest answer is the process name. // Linux separates arguments with NUL and ps turns those into spaces; one // NUL-terminated string is a valid degenerate case of that. static void gen_pid_cmdline(struct sb *s, const struct procinfo *pi) { sbputs(s, pi->name); sbputc(s, '\0'); } // Generate the whole of one file into `buf`. Returns its length. static int gen_file(uint64 ino, char *buf, int cap) { struct sb s = { buf, 0, cap }; struct procinfo pi; if (INO_IS_GLOBAL(ino)) return proc_gen_global((int)(ino & 0xFF), buf, cap); if (proc_snapshot_pid(INO_PID(ino), &pi) < 0) return 0; // it exited between lookup and read switch (INO_FILE(ino)) { case PF_STAT: gen_pid_stat(&s, &pi); break; case PF_STATUS: gen_pid_status(&s, &pi); break; case PF_CMDLINE: gen_pid_cmdline(&s, &pi); break; case PF_STATM: gen_pid_statm(&s, &pi); break; case PF_EXE: sbputs(&s, pi.exe[0] ? pi.exe : pi.name); break; default: return 0; } return s.n; } // --------------------------------------------------------------------------- // fs_ops // --------------------------------------------------------------------------- static void procfs_iload(struct inode *ip) { uint64 ino = ip->inum; ip->valid = 1; ip->nlink = 1; ip->major = ip->minor = 0; // Read-only, with one exception: /proc/sysrq is the deliberate write target // (`echo c > /proc/sysrq` to crash on purpose). Marking everything 2 made // filewrite() refuse it with "permission denied", which the USB suite // caught immediately -- three of its checks drive the disk through sysrq. ip->rwmode = (INO_IS_GLOBAL(ino) && (ino & 0xFF) == 10) ? 3 : 2; ip->showmode = 1; ip->atime = ip->mtime = ip->ctime = 0; if (ino == PROCFS_ROOT || (!INO_IS_GLOBAL(ino) && INO_FILE(ino) == PF_DIR)) { ip->type = T_DIR; ip->size = 0; ip->nlink = 2; return; } ip->type = T_FILE; // Size is reported as zero. // // A synthetic file has no length until it is generated, and generating it // here -- on every stat(), for every process -- to fill in a number almost // nobody reads would be absurd. Linux does exactly the same thing, which is // why `ls -l /proc` shows a column of zeroes, and why every tool that reads // /proc reads until EOF instead of trusting st_size. ip->size = 0; } static void procfs_istore(struct inode *ip) { (void)ip; } static void procfs_ifree(struct inode *ip) { (void)ip; } static void procfs_truncate(struct inode *ip, uint64 n) { (void)ip; (void)n; } static int procfs_read(struct inode *ip, int user_dst, uint64 dst, uint64 off, uint n) { char *buf; int len, r; if (ip->type == T_DIR) return -EISDIR; // The 2048-byte scratch used to be a local. KSTACKSIZE is 4096, so that was // half the kernel stack in one array, on a path already several frames deep // -- a generator with any locals of its own, or one more frame below it, // runs off the end. /proc/schfs is what finally did it: the canary at the // bottom of the stack came back zeroed. Nothing about SchFS caused this; it // only pushed an existing overrun over the edge. // // A whole page for a 2 KB buffer is not wasteful here -- kalloc's unit is a // page either way, and this is not a hot path. if ((buf = kalloc()) == 0) return -ENOMEM; len = gen_file(ip->inum, buf, PROCFS_GENBUF); if (off >= (uint64)len) { kfree(buf); return 0; } if (off + n > (uint64)len) n = (uint)((uint64)len - off); r = (int)n; if (user_dst) { if (copy_to_user(dst, buf + off, n) < 0) r = -EFAULT; } else { memmove((char *)dst, buf + off, n); } kfree(buf); return r; } static int procfs_write(struct inode *ip, int user_src, uint64 src, uint64 off, uint n) { // Only /proc/sysrq accepts writes, and its handler lives with the other // generators. Everything else is read-only, and says so. if (INO_IS_GLOBAL(ip->inum) && (ip->inum & 0xFF) == 10) return proc_write_sysrq(user_src, src, n); (void)off; return -EROFS; } // Decimal string -> pid, or -1 if it is not all digits. This is also what // makes a name like "meminfo" fall through to the global table. static int name_to_pid(const char *name) { int v = 0; if (!name || !*name) return -1; while (*name) { if (*name < '0' || *name > '9') return -1; v = v * 10 + (*name++ - '0'); if (v > 1 << 20) return -1; } return v; } static struct inode * procfs_dirlookup(struct inode *dp, char *name, uint *poff) { struct procinfo pi; int i, pid; if (poff) *poff = 0; if (namecmp(name, ".") == 0) return iget(dp->dev, dp->inum); if (namecmp(name, "..") == 0) return iget(dp->dev, PROCFS_ROOT); // /proc//.. is /proc // Inside /proc// if (dp->inum != PROCFS_ROOT) { for (i = 0; i < NPIDFILES; i++) if (namecmp(name, (char *)pidfiles[i].name) == 0) return iget(dp->dev, (dp->inum & ~0xFFULL) | pidfiles[i].id); return 0; } // In /proc itself: a number is a process, a word is a global file. pid = name_to_pid(name); if (pid > 0) { if (proc_snapshot_pid(pid, &pi) < 0) return 0; return iget(dp->dev, ((uint64)pid << 8) | PF_DIR); } for (i = 0; i < NGLOBALS; i++) if (namecmp(name, (char *)globals[i].name) == 0) return iget(dp->dev, PROCFS_GLOBAL | globals[i].id); return 0; } // Nothing in /proc can be created, removed or linked. Saying so with an error // is better than a silent no-op: a program that thinks it wrote a file and // finds it missing later has a much harder problem to debug. static int procfs_dirlink(struct inode *dp, char *name, uint64 inum) { (void)dp; (void)name; (void)inum; return -EROFS; } static int procfs_dirunlink(struct inode *dp, char *name, uint off) { (void)dp; (void)name; (void)off; return -EROFS; } static int procfs_dirempty(struct inode *dp) { (void)dp; return 0; } static struct inode *procfs_ialloc(uint dev, short type) { (void)dev; (void)type; return 0; } // One getdents64 record. struct linux_dirent64 { uint64 d_ino; int64 d_off; ushort d_reclen; uchar d_type; char d_name[]; }; #define DT_DIR_ 4 #define DT_REG_ 8 static int emit(char *out, int cap, int used, uint64 ino, int type, const char *name) { struct linux_dirent64 *de; int nlen = 0, reclen; while (name[nlen]) nlen++; reclen = (int)sizeof(*de) + nlen + 1; reclen = (reclen + 7) & ~7; // 8-byte aligned, as getdents64 requires if (used + reclen > cap) return -1; de = (struct linux_dirent64 *)(out + used); de->d_ino = ino; de->d_off = used + reclen; de->d_reclen = (ushort)reclen; de->d_type = (uchar)type; memmove(de->d_name, name, nlen); de->d_name[nlen] = 0; return reclen; } static void utoa(uint v, char *buf) { char t[12]; int i = 0, j = 0; do { t[i++] = '0' + (v % 10); v /= 10; } while (v); while (i) buf[j++] = t[--i]; buf[j] = 0; } // The listing is generated fresh each time, and *off is an index into it. // // A process that exits between two getdents calls therefore shifts everything // after it up by one, and one entry can be missed. That is a real race and it // is the same one Linux has -- /proc has no stable ordering to offer, and // anything that wants a consistent snapshot has to re-read. static int procfs_getdents(struct inode *dp, char *out, int cap, uint64 *off) { struct procinfo pi; char name[16]; int used = 0, idx = 0, i, r; uint64 want = *off; #define EMIT(ino, type, nm) do { \ if (idx++ >= (int)want) { \ r = emit(out, cap, used, (ino), (type), (nm)); \ if (r < 0) goto full; \ used += r; (*off)++; \ } \ } while (0) if (dp->inum == PROCFS_ROOT) { EMIT(PROCFS_ROOT, DT_DIR_, "."); EMIT(PROCFS_ROOT, DT_DIR_, ".."); for (i = 0; i < NGLOBALS; i++) EMIT(PROCFS_GLOBAL | globals[i].id, DT_REG_, globals[i].name); for (i = 0; proc_snapshot(i, &pi) == 0; i++) { utoa((uint)pi.pid, name); EMIT(((uint64)pi.pid << 8) | PF_DIR, DT_DIR_, name); } } else { EMIT(dp->inum, DT_DIR_, "."); EMIT(PROCFS_ROOT, DT_DIR_, ".."); for (i = 0; i < NPIDFILES; i++) EMIT((dp->inum & ~0xFFULL) | pidfiles[i].id, DT_REG_, pidfiles[i].name); } full: #undef EMIT return used; } const struct fs_ops procfs_ops = { .name = "procfs", .iload = procfs_iload, .istore = procfs_istore, .ifree = procfs_ifree, .read = procfs_read, .write = procfs_write, .truncate = procfs_truncate, .dirlookup = procfs_dirlookup, .dirlink = procfs_dirlink, .dirunlink = procfs_dirunlink, .dirempty = procfs_dirempty, .ialloc = procfs_ialloc, .getdents = procfs_getdents, }; // Graft this filesystem over /proc. // // Called by init once it has made the directory. The mknod'd files that used // to live there are simply covered: path resolution stops at the mount point, // so they are unreachable and harmless. Removing them from init is a separate, // later cleanup -- doing both at once would make a boot failure ambiguous // between "the mount is wrong" and "the nodes are missing". void procfs_mount(void) { struct inode *dp = namei("/proc"); if (dp == 0) { cprintf("procfs: /proc does not exist; not mounted\n"); return; } if (mount_add(dp->dev, dp->inum, PROCFS_DEV, PROCFS_ROOT, "/proc", "proc", "proc") < 0) cprintf("procfs: /proc already has something mounted on it\n"); iput(dp); }