Kernel devices (input, mem, tty, graphics, block) get /sys/devices/virtual entries with dev/uevent/subsystem, /sys/class and /sys/dev links, input capabilities, and writable uevent files that announce on the netlink socket. eudev now runs, processes a full trigger and settles. Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01EVRhxi9zpdW2uDUN4FVcao
691 lines
21 KiB
C
691 lines
21 KiB
C
// 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"
|
|
#include "core/uaccess.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
|
|
|
|
// What udev adds to /dev: directories (/dev/input/by-path, /dev/disk/...) and
|
|
// symlinks (/dev/fd, /dev/stdin, /dev/char/13:64, /dev/disk/by-id/...). The
|
|
// device nodes themselves stay the kernel's; this is a small table of names
|
|
// that live only in memory, as they would in a tmpfs, listed after the real
|
|
// entries of whichever directory they are in. Nothing here can hold data: a
|
|
// regular file in /dev is refused.
|
|
#define DEVFS_EXTRA0 32768ULL
|
|
#define NEXTRA 192
|
|
#define EXTRA_NAME 48
|
|
#define EXTRA_TARGET 112
|
|
|
|
struct extra {
|
|
uchar used; // allocated (an inode exists or is about to)
|
|
uchar linked; // has a name in a directory
|
|
short type; // T_DIR or T_SYMLINK
|
|
uint64 parent; // inum of the directory it is in
|
|
ushort tlen;
|
|
char name[EXTRA_NAME];
|
|
char target[EXTRA_TARGET];
|
|
};
|
|
|
|
static struct {
|
|
struct spinlock lock;
|
|
struct extra e[NEXTRA];
|
|
} extras;
|
|
|
|
static int extras_ready;
|
|
|
|
static void
|
|
extras_init(void)
|
|
{
|
|
if (!extras_ready) {
|
|
initlock(&extras.lock, "devfsx");
|
|
extras_ready = 1;
|
|
}
|
|
}
|
|
|
|
static int
|
|
extra_of(uint64 inum)
|
|
{
|
|
if (inum < DEVFS_EXTRA0)
|
|
return -1;
|
|
uint64 i = inum - DEVFS_EXTRA0;
|
|
return i < NEXTRA ? (int)i : -1;
|
|
}
|
|
|
|
// 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;
|
|
|
|
int x = extra_of(ip->inum);
|
|
if (x >= 0) {
|
|
extras_init();
|
|
acquire(&extras.lock);
|
|
if (extras.e[x].used) {
|
|
ip->type = extras.e[x].type;
|
|
ip->nlink = extras.e[x].type == T_DIR ? 2 : 1;
|
|
ip->size = extras.e[x].tlen;
|
|
ip->mode = extras.e[x].type == T_DIR ? 0755 : 0777;
|
|
} else {
|
|
ip->type = 0;
|
|
ip->nlink = 0;
|
|
}
|
|
release(&extras.lock);
|
|
} else 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)
|
|
{
|
|
int x = extra_of(ip->inum);
|
|
if (x < 0)
|
|
return;
|
|
extras_init();
|
|
acquire(&extras.lock);
|
|
extras.e[x].used = 0;
|
|
release(&extras.lock);
|
|
}
|
|
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)
|
|
{
|
|
int x = extra_of(ip->inum);
|
|
// mkdir lays down the "." and ".." of a v1-style directory by reading and
|
|
// rewriting its first two entries (build_dir_tree). A directory here has
|
|
// none stored, so the read gives zeros and the write is swallowed.
|
|
if (x >= 0 && ip->type == T_DIR) {
|
|
char z[64];
|
|
memset(z, 0, sizeof z);
|
|
for (uint done = 0; done < n; ) {
|
|
uint c = n - done < sizeof z ? n - done : (uint)sizeof z;
|
|
if (u) {
|
|
if (copy_to_user(d + done, z, c) < 0)
|
|
return -EFAULT;
|
|
} else {
|
|
memmove((char *)d + done, z, c);
|
|
}
|
|
done += c;
|
|
}
|
|
(void)o;
|
|
return (int)n;
|
|
}
|
|
if (x < 0 || ip->type != T_SYMLINK)
|
|
return -1;
|
|
extras_init();
|
|
char buf[EXTRA_TARGET];
|
|
acquire(&extras.lock);
|
|
uint len = extras.e[x].tlen;
|
|
memmove(buf, extras.e[x].target, len);
|
|
release(&extras.lock);
|
|
if (o >= len)
|
|
return 0;
|
|
if (o + n > len)
|
|
n = (uint)(len - o);
|
|
if (u) {
|
|
if (copy_to_user(d, buf + o, n) < 0)
|
|
return -EFAULT;
|
|
} else {
|
|
memmove((char *)d, buf + o, n);
|
|
}
|
|
return (int)n;
|
|
}
|
|
|
|
// Only a symlink's target can be written, once, from offset 0.
|
|
static int
|
|
devfs_write(struct inode *ip, int u, uint64 s, uint64 o, uint n)
|
|
{
|
|
int x = extra_of(ip->inum);
|
|
if (x >= 0 && ip->type == T_DIR)
|
|
return (int)n; // see devfs_read
|
|
if (x < 0 || ip->type != T_SYMLINK || o != 0 || n >= EXTRA_TARGET)
|
|
return -1;
|
|
extras_init();
|
|
char buf[EXTRA_TARGET];
|
|
if (u) {
|
|
if (copy_from_user(buf, s, n) < 0)
|
|
return -EFAULT;
|
|
} else {
|
|
memmove(buf, (char *)s, n);
|
|
}
|
|
acquire(&extras.lock);
|
|
memmove(extras.e[x].target, buf, n);
|
|
extras.e[x].tlen = (ushort)n;
|
|
release(&extras.lock);
|
|
ip->size = n;
|
|
return (int)n;
|
|
}
|
|
|
|
static int
|
|
streq(const char *a, const char *b)
|
|
{
|
|
while (*a && *a == *b) { a++; b++; }
|
|
return *a == *b;
|
|
}
|
|
|
|
static struct inode *
|
|
extra_find(uint64 parent, const char *name)
|
|
{
|
|
extras_init();
|
|
acquire(&extras.lock);
|
|
for (int i = 0; i < NEXTRA; i++)
|
|
if (extras.e[i].used && extras.e[i].linked && extras.e[i].parent == parent &&
|
|
streq(name, extras.e[i].name)) {
|
|
release(&extras.lock);
|
|
return iget(DEVFS_DEV, DEVFS_EXTRA0 + (uint64)i);
|
|
}
|
|
release(&extras.lock);
|
|
return 0;
|
|
}
|
|
|
|
static struct inode *
|
|
devfs_dirlookup(struct inode *dp, char *name, uint *poff)
|
|
{
|
|
if (poff)
|
|
*poff = 0;
|
|
|
|
int dx = extra_of(dp->inum);
|
|
if (dx >= 0) {
|
|
extras_init();
|
|
if (streq(name, "."))
|
|
return iget(DEVFS_DEV, dp->inum);
|
|
if (streq(name, "..")) {
|
|
acquire(&extras.lock);
|
|
uint64 up = extras.e[dx].parent;
|
|
release(&extras.lock);
|
|
return iget(DEVFS_DEV, up ? up : DEVFS_ROOT);
|
|
}
|
|
return extra_find(dp->inum, name);
|
|
}
|
|
|
|
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 extra_find(dp->inum, name);
|
|
}
|
|
|
|
// What can be created here: a directory or a symlink, by ialloc and then
|
|
// dirlink -- the order every filesystem is driven in. The device nodes
|
|
// themselves are the kernel's: mknod answers EROFS by never getting an inode.
|
|
static struct inode *
|
|
devfs_ialloc(uint dev, short type)
|
|
{
|
|
(void)dev;
|
|
if (type != T_DIR && type != T_SYMLINK)
|
|
return 0;
|
|
extras_init();
|
|
acquire(&extras.lock);
|
|
for (int i = 0; i < NEXTRA; i++)
|
|
if (!extras.e[i].used) {
|
|
memset(&extras.e[i], 0, sizeof(extras.e[i]));
|
|
extras.e[i].used = 1;
|
|
extras.e[i].type = type;
|
|
release(&extras.lock);
|
|
return iget(DEVFS_DEV, DEVFS_EXTRA0 + (uint64)i);
|
|
}
|
|
release(&extras.lock);
|
|
return 0;
|
|
}
|
|
|
|
static int
|
|
devfs_dirlink(struct inode *dp, char *name, uint64 inum)
|
|
{
|
|
int x = extra_of(inum);
|
|
if (streq(name, ".") || streq(name, ".."))
|
|
return 0; /* a new directory's own dots */
|
|
if (x < 0 || strlen(name) >= EXTRA_NAME)
|
|
return -EINVAL;
|
|
extras_init();
|
|
acquire(&extras.lock);
|
|
if (!extras.e[x].used || extras.e[x].linked) {
|
|
release(&extras.lock);
|
|
return -EINVAL;
|
|
}
|
|
extras.e[x].parent = dp->inum;
|
|
extras.e[x].linked = 1;
|
|
memmove(extras.e[x].name, name, strlen(name) + 1);
|
|
release(&extras.lock);
|
|
return 0;
|
|
}
|
|
|
|
static int
|
|
devfs_dirempty(struct inode *dp)
|
|
{
|
|
if (extra_of(dp->inum) < 0)
|
|
return 0;
|
|
extras_init();
|
|
acquire(&extras.lock);
|
|
for (int i = 0; i < NEXTRA; i++)
|
|
if (extras.e[i].used && extras.e[i].linked && extras.e[i].parent == dp->inum) {
|
|
release(&extras.lock);
|
|
return 0;
|
|
}
|
|
release(&extras.lock);
|
|
return 1;
|
|
}
|
|
|
|
static int
|
|
devfs_dirunlink(struct inode *dp, char *name, uint off)
|
|
{
|
|
(void)off;
|
|
extras_init();
|
|
acquire(&extras.lock);
|
|
for (int i = 0; i < NEXTRA; i++)
|
|
if (extras.e[i].used && extras.e[i].linked && extras.e[i].parent == dp->inum &&
|
|
streq(name, extras.e[i].name)) {
|
|
extras.e[i].linked = 0; /* the inode goes when nobody holds it */
|
|
release(&extras.lock);
|
|
return 0;
|
|
}
|
|
release(&extras.lock);
|
|
return -EROFS; /* a device node is the kernel's */
|
|
}
|
|
|
|
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;
|
|
|
|
int isx = extra_of(dp->inum) >= 0;
|
|
if (dp->inum != DEVFS_ROOT && dp->inum != DEVFS_INPUT &&
|
|
dp->inum != DEVFS_PTS && !isx)
|
|
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++;
|
|
}
|
|
|
|
// A directory udev made: the dots, then what is in it.
|
|
if (isx) {
|
|
uint64 base = 2;
|
|
extras_init();
|
|
acquire(&extras.lock);
|
|
for (int i = 0; i < NEXTRA; i++) {
|
|
if (!extras.e[i].used || !extras.e[i].linked || extras.e[i].parent != dp->inum)
|
|
continue;
|
|
if (n > base) { base++; continue; }
|
|
if (emit(out, cap, &used, DEVFS_EXTRA0 + (uint64)i,
|
|
extras.e[i].type == T_DIR ? 4 : 10 /* DT_LNK */, extras.e[i].name) < 0)
|
|
break;
|
|
n++;
|
|
base++;
|
|
}
|
|
release(&extras.lock);
|
|
goto out;
|
|
}
|
|
|
|
// /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++;
|
|
}
|
|
|
|
// What udev added to this directory, after everything the kernel put there.
|
|
extras_init();
|
|
acquire(&extras.lock);
|
|
for (int i = 0; i < NEXTRA; i++) {
|
|
if (!extras.e[i].used || !extras.e[i].linked || extras.e[i].parent != dp->inum)
|
|
continue;
|
|
if (n > base) { base++; continue; }
|
|
if (emit(out, cap, &used, DEVFS_EXTRA0 + (uint64)i,
|
|
extras.e[i].type == T_DIR ? 4 : 10, extras.e[i].name) < 0)
|
|
break;
|
|
n++;
|
|
base++;
|
|
}
|
|
release(&extras.lock);
|
|
|
|
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);
|
|
}
|