// SPDX-License-Identifier: GPL-2.0-or-later // // devtmpfs: /dev, generated rather than stored. // // It used to be ~20 mknod() calls in cmd/schinit, which was wrong in two ways // that are worth separating. // // *The cost.* Every node was a real inode on the root filesystem, and SchFS // commits each mutation as its own transaction group -- a copy-on-write rewrite // of the path to the root, then an uberblock behind a cache flush. Measured: // 2600 ms of a first boot spent writing device nodes to a disk, before schinit // printed its first line. On later boots the nodes already existed and the same // code took 0 ms, so the cost was invisible unless you built a fresh image -- // which is exactly what anyone working on the system does every time. // // *The duplication, which matters more.* Major and minor numbers are the // kernel's: they are what devsw[] in kernel/dev/cdev.c dispatches on. schinit // held a second copy of that mapping and nothing checked the two agreed. A new // character device meant editing a userspace program, and getting a minor wrong // there produced a node that opened successfully and did nothing. // // So the table below sits next to the thing it describes, and /dev is a // mount rather than a directory somebody populates. `mount("dev", "/dev", // "devtmpfs", 0, 0)` is the whole of what init does now. // // The namespace is all this provides. Reads and writes never reach here -- // fileread/filewrite see T_DEV and dispatch through devsw themselves -- which // is why there is no read op below that does anything. #include "core/types.h" #include "core/defs.h" #include "core/param.h" #include "uapi/stat.h" #include "core/spinlock.h" #include "core/sleeplock.h" #include "core/proc.h" #include "fs/fs.h" #include "fs/file.h" #include "uapi/errno.h" #include "fs/blkdev.h" #include "dev/pty.h" // Inode numbering. Two directories and a flat table of nodes, so the number is // either a directory id or an index into devnodes[] offset past them. #define DEVFS_ROOT 1ULL #define DEVFS_INPUT 2ULL /* the /dev/input directory */ #define DEVFS_FILE0 16ULL /* devnodes[0] is inode 16 */ // Disks and partitions the block layer found: /dev/sata0, /dev/sata0p1, // /dev/usb0p1, named as it named them. Major 8 like hda, minor the block // device number, and inode DEVFS_BLK0 plus that number. Not in devnodes[], // because they come and go: a stick plugged in appears, pulled out vanishes. #define DEVFS_BLK0 4096ULL // Pseudo-terminals: the /dev/pts directory, its slaves by pair number, and the // masters, which have inodes so that an open file can hold one but appear in no // directory -- the only way to one is opening /dev/ptmx. #define DEVFS_PTS 3ULL #define DEVFS_PTS0 8192ULL #define DEVFS_PTM0 12288ULL // Which directory a node lives in. Only two exist and a third would want a real // tree rather than a flag, which is the point at which this file should grow // one instead of adding DEV_DIR_THIRD. #define D_ROOT 0 #define D_INPUT 1 struct devnode { const char *name; short major; short minor; short dir; }; // The devices this kernel implements, in the order they appear in a listing. // // Minor numbers under major 1 follow Linux's mem-major layout where it costs // nothing (null=2, zero=5, full=7, random=8, urandom=9), which is worth keeping // because it is one less arbitrary mapping for anyone reading both. // // The majors are devsw[] indices and nothing else; see kernel/dev/cdev.c. static const struct devnode devnodes[] = { { "console", 1, 1, D_ROOT }, { "null", 1, 2, D_ROOT }, { "keyboard",1, 3, D_ROOT }, { "zero", 1, 5, D_ROOT }, { "full", 1, 7, D_ROOT }, { "random", 1, 8, D_ROOT }, { "urandom", 1, 9, D_ROOT }, /* /dev/tty is the caller's *controlling* terminal, which is a different thing from the console and used to be the same thing only because there was one terminal. Minor 10 resolves through proc->ctty; see vt_for_minor() in kernel/core/console.c. */ { "tty", 1, 10, D_ROOT }, /* The terminal on screen, as on Linux. Xorg opens it to find a free VT and switch to it, and without one ran on no VT at all: the console went on drawing into the framebuffer over the X screen. Any minor that is not a numbered terminal or the controlling one resolves to vt_current(). */ { "tty0", 1, 0, D_ROOT }, { "tty1", 1, 11, D_ROOT }, { "tty2", 1, 12, D_ROOT }, { "tty3", 1, 13, D_ROOT }, { "tty4", 1, 14, D_ROOT }, { "tty5", 1, 15, D_ROOT }, { "tty6", 1, 16, D_ROOT }, { "tty7", 1, 17, D_ROOT }, { "tty8", 1, 18, D_ROOT }, { "tty9", 1, 19, D_ROOT }, { "tty10", 1, 20, D_ROOT }, { "tty11", 1, 21, D_ROOT }, { "tty12", 1, 22, D_ROOT }, { "stdin", 1, 1, D_ROOT }, { "stdout", 1, 1, D_ROOT }, { "stderr", 1, 1, D_ROOT }, { "fb0", 2, 0, D_ROOT }, { "pci", 9, 0, D_ROOT }, { "dbg", 10, 0, D_ROOT }, // The same device, answering only the views that read. Minor 1 rather than // a flag, because the minor is the one piece of per-open state a character // device already sees -- so which one you opened *is* the permission, and // nothing has to be remembered between the write and the read. See // include/core/dbg.h and cmd/schdbgd. { "dbgro", 10, 1, D_ROOT }, { "netdev", 11, 0, D_ROOT }, // Audio, spelled the way UNIX spells it. See include/dev/audio.h. { "dsp", 12, 0, D_ROOT }, { "mouse", 3, 1, D_ROOT }, // Opening it makes a new pseudo-terminal pair; see kernel/dev/pty.c. { "ptmx", PTMX_MAJOR, 0, D_ROOT }, // Raw disks. Created whether or not a drive is present: opening one with no // disk behind it fails with ENXIO, which is a better answer than a missing // path -- and with no node at all, `echo x > /dev/hdb` silently creates an // ordinary file of that name and appears to work. { "hda", 8, 0, D_ROOT }, { "hdb", 8, 1, D_ROOT }, // Raw input events, Linux evdev layout. The minor is the device index, so // event0 is the keyboard and event1 the mouse. { "event0", 7, 0, D_INPUT }, { "event1", 7, 1, D_INPUT }, }; #define NDEVNODES ((int)(sizeof(devnodes) / sizeof(devnodes[0]))) static int node_of(uint64 inum) { if (inum < DEVFS_FILE0) return -1; uint64 i = inum - DEVFS_FILE0; return i < (uint64)NDEVNODES ? (int)i : -1; } static void devfs_iload(struct inode *ip) { int i = node_of(ip->inum); ip->major = ip->minor = 0; ip->size = 0; if (ip->inum == DEVFS_ROOT || ip->inum == DEVFS_INPUT || ip->inum == DEVFS_PTS) { ip->type = T_DIR; ip->nlink = 2; } else if (ip->inum >= DEVFS_PTS0 && ip->inum < DEVFS_PTS0 + NPTY && pty_used((int)(ip->inum - DEVFS_PTS0))) { ip->type = T_DEV; ip->nlink = 1; ip->major = PTS_MAJOR; ip->minor = (short)(ip->inum - DEVFS_PTS0); ip->mode = 0620; } else if (ip->inum >= DEVFS_PTM0 && ip->inum < DEVFS_PTM0 + NPTY) { ip->type = T_DEV; ip->nlink = 1; ip->major = PTM_MAJOR; ip->minor = (short)(ip->inum - DEVFS_PTM0); } else if (i >= 0) { ip->type = T_DEV; ip->nlink = 1; ip->major = devnodes[i].major; ip->minor = devnodes[i].minor; } else if (ip->inum >= DEVFS_BLK0 && blkdev_present((uint)(ip->inum - DEVFS_BLK0))) { ip->type = T_DEV; ip->nlink = 1; ip->major = 8; ip->minor = (short)(ip->inum - DEVFS_BLK0); } else { ip->type = 0; /* no such node */ ip->nlink = 0; } ip->valid = 1; } static void devfs_istore(struct inode *ip) { (void)ip; } static void devfs_ifree(struct inode *ip) { (void)ip; } static void devfs_truncate(struct inode *ip, uint64 n) { (void)ip; (void)n; } // Never reached for a device node: fileread and filewrite see T_DEV and go // through devsw. Present because fs_ops has the slots, and answering -EIO is // better than a null pointer if something ever does arrive here. static int devfs_read(struct inode *ip, int u, uint64 d, uint64 o, uint n) { (void)ip; (void)u; (void)d; (void)o; (void)n; return -1; } static int devfs_write(struct inode *ip, int u, uint64 s, uint64 o, uint n) { (void)ip; (void)u; (void)s; (void)o; (void)n; return -1; } static int streq(const char *a, const char *b) { while (*a && *a == *b) { a++; b++; } return *a == *b; } static struct inode * devfs_dirlookup(struct inode *dp, char *name, uint *poff) { if (poff) *poff = 0; if (streq(name, ".") || (streq(name, "..") && dp->inum == DEVFS_ROOT)) return iget(DEVFS_DEV, dp->inum); if (streq(name, "..")) return iget(DEVFS_DEV, DEVFS_ROOT); /* out of /dev/input */ if (dp->inum == DEVFS_ROOT && streq(name, "input")) return iget(DEVFS_DEV, DEVFS_INPUT); if (dp->inum == DEVFS_ROOT && streq(name, "pts")) return iget(DEVFS_DEV, DEVFS_PTS); // /dev/pts/N: a pair that exists. if (dp->inum == DEVFS_PTS) { int n = 0; const char *s = name; if (!*s) return 0; for (; *s; s++) { if (*s < '0' || *s > '9') return 0; n = n * 10 + (*s - '0'); if (n >= NPTY) return 0; } return pty_used(n) ? iget(DEVFS_DEV, DEVFS_PTS0 + (uint64)n) : 0; } // /dev/tty is the caller's controlling terminal. When that is a pty, it is // the slave itself -- what a program inside xterm or tmux opens to talk to // its terminal must be that terminal, not the console. if (dp->inum == DEVFS_ROOT && streq(name, "tty") && myproc() && myproc()->ctty >= PTY_CTTY && myproc()->ctty < PTY_CTTY + NPTY && pty_used(myproc()->ctty - PTY_CTTY)) return iget(DEVFS_DEV, DEVFS_PTS0 + (uint64)(myproc()->ctty - PTY_CTTY)); short want = dp->inum == DEVFS_INPUT ? D_INPUT : D_ROOT; for (int i = 0; i < NDEVNODES; i++) if (devnodes[i].dir == want && streq(name, devnodes[i].name)) return iget(DEVFS_DEV, DEVFS_FILE0 + (uint64)i); if (want == D_ROOT) for (int k = 0, b; (b = blkdev_nth(k)) >= 0; k++) if (streq(name, blkdev_name((uint)b))) return iget(DEVFS_DEV, DEVFS_BLK0 + (uint64)b); return 0; } // A read-only filesystem, and these say so rather than pretending. mknod on // /dev returning EROFS is the correct answer: the node either exists because // the kernel has the device, or it does not exist because it has not. static int devfs_dirlink(struct inode *dp, char *name, uint64 inum) { (void)dp; (void)name; (void)inum; return -EROFS; } static int devfs_dirunlink(struct inode *dp, char *name, uint off) { (void)dp; (void)name; (void)off; return -EROFS; } static int devfs_dirempty(struct inode *dp) { (void)dp; return 0; } static struct inode *devfs_ialloc(uint dev, short type) { (void)dev; (void)type; return 0; } struct linux_dirent64 { uint64 d_ino; int64 d_off; uint16 d_reclen; uchar d_type; char d_name[]; }; static int emit(char *out, int cap, int *used, uint64 ino, uchar type, const char *name) { int nl = (int)strlen(name); int rec = (int)((sizeof(struct linux_dirent64) + nl + 1 + 7) & ~7); if (*used + rec > cap) return -1; /* caller stops and resumes next call */ struct linux_dirent64 *d = (struct linux_dirent64 *)(out + *used); d->d_ino = ino; d->d_off = 0; d->d_reclen = (uint16)rec; d->d_type = type; memmove(d->d_name, name, nl); memset(d->d_name + nl, 0, rec - (int)sizeof(*d) - nl); *used += rec; return 0; } // `off` is an ordinal over the entries of this directory: 0 is ".", 1 is "..", // and the rest follow. An opaque cookie the way SchFS uses one buys nothing // here, because the table is fixed and small enough to index directly. static int devfs_getdents(struct inode *dp, char *out, int cap, uint64 *poff) { int used = 0; uint64 n = *poff; if (dp->inum != DEVFS_ROOT && dp->inum != DEVFS_INPUT && dp->inum != DEVFS_PTS) return -1; if (n == 0) { if (emit(out, cap, &used, dp->inum, 4 /* DT_DIR */, ".") < 0) return -1; n++; } if (n == 1) { uint64 up = dp->inum == DEVFS_ROOT ? DEVFS_ROOT : DEVFS_ROOT; if (emit(out, cap, &used, up, 4, "..") < 0) goto out; n++; } // /dev/pts: the dots, then the pairs that exist. Ordinal 2 + N is pair N, // whether or not the ones before it exist, so a pair opened between two // calls does not shift the rest. if (dp->inum == DEVFS_PTS) { for (; n >= 2 && n < 2 + NPTY; n++) { int k = (int)(n - 2); char nm[4]; if (!pty_used(k)) continue; nm[0] = k >= 10 ? '0' + k / 10 : '0' + k; nm[1] = k >= 10 ? '0' + k % 10 : 0; nm[2] = 0; if (emit(out, cap, &used, DEVFS_PTS0 + (uint64)k, 2, nm) < 0) goto out; } goto out; } // /dev/input and /dev/pts are directory entries of the root, emitted // between the dots and the nodes so that their ordinals are stable. if (dp->inum == DEVFS_ROOT && n == 2) { if (emit(out, cap, &used, DEVFS_INPUT, 4, "input") < 0) goto out; n++; } if (dp->inum == DEVFS_ROOT && n == 3) { if (emit(out, cap, &used, DEVFS_PTS, 4, "pts") < 0) goto out; n++; } short want = dp->inum == DEVFS_INPUT ? D_INPUT : D_ROOT; uint64 base = dp->inum == DEVFS_ROOT ? 4 : 2; for (int i = 0; i < NDEVNODES; i++) { if (devnodes[i].dir != want) continue; if (n > base) { base++; continue; } /* already emitted */ if (emit(out, cap, &used, DEVFS_FILE0 + (uint64)i, 2 /* DT_CHR */, devnodes[i].name) < 0) goto out; n++; base++; } if (want == D_ROOT) for (int k = 0, b; (b = blkdev_nth(k)) >= 0; k++) { if (n > base) { base++; continue; } if (emit(out, cap, &used, DEVFS_BLK0 + (uint64)b, 6 /* DT_BLK */, blkdev_name((uint)b)) < 0) goto out; n++; base++; } out: *poff = n; return used; } const struct fs_ops devfs_ops = { .name = "devtmpfs", .iload = devfs_iload, .istore = devfs_istore, .ifree = devfs_ifree, .read = devfs_read, .write = devfs_write, .truncate = devfs_truncate, .dirlookup = devfs_dirlookup, .dirlink = devfs_dirlink, .dirunlink = devfs_dirunlink, .dirempty = devfs_dirempty, .ialloc = devfs_ialloc, .getdents = devfs_getdents, }; // Graft it over `target` -- /dev from init, and again wherever a chroot // wants one. It was always /dev whatever was asked for, so a second mount // failed as "already mounted" and a chroot had no devices. int devfs_mount(const char *target) { struct inode *dp = namei((char *)target); int r; if (dp == 0) return -ENOENT; r = mount_add(dp->dev, dp->inum, DEVFS_DEV, DEVFS_ROOT, (char *)target, "dev", "devtmpfs"); iput(dp); return r < 0 ? -EBUSY : 0; } // The inode of pty master N, for ptmx_open() to hand to the file it opens. struct inode * devfs_pty_master(int n) { if (n < 0 || n >= NPTY) return 0; return iget(DEVFS_DEV, DEVFS_PTM0 + (uint64)n); }