PCI and USB devices in the /sys/devices tree, with uevents and USB hot-plug

PCI devices live at /sys/devices/pci0000:00/<addr> and USB devices under the
xHCI controller that carries their bus, below that bus's root hub usbN;
/sys/bus/pci/devices and /sys/bus/usb/devices are symlinks into it, and every
device has its subsystem and driver links, which is how udev learns SUBSYSTEM
and DRIVER. Writing to a device's uevent now announces it with MODALIAS,
PCI_ID, PRODUCT and the rest, so udevadm trigger covers them, and the USB core
sends add when a device is attached and remove as it goes. The root hub is
announced when the controller comes up, before anything is plugged in.

Checked under QEMU: lspci and udevadm info through the links, udevadm monitor
showing the kernel and udev add event for device_add usb-kbd/usb-mouse and
the remove event for device_del, and a triggered add for usb1 and 1-5.

Known limits: the bus-to-controller mapping is a convention (bus N is the Nth
xHCI in PCI order); PCI bridges are not parents; USB interfaces and
/dev/bus/usb nodes are absent; busybox's lsusb reads /dev/bus/usb and prints
nothing.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01EVRhxi9zpdW2uDUN4FVcao
This commit is contained in:
Nexoniarz 2026-10-26 15:51:28 +01:00
commit 8d836c66b2
8 changed files with 526 additions and 67 deletions

View file

