SchOS/docs/architecture/udev.md
Nexoniarz d650ff433b udev: PCI bridges as parents, USB interfaces, the controller's own address, and the attributes lsusb reads
PCI devices now nest under the bridge they sit behind (from each bridge's
secondary..subordinate window), so a controller behind a root port is where
Linux would put it. USB interfaces (1-5:1.0) are directories with class,
number, a driver link and a uevent with INTERFACE= and a MODALIAS, announced
after the drivers have been offered the device and removed before it. The xHCI
driver tells the USB core its PCI address when it comes up, replacing the guess
that bus N was the Nth controller. Devices and root hubs carry version,
bMaxPacketSize0, bNumInterfaces, bmAttributes and the rest; lsusb -t from
Alpine's usbutils now draws the tree.

Checked on q35 with an xHCI behind a pcie-root-port and a USB keyboard on each
controller. Plain lsusb still fails: libusb wants usbfs, and refuses a kernel
that calls itself 0.1.0.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01EVRhxi9zpdW2uDUN4FVcao
2026-10-26 17:54:13 +01:00

138 lines
7.7 KiB
Markdown

# udev
Alpine's `eudev` runs on SchOS as an ordinary process, started by `schinit`
(`etc/schinit/udevd.job`, with `udev-trigger.job` replaying the devices that
were there first). It is what lets Xorg *find* its keyboard and mouse instead of
being told, and the road to a Wayland compositor starts here: libinput and
libseat both ask udev what exists.
The kernel's half is three pieces, all small, none of them specific to udev.
## The device table: `kernel/dev/kdev.c`
The devices the kernel implements itself — input event nodes, the memory
devices, terminals, the framebuffer, the disks — are listed once, with their
class, name, `/dev` name and parent. Everything below is generated from it.
The device numbers are the ones `stat()` reports for the node, not SchOS's
internal ones: `/dev/tty1` is `4:1` to a Linux program (`linux_tty_rdev` in
`kernel/sys/sys_fs.c`), and udev compares what it reads in `/sys` with what
`stat()` says about the node. `dev_stat_numbers()` is the one mapping both use.
## `/sys`: `kernel/fs/sysfs.c`
```
/sys/devices/pci0000:00/0000:00:1f.2/ vendor device class ... uevent subsystem -> ../../../bus/pci driver
/sys/devices/pci0000:00/0000:00:14.0/usb1/1-3 idVendor idProduct ... uevent subsystem -> ../../../../../bus/usb
/sys/bus/pci/devices/0000:00:1f.2 -> ../../../devices/pci0000:00/0000:00:1f.2
/sys/bus/usb/devices/1-3 -> ../../../devices/pci0000:00/0000:00:14.0/usb1/1-3
/sys/devices/virtual/input/input0/ name capabilities/{ev,key,rel,abs} properties
/sys/devices/virtual/input/input0/event0/ dev uevent subsystem -> ../../../../../class/input
/sys/class/input/event0 -> ../../devices/virtual/input/input0/event0
/sys/dev/char/13:64 ... -> ../../devices/virtual/...
```
Symlinks are real (`T_SYMLINK` inodes whose target is generated). `uevent` is
readable (`DEVPATH=`, `SUBSYSTEM=`, `MAJOR=`, `MINOR=`, `DEVNAME=`) and
writable: `echo add > uevent` — which is what `udevadm trigger` does — makes the
kernel announce the device again. PCI and USB devices announce the same way:
`MODALIAS`, `PCI_ID`, `PCI_CLASS`, `PCI_SLOT_NAME` for a PCI device, `PRODUCT`,
`TYPE`, `BUSNUM`, `DEVNUM` for a USB one.
**PCI devices** are nested under the PCI-to-PCI bridge they sit behind: a
device's parent is the bridge whose secondary..subordinate bus range holds its
bus (the innermost if several do), read from the bridge's config space, so an
xHCI behind a root port is `pci0000:00/0000:00:04.0/0000:01:00.0`. **USB
devices** sit under the xHCI controller that carries their bus, below that
bus's root hub `usbN`; the controller tells the USB core its own PCI address
when it comes up, so the link is recorded, not guessed. A hub's children are
its siblings, not its children.
**USB interfaces** (`1-5:1.0`) are directories below their device, with
`bInterfaceClass`, `bInterfaceNumber`, a `driver` link when one is bound, and a
`uevent` carrying `DEVTYPE=usb_interface`, `INTERFACE=` and a `MODALIAS=usb:...`
that names the interface's class. They are announced after the drivers have
been offered the device, so the event says which interface was taken, and
removed before the device. Devices also carry what `lsusb -t` and libusb read:
`version`, `bMaxPacketSize0`, `bNumInterfaces`, `bmAttributes`, `bMaxPower`.
**Hot-plug is real**: the USB core announces `add` when a device is attached
and `remove` as it detaches, and `usbN` is announced when its controller comes
up. With `udevadm monitor`, plugging in a mouse prints the kernel event and
then the same event after udev's rules have run. PCI has no hot-plug here.
The input capability files are what `input_id` reads to decide
`ID_INPUT_KEYBOARD` or `ID_INPUT_MOUSE`. SchOS has two input devices whatever
the machine — every keyboard on `event0`, every mouse on `event1`
(`kernel/dev/input.c`) — so each has a fixed, plausible capability set.
## `/dev`: `kernel/fs/devfs.c`
devtmpfs is no longer read-only: udev creates `/dev/char/MAJ:MIN`, `/dev/fd`,
`/dev/input/by-*` and the like. A small in-memory overlay (192 entries) holds
directories and symlinks, listed after the kernel's own nodes in whichever
directory they are in. Device nodes themselves are still the kernel's: `mknod`
and regular files in `/dev` are refused.
## The sockets: `kernel/net/unix.c`
- **`AF_UNIX` `SOCK_DGRAM` and `SOCK_SEQPACKET`** keep message boundaries.
udevd's control socket is `SEQPACKET`, and its manager and workers talk over
a `DGRAM` socketpair. A message carries its sender's credentials with it
(several workers share one end), a zero-length datagram is a datagram, and a
datagram socket never reads end-of-stream.
- **`AF_NETLINK` / `NETLINK_KOBJECT_UEVENT`** sockets bind to multicast groups:
group 1 is the kernel's events, group 2 is what udevd re-announces once its
rules have run. A datagram can also be addressed to a port id — udevd hands
an event to an idle worker that way, and the worker believes it only from the
manager's port. The kernel's messages arrive as credentials with uid 0, pid 0.
A queue that is full drops the message, as Linux does, and `udevadm trigger`
is how a reader recovers.
## libinput and libseat
`libinput` (Alpine) reads the evdev nodes udev tagged and `xf86-input-libinput`
gives Xorg the same library; `libinput list-devices` as an ordinary user shows
`SchOS keyboard` and `SchOS pointer`. Xorg's `40-libinput.conf` now claims both,
and the log says `Using input driver 'libinput'`.
`seatd` (job `etc/schinit/seatd.job`, socket `/run/seatd.sock`, group `video`)
and `libseat` let a program that is not root take devices from the system. The
client asks seatd for `seat0`; seatd takes the VT (`VT_SETMODE`, `KDSETMODE`,
`KDSKBMODE`, all there already), opens the device as root and passes the
descriptor back over the socket with `SCM_RIGHTS`. Checked as `schos` against
`/dev/input/event0` and `event1` with a small program linked to libseat, reading
the device's name through the descriptor it was handed.
Getting there found two kernel bugs, both in descriptor passing:
- **A socket returned by `accept()` was not an `AF_UNIX` socket as far as
`sendmsg` was concerned.** The end `connect()` queues for the listener was
allocated with its domain left at zero, and `sendmsg` refuses `SCM_RIGHTS` on
anything else. So a *server* could receive descriptors and never send one:
seatd, and equally a Wayland compositor handing a client a buffer.
- **A control buffer longer than its messages was an error.** seatd passes a
zeroed array with one descriptor at the front; Linux treats a following header
too short to be one as the end. `sendmsg` now does too.
## What is not there
- `/dev/bus/usb` nodes (usbfs). `lsusb -t` works because it reads sysfs; plain
`lsusb` goes through libusb, which also refuses to start while `uname -r`
says `0.1.0` (it wants Linux 2.6.32 or newer) and wants usbfs to open devices.
- A hub's port count (`maxchild` reads 0), and string descriptors
(`manufacturer`, `product`, `serial`).
- A USB keyboard works because every keyboard is `event0`; udev knows the
interface arrived but the input device does not hang below it.
- `hwdb` (`/etc/udev/hwdb.bin`): `udevadm hwdb --update` has not been run in the
image, so `60-input-id` logs "returned non-zero" for the hwdb import. The
properties that matter come from `input_id`.
- Block device partitions, `by-id` / `by-uuid` links, and a `/sys/block`.
- A compositor. libinput and libseat are in place, DRM is not; see [../plan/roadmap.md](../plan/roadmap.md).
## Testing
Boot the ISO and run `udevd --debug &`, then `udevadm trigger; udevadm settle`.
`/run/udev/data/c7:0` should contain
`ID_INPUT_KEYBOARD=1`, and `udevadm info /dev/input/event0` should print the
device. Under X, `/var/log/Xorg.0.log` says `config/udev: Adding input device`.