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

@ -32,6 +32,7 @@
#include "fs/file.h"
#include "dev/pci.h"
#include "dev/usb.h"
#include "core/hwinfo.h"
#include "xhci.h"
#define RING_TRBS 64 // TRBs per ring, including the link
@ -1673,6 +1674,11 @@ xhci_probe(struct pci_dev *d)
kinfo("xhci: no devices attached\n");
// Number the bus and announce its root hub before the first port can be
// scanned, so `usbN` is in /sys, and udev knows of it, ahead of anything
// plugged into it.
usb_bus_add(x);
// Now, and only now, turn interrupts on. The order is: claim the line
// (which unmasks it and sets level/active-low), clear anything the
// controller already considers pending so that it drives INTx low, and then

View file

@ -185,20 +185,52 @@ static int present_init;
static struct usb_dev *present[NPRESENT];
static void *buses[NBUSES];
// Give a controller its bus number if it has none. Caller holds present_lock.
// Returns the number (1-based), or 0 if there is no room.
static int
bus_number(void *hc)
{
for(int b = 0; b < NBUSES; b++){
if(buses[b] == hc) return b + 1;
if(buses[b] == 0){ buses[b] = hc; return b + 1; }
}
return 0;
}
static void
present_add(struct usb_dev *dev)
present_init_once(void)
{
if(!present_init){
initlock(&present_lock, "usbpresent");
present_init = 1;
}
}
// A controller is up: number its bus now and announce its root hub, so usbN
// is in /sys before anything is plugged into it. The loader of the driver that
// calls this runs once per controller, in PCI order -- which is what ties bus
// N to the Nth controller in sysfs.c.
void
usb_bus_add(void *hc)
{
int n;
present_init_once();
acquire(&present_lock);
n = bus_number(hc);
release(&present_lock);
if(n)
sysfs_usb_root_uevent(n, "add");
}
static void
present_add(struct usb_dev *dev)
{
present_init_once();
acquire(&present_lock);
for(int i = 0; i < NPRESENT; i++)
if(present[i] == 0){ present[i] = dev; break; }
for(int b = 0; b < NBUSES; b++){
if(buses[b] == dev->hc) break;
if(buses[b] == 0){ buses[b] = dev->hc; break; }
}
bus_number(dev->hc);
release(&present_lock);
}
@ -223,6 +255,44 @@ put_num(char *out, int *n, int cap, int v)
out[*n] = 0;
}
// Copy `d`'s identity out. Caller holds present_lock, or is the one thread
// that owns `d` (attach and detach).
static void
fill_info(struct usb_dev *d, struct usb_info *o)
{
memset(o, 0, sizeof *o);
o->bus = 1;
for(int b = 0; b < NBUSES; b++)
if(buses[b] == d->hc){ o->bus = b + 1; break; }
o->devnum = d->slot;
o->speed = d->speed;
o->vid = d->desc.idVendor;
o->pid = d->desc.idProduct;
o->bcd = d->desc.bcdDevice;
o->cls = d->desc.bDeviceClass;
o->subcls = d->desc.bDeviceSubClass;
o->proto = d->desc.bDeviceProtocol;
o->nconf = d->desc.bNumConfigurations;
// "<bus>-<root port>[.<hub port>...]": the route string holds one
// downstream port per tier below the root, a nibble each.
int n = 0, cap = (int)sizeof o->path;
put_num(o->path, &n, cap, o->bus);
o->path[n++] = '-';
put_num(o->path, &n, cap, d->port);
for(int t = 0; t < d->tier && n < cap - 3; t++){
o->path[n++] = '.';
put_num(o->path, &n, cap, (int)((d->route >> (4 * t)) & 0xF));
}
const char *drv = d->driver;
for(int k = 0; !drv && k < NIFDEV; k++)
if(ifdevs[k].in_use && ifdevs[k].base == d)
drv = ifdevs[k].driver;
if(drv)
safestrcpy(o->driver, drv, sizeof o->driver);
}
int
usb_info_nth(int idx, struct usb_info *o)
{
@ -234,37 +304,7 @@ usb_info_nth(int idx, struct usb_info *o)
struct usb_dev *d = present[i];
if(d == 0 || seen++ != idx)
continue;
memset(o, 0, sizeof *o);
o->bus = 1;
for(int b = 0; b < NBUSES; b++)
if(buses[b] == d->hc){ o->bus = b + 1; break; }
o->devnum = d->slot;
o->speed = d->speed;
o->vid = d->desc.idVendor;
o->pid = d->desc.idProduct;
o->bcd = d->desc.bcdDevice;
o->cls = d->desc.bDeviceClass;
o->subcls = d->desc.bDeviceSubClass;
o->proto = d->desc.bDeviceProtocol;
o->nconf = d->desc.bNumConfigurations;
// "<bus>-<root port>[.<hub port>...]": the route string holds one
// downstream port per tier below the root, a nibble each.
int n = 0, cap = (int)sizeof o->path;
put_num(o->path, &n, cap, o->bus);
o->path[n++] = '-';
put_num(o->path, &n, cap, d->port);
for(int t = 0; t < d->tier && n < cap - 3; t++){
o->path[n++] = '.';
put_num(o->path, &n, cap, (int)((d->route >> (4 * t)) & 0xF));
}
const char *drv = d->driver;
for(int k = 0; !drv && k < NIFDEV; k++)
if(ifdevs[k].in_use && ifdevs[k].base == d)
drv = ifdevs[k].driver;
if(drv)
safestrcpy(o->driver, drv, sizeof o->driver);
fill_info(d, o);
release(&present_lock);
return 0;
}
@ -272,6 +312,35 @@ usb_info_nth(int idx, struct usb_info *o)
return -1;
}
int
usb_nbuses(void)
{
int n = 0;
if(!present_init)
return 0;
acquire(&present_lock);
for(int b = 0; b < NBUSES; b++)
if(buses[b])
n++;
release(&present_lock);
return n;
}
// Announce `dev` to udev. Attach says "add" once the device is in the table,
// so a rule that opens its /sys directory finds it; detach says "remove"
// first, while the directory still exists.
static void
announce(struct usb_dev *dev, const char *action)
{
struct usb_info u;
if(!present_init)
return;
acquire(&present_lock);
fill_info(dev, &u);
release(&present_lock);
sysfs_usb_uevent(&u, action);
}
void
usb_attach(struct usb_dev *dev)
{
@ -282,6 +351,7 @@ usb_attach(struct usb_dev *dev)
if(buf == 0)
return;
present_add(dev);
announce(dev, "add");
// Nine bytes first, because wTotalLength is in them and there is no other
// way to know how much to ask for.
@ -365,6 +435,7 @@ usb_attach(struct usb_dev *dev)
void
usb_detach(struct usb_dev *dev)
{
announce(dev, "remove");
present_del(dev);
for(int i = 0; i < NIFDEV; i++){
struct usb_dev *d = &ifdevs[i];