@ -2,12 +2,21 @@
//
// /sys, laid out the way Linux lays it out, for the parts that exist here.
//
// /sys/bus/pci/devices/0000:00:1f.2/ vendor device class revision irq
// /sys/devices/pci0000:00/0000:00:1f.2/ vendor device class revision irq
// subsystem_vendor subsystem_device
// resource config uevent boot_vga
// enable graphics/fb0/
// /sys/bus/usb/devices/1-3.2/ idVendor idProduct bcdDevice busnum
// enable graphics/fb0/ subsystem driver
// /sys/devices/pci0000:00/0000:00:14.0/usb1/1-3.2/
// idVendor idProduct bcdDevice busnum
// devnum speed bDevice* uevent
// subsystem driver
// /sys/bus/pci/devices/<addr> symlinks to the above, as on Linux
// /sys/bus/usb/devices/<name>
//
// The real directories live under /sys/devices because that is what udev
// resolves a device to, and what an event's DEVPATH has to name. USB devices
// sit under the xHCI controller that carries their bus, flat below its usbN
// root hub: a hub's children are siblings of the hub here, not nested in it.
// /sys/firmware/acpi/tables/DSDT the tables, byte for byte
// /sys/firmware/dmi/tables/DMI the SMBIOS table and entry point
// /sys/firmware/memmap/N/ start end type
@ -46,6 +55,7 @@
#include "gfx/gfx.h"
#include "dev/acpi.h"
#include "dev/kdev.h"
#include "net/socket.h"
// ---------------------------------------------------------------------------
// Inode numbers: kind, index, file -- the whole identity, since iget gives
@ -66,6 +76,9 @@
#define K_KCLINK 12ULL // idx a kdev: the symlink in /sys/class/<subsys>
#define K_KDEVD 13ULL // idx 0 char, 1 block: /sys/dev/char, /sys/dev/block
#define K_KDEVL 14ULL // idx a kdev: the "MAJ:MIN" symlink in /sys/dev/*
#define K_PCILINK 15ULL // idx a pci_get() index: the symlink in /sys/bus/pci/devices
#define K_USBLINK 16ULL // idx bus << 8 | devnum (0: the root hub): /sys/bus/usb/devices
#define K_USBROOT 17ULL // idx a bus number: the root hub, /sys/devices/pci.../usbN
// Files in a kdev directory (K_KDEV).
enum { KF_DIR, KF_DEV, KF_UEVENT, KF_SUBSYSTEM, KF_NAME, KF_PROPERTIES,
@ -79,7 +92,7 @@ enum { KF_DIR, KF_DEV, KF_UEVENT, KF_SUBSYSTEM, KF_NAME, KF_PROPERTIES,
enum {
D_ROOT, D_BUS, D_PCI, D_PCIDEV, D_USB, D_USBDEV, D_FW, D_ACPI, D_ACPITAB,
D_DMI, D_DMITAB, D_MEMMAP, D_EFI, D_CLASS, D_CLASSDMI, D_DMIID, D_PSU,
D_DEVICES, D_VIRTUAL, D_DEVT,
D_DEVICES, D_VIRTUAL, D_DEVT, D_PCIROOT,
D_NDIRS
};
@ -104,6 +117,7 @@ static const struct { const char *name; int parent; } dirs[D_NDIRS] = {
[D_DEVICES] = { "devices", D_ROOT },
[D_VIRTUAL] = { "virtual", D_DEVICES },
[D_DEVT] = { "dev", D_ROOT },
[D_PCIROOT] = { "pci0000:00", D_DEVICES },
};
#define SYSFS_ROOT INO(K_DIR, D_ROOT, 0)
@ -123,6 +137,8 @@ static const char *const pcifiles[] = {
// boot_vga. Empty, because the path existing is the whole answer.
#define PCI_GRAPHICS 13
#define PCI_FB0 14
#define PCI_SUBSYS 15 // symlink: ../../../bus/pci
#define PCI_DRIVER 16 // symlink, only while a driver is bound
// The display device: the one whose memory the framebuffer is in, which is
// what boot_vga means on Linux -- the card firmware brought up and drew on.
@ -150,9 +166,21 @@ boot_vga_idx(void)
static const char *const usbfiles[] = {
0, "idVendor", "idProduct", "bcdDevice", "busnum", "devnum", "speed",
"bDeviceClass", "bDeviceSubClass", "bDeviceProtocol", "bNumConfigurations",
"uevent",
"uevent", "subsystem", "driver",
};
#define NUSBFILES ((int)(sizeof usbfiles / sizeof usbfiles[0]))
#define USB_UEVENT 11
#define USB_SUBSYS 12 // symlink
#define USB_DRIVER 13 // symlink, only while a driver is bound
// A root hub, usbN: the little that udev and lsusb read.
static const char *const hubfiles[] = {
0, "uevent", "subsystem", "idVendor", "idProduct", "busnum", "devnum",
"bDeviceClass", "speed",
};
#define NHUBFILES ((int)(sizeof hubfiles / sizeof hubfiles[0]))
#define HUB_UEVENT 1
#define HUB_SUBSYS 2
static const char *const memfiles[] = { 0, "start", "end", "type" };
@ -272,6 +300,24 @@ usb_by_idx(int idx, struct usb_info *u)
return -1;
}
// The PCI device a USB bus is: the Nth xHCI controller in PCI order, N being
// the bus number. The USB core numbers a bus when its first device appears and
// does not record which controller it came from, so this is a convention that
// holds as long as every xHCI controller has a driver -- true here, where one
// driver takes them all. -1 if there is no such controller.
static int
usb_hc_pci(int bus)
{
int n = 0;
for (int i = 0; i < pci_count(); i++) {
struct pci_dev *d = pci_get(i);
if (d->class == 0x0C && d->subclass == 0x03 && d->prog_if == 0x30 &&
++n == bus)
return i;
}
return -1;
}
// BAT0, BAT1 for batteries and AC, AC1 for adapters, numbered in the order
// ACPI lists them -- the names Linux gives most laptops.
static void
@ -523,19 +569,40 @@ children(uint64 dino, child_fn cb, void *ctx)
if (cb(ctx, INO(K_KCLASS, s, 0), 1, kdev_subsys(s)))
return 1;
break;
case D_PCIROOT:
case D_PCIDEV:
// The devices themselves live in the first; the second holds the
// links udev and lspci walk to reach them.
for (int i = 0; i < pci_count(); i++) {
struct out o = { name, 0, sizeof name - 1 };
pci_name(&o, pci_get(i));
name[o.n] = 0;
if (cb(ctx, INO(K_PCI, i, 0), 1, name))
if (idx == D_PCIDEV ? cb(ctx, INO(K_PCILINK, i, 0), 2, name)
: cb(ctx, INO(K_PCI, i, 0), 1, name))
return 1;
}
// A bus whose controller we cannot place hangs off the root instead.
if (idx == D_PCIROOT)
for (int b = 1; b <= usb_nbuses(); b++)
if (usb_hc_pci(b) < 0) {
struct out o = { name, 0, sizeof name - 1 };
puts_(&o, "usb"); putu(&o, (uint64)b);
name[o.n] = 0;
if (cb(ctx, INO(K_USBROOT, b, 0), 1, name))
return 1;
}
break;
case D_USBDEV: {
struct usb_info u;
for (int b = 1; b <= usb_nbuses(); b++) {
struct out o = { name, 0, sizeof name - 1 };
puts_(&o, "usb"); putu(&o, (uint64)b);
name[o.n] = 0;
if (cb(ctx, INO(K_USBLINK, b << 8, 0), 2, name))
return 1;
}
for (int i = 0; usb_info_nth(i, &u) == 0; i++)
if (cb(ctx, INO(K_USB, (u.bus << 8) | u.devnum, 0), 1, u.path))
if (cb(ctx, INO(K_USBLINK, (u.bus << 8) | u.devnum, 0), 2, u.path))
return 1;
break;
}
@ -593,13 +660,41 @@ children(uint64 dino, child_fn cb, void *ctx)
if (idx == boot_vga_idx() && gfx_fb_phys() &&
cb(ctx, INO(K_PCI, idx, PCI_GRAPHICS), 1, "graphics"))
return 1;
if (cb(ctx, INO(K_PCI, idx, PCI_SUBSYS), 2, "subsystem"))
return 1;
if (idx < pci_count() && pci_get(idx)->driver &&
cb(ctx, INO(K_PCI, idx, PCI_DRIVER), 2, "driver"))
return 1;
for (int b = 1; b <= usb_nbuses(); b++)
if (usb_hc_pci(b) == idx) {
char un[12] = "usb";
struct out o = { un + 3, 0, 8 };
putu(&o, (uint64)b);
un[3 + o.n] = 0;
if (cb(ctx, INO(K_USBROOT, b, 0), 1, un))
return 1;
}
} else if (kind == K_USBROOT && I_FILE(dino) == 0) {
struct usb_info u;
for (int f = 1; f < NHUBFILES; f++)
if (cb(ctx, INO(K_USBROOT, idx, f), f == HUB_SUBSYS ? 2 : 0, hubfiles[f]))
return 1;
for (int i = 0; usb_info_nth(i, &u) == 0; i++)
if (u.bus == idx &&
cb(ctx, INO(K_USB, (u.bus << 8) | u.devnum, 0), 1, u.path))
return 1;
} else if (kind == K_PCI && I_FILE(dino) == PCI_GRAPHICS) {
if (cb(ctx, INO(K_PCI, idx, PCI_FB0), 1, "fb0"))
return 1;
} else if (kind == K_USB && I_FILE(dino) == 0) {
for (int f = 1; f < NUSBFILES; f++)
if (cb(ctx, INO(K_USB, idx, f), 0, usbfiles[f]))
struct usb_info u;
int have = usb_by_idx(idx, &u) == 0;
for (int f = 1; f < NUSBFILES; f++) {
if (f == USB_DRIVER && !(have && u.driver[0]))
continue;
if (cb(ctx, INO(K_USB, idx, f), f >= USB_SUBSYS ? 2 : 0, usbfiles[f]))
return 1;
}
} else if (kind == K_PSU && I_FILE(dino) == 0) {
struct acpi_psu b;
if (acpi_psu_get(idx, &b) == 0)
@ -625,8 +720,14 @@ parent_of(uint64 dino)
case K_PCI:
if (I_FILE(dino) == PCI_GRAPHICS) return INO(K_PCI, I_IDX(dino), 0);
if (I_FILE(dino) == PCI_FB0) return INO(K_PCI, I_IDX(dino), PCI_GRAPHICS);
return INO(K_DIR, D_PCIDEV, 0);
case K_USB: return INO(K_DIR, D_USBDEV, 0);
return INO(K_DIR, D_PCIROOT, 0);
case K_PCILINK: return INO(K_DIR, D_PCIDEV, 0);
case K_USBLINK: return INO(K_DIR, D_USBDEV, 0);
case K_USBROOT: {
int hc = usb_hc_pci(I_IDX(dino));
return hc >= 0 ? INO(K_PCI, hc, 0) : INO(K_DIR, D_PCIROOT, 0);
}
case K_USB: return INO(K_USBROOT, I_IDX(dino) >> 8, 0);
case K_MEMMAP: return INO(K_DIR, D_MEMMAP, 0);
case K_PSU: return INO(K_DIR, D_PSU, 0);
case K_KSUBV: return INO(K_DIR, D_VIRTUAL, 0);
@ -788,6 +889,131 @@ gen_usb(struct out *o, const struct usb_info *u, int f)
putc_(o, '\n');
}
// --- where a device is, and how to say so -----------------------------------
// "devices/pci0000:00/0000:00:14.0/usb1": the directory of a root hub; with
// `path`, the device below it. No leading slash. Returns how many directories
// lie between it and /sys, which is how many "../" a link inside it needs.
static int
usb_dir(struct out *o, int bus, const char *path)
{
int hc = usb_hc_pci(bus), depth = 3;
puts_(o, "devices/pci0000:00/");
if (hc >= 0) {
pci_name(o, pci_get(hc));
putc_(o, '/');
depth++;
}
puts_(o, "usb");
putu(o, (uint64)bus);
if (path) {
putc_(o, '/');
puts_(o, path);
depth++;
}
return depth;
}
static void
updirs(struct out *o, int n)
{
while (n-- > 0)
puts_(o, "../");
}
// The target of a symlink for the PCI or USB entry `ino`, or -1.
static int
link_text(uint64 ino, struct out *o)
{
int idx = I_IDX(ino), f = I_FILE(ino);
struct usb_info u;
switch (I_KIND(ino)) {
case K_PCILINK:
if (idx >= pci_count())
return -1;
puts_(o, "../../../devices/pci0000:00/");
pci_name(o, pci_get(idx));
return o->n;
case K_PCI:
if (idx >= pci_count())
return -1;
if (f == PCI_SUBSYS) {
puts_(o, "../../../bus/pci");
return o->n;
}
if (f == PCI_DRIVER && pci_get(idx)->driver) {
puts_(o, "../../../bus/pci/drivers/");
puts_(o, pci_get(idx)->driver);
return o->n;
}
return -1;
case K_USBLINK:
if ((idx & 0xFF) == 0) {
if (idx >> 8 < 1 || idx >> 8 > usb_nbuses())
return -1;
puts_(o, "../../../");
usb_dir(o, idx >> 8, 0);
return o->n;
}
if (usb_by_idx(idx, &u) < 0)
return -1;
puts_(o, "../../../");
usb_dir(o, u.bus, u.path);
return o->n;
case K_USB:
if (usb_by_idx(idx, &u) < 0)
return -1;
if (f == USB_SUBSYS) {
struct out d = { o->p + o->n, 0, 0 }; // measure only
updirs(o, usb_dir(&d, u.bus, u.path));
puts_(o, "bus/usb");
return o->n;
}
if (f == USB_DRIVER && u.driver[0]) {
struct out d = { o->p + o->n, 0, 0 };
updirs(o, usb_dir(&d, u.bus, u.path));
puts_(o, "bus/usb/drivers/");
puts_(o, u.driver);
return o->n;
}
return -1;
case K_USBROOT:
if (f == HUB_SUBSYS && idx >= 1 && idx <= usb_nbuses()) {
struct out d = { o->p + o->n, 0, 0 };
updirs(o, usb_dir(&d, idx, 0));
puts_(o, "bus/usb");
return o->n;
}
return -1;
}
return -1;
}
static void
gen_hub(struct out *o, int bus, int f)
{
switch (f) {
case 1:
puts_(o, "DEVTYPE=usb_device\nPRODUCT=1d6b/3/");
putx(o, 0x0601, 4, 0);
puts_(o, "\nTYPE=9/0/1\nBUSNUM=");
if (bus < 100) putc_(o, '0');
if (bus < 10) putc_(o, '0');
putu(o, (uint64)bus);
puts_(o, "\nDEVNUM=001");
break;
case 3: puts_(o, "1d6b"); break;
case 4: puts_(o, "0003"); break;
case 5: putu(o, (uint64)bus); break;
case 6: putu(o, 1); break;
case 7: puts_(o, "09"); break;
case 8: puts_(o, "5000"); break;
}
putc_(o, '\n');
}
// A text file, whole. Returns its length, or -1 if there is no such file.
static int
text(uint64 ino, char *buf, int cap)
@ -796,15 +1022,29 @@ text(uint64 ino, char *buf, int cap)
int idx = I_IDX(ino), f = I_FILE(ino);
switch (I_KIND(ino)) {
case K_PCILINK:
case K_USBLINK:
return link_text(ino, &o);
case K_PCI:
if (f == PCI_SUBSYS || f == PCI_DRIVER)
return link_text(ino, &o);
if (idx >= pci_count() || f < 1 || f >= NPCIFILES || f == PCI_CONFIG)
return -1;
gen_pci(&o, pci_get(idx), f);
return o.n;
case K_USBROOT:
if (idx < 1 || idx > usb_nbuses() || f < 1 || f >= NHUBFILES)
return -1;
if (f == HUB_SUBSYS)
return link_text(ino, &o);
gen_hub(&o, idx, f);
return o.n;
case K_USB: {
struct usb_info u;
if (usb_by_idx(idx, &u) < 0 || f < 1 || f >= NUSBFILES)
return -1;
if (f == USB_SUBSYS || f == USB_DRIVER)
return link_text(ino, &o);
gen_usb(&o, &u, f);
return o.n;
}
@ -895,6 +1135,77 @@ text(uint64 ino, char *buf, int cap)
return -1;
}
// ---------------------------------------------------------------------------
// Announcing devices
//
// A uevent is "action@/devpath", then NUL-separated KEY=value pairs: the
// four the kernel always sends, then whatever the device's own `uevent` file
// says. The same text serves a hot-plug event and the answer to
// `echo add > uevent`, so udev sees one thing however it learned of a device.
// ---------------------------------------------------------------------------
static void
announce(const char *action, const char *devpath, const char *subsys,
const char *vars, int vlen)
{
char msg[700];
struct out o = { msg, 0, (int)sizeof msg - 1 };
puts_(&o, action); puts_(&o, "@/"); puts_(&o, devpath); putc_(&o, 0);
puts_(&o, "ACTION="); puts_(&o, action); putc_(&o, 0);
puts_(&o, "DEVPATH=/"); puts_(&o, devpath); putc_(&o, 0);
puts_(&o, "SUBSYSTEM="); puts_(&o, subsys); putc_(&o, 0);
puts_(&o, "SEQNUM="); putu(&o, kdev_seqnum()); putc_(&o, 0);
for (int i = 0; i < vlen; i++)
putc_(&o, vars[i] == '\n' ? 0 : vars[i]);
uevent_send(UEVENT_GROUP_KERNEL, msg, o.n, 0);
}
void
sysfs_usb_uevent(const struct usb_info *u, const char *action)
{
char path[96], vars[300];
struct out p = { path, 0, sizeof path - 1 }, v = { vars, 0, sizeof vars - 1 };
usb_dir(&p, u->bus, u->path);
path[p.n] = 0;
gen_usb(&v, u, USB_UEVENT);
announce(action, path, "usb", vars, v.n);
}
static void
pci_uevent(int idx, const char *action)
{
char path[64], vars[400];
struct out p = { path, 0, sizeof path - 1 }, v = { vars, 0, sizeof vars - 1 };
if (idx >= pci_count())
return;
puts_(&p, "devices/pci0000:00/");
pci_name(&p, pci_get(idx));
path[p.n] = 0;
gen_pci(&v, pci_get(idx), 10);
announce(action, path, "pci", vars, v.n);
}
static void
hub_uevent(int bus, const char *action)
{
char path[96], vars[200];
struct out p = { path, 0, sizeof path - 1 }, v = { vars, 0, sizeof vars - 1 };
usb_dir(&p, bus, 0);
path[p.n] = 0;
gen_hub(&v, bus, HUB_UEVENT);
announce(action, path, "usb", vars, v.n);
}
void
sysfs_usb_root_uevent(int bus, const char *action)
{
hub_uevent(bus, action);
}
// ---------------------------------------------------------------------------
// fs_ops
// ---------------------------------------------------------------------------
@ -913,6 +1224,8 @@ exists_dir(uint64 ino)
struct usb_info u;
return usb_by_idx(I_IDX(ino), &u) == 0;
}
case K_USBROOT:
return I_IDX(ino) >= 1 && I_IDX(ino) <= usb_nbuses();
case K_MEMMAP: {
struct e820_rec e;
return e820_nth(I_IDX(ino), &e) == 0;
@ -939,8 +1252,12 @@ static int
is_link(uint64 ino)
{
int k = I_KIND(ino);
return k == K_KCLINK || k == K_KDEVL ||
(k == K_KDEV && I_FILE(ino) == KF_SUBSYSTEM);
int f = I_FILE(ino);
return k == K_KCLINK || k == K_KDEVL || k == K_PCILINK || k == K_USBLINK ||
(k == K_KDEV && f == KF_SUBSYSTEM) ||
(k == K_PCI && (f == PCI_SUBSYS || f == PCI_DRIVER)) ||
(k == K_USB && (f == USB_SUBSYS || f == USB_DRIVER)) ||
(k == K_USBROOT && f == HUB_SUBSYS);
}
static void
@ -991,7 +1308,8 @@ sysfs_iload(struct inode *ip)
ip->type = T_FILE;
if ((I_KIND(ino) == K_PCI && (I_FILE(ino) == PCI_ENABLE ||
I_FILE(ino) == 10 /* uevent */)) ||
(I_KIND(ino) == K_USB && I_FILE(ino) == NUSBFILES - 1) ||
(I_KIND(ino) == K_USB && I_FILE(ino) == USB_UEVENT) ||
(I_KIND(ino) == K_USBROOT && I_FILE(ino) == HUB_UEVENT) ||
(I_KIND(ino) == K_KDEV && I_FILE(ino) == KF_UEVENT))
ip->rwmode = 3;
return;
@ -1057,11 +1375,39 @@ sysfs_write(struct inode *ip, int u, uint64 s, uint64 o, uint n)
if (I_KIND(ip->inum) == K_PCI && I_FILE(ip->inum) == PCI_ENABLE)
return (int)n;
// `echo add > uevent`: udevadm trigger asks the kernel to say again what it
// said when the device appeared. Only the kernel's own devices are announced;
// a PCI or USB device's uevent accepts the write and says nothing, because
// their /sys paths are not the /devices tree an event's DEVPATH must name.
if (I_KIND(ip->inum) == K_PCI || I_KIND(ip->inum) == K_USB)
return (int)n;
// said when the device appeared.
{
int kind = I_KIND(ip->inum), f = I_FILE(ip->inum), idx = I_IDX(ip->inum);
int pcie = kind == K_PCI && f == 10;
int usbe = kind == K_USB && f == USB_UEVENT;
int hube = kind == K_USBROOT && f == HUB_UEVENT;
if (pcie || usbe || hube) {
char act[16];
uint k = n < sizeof(act) - 1 ? n : (uint)sizeof(act) - 1;
if (u) {
if (copy_from_user(act, s, k) < 0)
return -EFAULT;
} else {
memmove(act, (void *)s, k);
}
uint e = 0;
while (e < k && act[e] != '\n' && act[e] != ' ')
e++;
act[e] = 0;
const char *a = e ? act : "change";
struct usb_info ui;
if (pcie)
pci_uevent(idx, a);
else if (usbe && usb_by_idx(idx, &ui) == 0)
sysfs_usb_uevent(&ui, a);
else if (hube)
hub_uevent(idx, a);
return (int)n;
}
if (kind == K_PCI || kind == K_USB)
return (int)n;
}
if (I_KIND(ip->inum) == K_KDEV && I_FILE(ip->inum) == KF_UEVENT) {
char act[16];
uint k = n < sizeof(act) - 1 ? n : (uint)sizeof(act) - 1;