eudev starts at boot; Xorg finds its input devices through it

udevd and a one-shot trigger run from schinit, 10-schos.conf no longer lists
event0/event1, and the docs say what udev covers and what it does not.

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 11:44:21 +01:00
commit b3f72cd95e
13 changed files with 169 additions and 50 deletions

View file

@ -204,8 +204,8 @@ Another: **syscalls are declared in exactly one place**,
- **No GPU acceleration.** Xorg draws in software; the plan is Linux's `amdgpu`
through LDLS ([docs/plan/gpu.md](docs/plan/gpu.md)). There is no IOMMU either,
so a driver in a process is isolated against its bugs, not against malice.
- **No udev.** Xorg is told its input devices by a config file instead of
finding them; `/sys` has no device tree and no uevent socket.
- **udev sees the kernel's own devices only.** eudev runs and Xorg finds its input devices
through it; PCI and USB devices are not yet in the `/sys/devices` tree.
- **WebKit browsers do not start.** NetSurf does; Epiphany's web process dies at
start-up, and it has not been run to ground.
- **USB 2.0 controllers (EHCI)** have no driver. On older machines the ports

View file

@ -94,6 +94,7 @@ Read in this order if you are new:
| [fat.md](fat.md) | FAT12/16/32 with long names: USB sticks, SD cards, the ESP. |
| [devices.md](devices.md) | Buses, classes, binding, and the vtables consumers talk to. |
| [usb.md](usb.md) | xHCI, the USB core, and the three class drivers. |
| [udev.md](udev.md) | eudev on SchOS: the device table, the `/sys` tree, uevents, datagram sockets. |
| [graphics.md](graphics.md) | Framebuffer, `/dev/fb0`, text rendering, the console, PSOGM and GMMOS. |
| [desktop.md](desktop.md) | Xorg on the framebuffer, Window Maker, and what they stand on. |
| [packages.md](packages.md) | The apk root: Alpine's packages, and SchOS as one of them. |

View file

@ -49,11 +49,9 @@ the real program is for.
`xterm` runs and exits, and the server stays up.
- **`/root/.xinitrc`** is `exec wmaker`. Without it xinit falls back to twm and an xterm, which
are not in the image.
- **`/etc/X11/xorg.conf.d/10-schos.conf`** names the input devices. Alpine's Xorg asks udev for
them, SchOS has no udev, and X came up with no keyboard and no mouse: the pointer did not
move and nothing could end the session. SchOS has exactly two input nodes whatever the
machine, so a fixed list is the whole answer (`AutoAddDevices` off, Ctrl+Alt+Backspace
enabled).
- **`/etc/X11/xorg.conf.d/10-schos.conf`** holds what the defaults do not give: Ctrl+Alt+Backspace
enabled, the keyboard options, and the framebuffer driver. Input devices are *found*, not
named: Xorg asks libudev, which reads what `udevd` wrote to `/run/udev` ([udev.md](udev.md)).
- **`/etc/asound.conf`** is for sound, not X ([audio.md](audio.md)).
## Using it
@ -71,8 +69,9 @@ through ACPI, and reach X as a mouse: pointer, click and tap.
- **No acceleration.** Software rendering to the framebuffer: no DRM, no glamor, no DRI, no
OpenGL beyond what software gives. GPU drivers are a plan ([../plan/gpu.md](../plan/gpu.md)).
- **No udev.** Input devices are named in a config file, not found; `/sys` has no `devices`
tree and no uevent socket.
- **udev covers the kernel's own devices only.** Input, terminals, memory devices, the
framebuffer and disks are announced; PCI and USB devices are in `/sys/bus/*/devices` but are
not yet in the `/sys/devices` tree or on the uevent socket ([udev.md](udev.md)).
- **Touchpads are mice**: no two-finger scroll, no gestures, no palm rejection.
- **No resolution change from inside X.** The mode is the console's; `schmode` changes the
`vgpud` head only, and the loader's menu is where a real panel's mode is chosen.

View file

@ -275,10 +275,15 @@ at the moment it is read, so a stick shows up in `/sys/bus/usb/devices` as soon
as it is attached. Text files report size 0, as `/proc`'s do; binary ones (the
tables, `config`) report their real length, since tools copy them by it.
What is missing, compared with Linux: `/sys/devices` and its symlinks (the
The kernel's own devices (input, tty, mem, graphics, block) have the Linux tree:
`/sys/devices/virtual/<class>/<name>` with `dev`, `uevent` and a `subsystem`
link, `/sys/class/<class>/<name>` and `/sys/dev/{char,block}/MAJ:MIN` as symlinks
into it, and input capabilities; `uevent` is writable, and a write is announced
on the uevent netlink socket ([udev.md](udev.md)).
What is missing, compared with Linux: PCI and USB devices in that tree (the
`bus/*/devices` entries are directories rather than links into it),
`/sys/class` beyond `dmi` and `power_supply`, `/sys/block`, a uevent socket (so
no udev), and anything writable. PCI `config` is
`/sys/class` entries for them, `/sys/block`, and most of what is writable. PCI `config` is
256 bytes, because config space is read through port `0xCF8`, which reaches
no further.

View file

@ -649,7 +649,11 @@ information was there and the code did not ask.
`SO_BROADCAST`, `TCP_NODELAY` reach lwIP's pcb; `SO_SNDBUF`/`SO_RCVBUF` report the
real size; `SO_PASSCRED` on Unix sockets) and refuses the rest with `ENOPROTOOPT`.
Notably absent: `SO_RCVTIMEO`/`SO_SNDTIMEO`.
- **`SOCK_DGRAM` over AF_UNIX does not exist** — stream only.
- **`SOCK_DGRAM` and `SOCK_SEQPACKET` over AF_UNIX** keep message boundaries (each message carries a
16-byte header in the same ring) but have no unconnected form: `socketpair` and listeners work,
`sendto` a path does not.
- **Netlink `NETLINK_KOBJECT_UEVENT`** is the one multicast family: sockets bound to group 1 hear the
kernel's device events, group 2 what udevd re-announces, and a datagram can be addressed to a port id.
- **No IPv6**, no firewall or packet filter of any kind, and a TCP stack that has
been used for downloads at tens of megabits and no more than that.
- **Three drivers in the kernel, one Linux driver outside it**, and the class is

82
docs/architecture/udev.md Normal file
View file

@ -0,0 +1,82 @@
# 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/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. A write to a PCI or USB device's `uevent` is
accepted and says nothing yet, because those devices are not in the
`/sys/devices` tree and an event's `DEVPATH` has to name something that exists.
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.
## What is not there
- PCI and USB devices in `/sys/devices`, and `add`/`remove` events from the
hotplug thread. A USB keyboard works because every keyboard is `event0`, not
because udev knew it arrived.
- `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`.
- `libinput` and `libseat` (next; 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`.

View file

@ -114,7 +114,7 @@ it lives.
| `mprotect` is a no-op, nothing has NX | [kernel.md](../architecture/kernel.md) |
| No routing table | first interface wins, which is a rule and not routing — [networking.md](../architecture/networking.md) |
| No descriptor garbage collector | a socket passed over itself and never received leaks the pair |
| No udev / uevent socket | Xorg is told its input devices — [desktop.md](../architecture/desktop.md) |
| udev sees only the kernel's own devices | PCI and USB are not in the `/sys/devices` tree — [udev.md](../architecture/udev.md) |
| No EHCI driver | USB 2.0 ports wired only to EHCI do nothing — [usb.md](../architecture/usb.md) |
| Touchpads are mice | no two-finger scroll — [desktop.md](../architecture/desktop.md) |
| `ru_stime` is zero | [kernel.md](../architecture/kernel.md) |

View file

@ -62,11 +62,10 @@ compositor worth running is years of work that Alpine already packages.) The des
Wayland compositor (labwc, sway, weston, a GNOME or KDE session) is the larger step. What Wayland — and a modern X — need
from the system, in the order they are useful:
1. **udev.** Today Xorg is *told* its input devices (`xorg.conf.d/10-schos.conf`) because nothing announces them. The fix is
Alpine's `eudev` (or `udev`) running on SchOS, which wants: a **uevent netlink socket** (`NETLINK_KOBJECT_UEVENT`, with the
`add`/`remove`/`change` messages and their environment), a real **`/sys/devices` tree** with `/sys/bus/*/devices/*` and
`/sys/class/*/*` as symlinks into it, `uevent` files that can be written to replay events (`udevadm trigger`), and devtmpfs
that udev can create nodes in. libinput then enumerates evdev devices through it.
1. **udev — done for the kernel's own devices** ([udev.md](../architecture/udev.md)). eudev runs from `schinit`, the uevent
netlink socket and the `/sys/devices` tree exist, devtmpfs takes udev's links, and Xorg finds its keyboard and mouse
through it. **Left:** PCI and USB devices in the same tree, with `add`/`remove` events from the hotplug thread, so a USB
keyboard or a GPU appears to udev; libinput then enumerates evdev devices through it.
2. **libinput and libseat** (seatd is in Alpine): evdev with the properties libinput reads (`ID_INPUT_*` from udev), and a way
for a non-root compositor to take the keyboard and the display from the kernel — the `VT_PROCESS` handshake exists.
3. **A DRM device.** Wayland compositors want `/dev/dri/card0` with KMS, and Mesa wants `renderD128`. SchOS has neither: it has

View file

@ -0,0 +1,14 @@
# Replay the devices that were already there when udevd started, so /run/udev
# knows about them. Runs once; see /etc/udev-trigger.
#
# udevd is started by its own job and schinit has no ordering, so the script
# waits for the daemon's control socket.
Label = os.schos.udev-trigger
Program = /bin/sh
Args = sh /etc/udev-trigger
RunAtLoad = yes
KeepAlive = no
StandardOut = /dev/tty1
StandardError = /dev/tty1

16
etc/schinit/udevd.job Normal file
View file

@ -0,0 +1,16 @@
# eudev's daemon: the thing that turns the kernel's uevents into /run/udev
# data and /dev symlinks, which is how Xorg and every Wayland compositor learn
# what input and display devices exist.
#
# Alpine's eudev package; the kernel side is the uevent netlink socket and the
# /sys device tree (kernel/dev/kdev.c, kernel/fs/sysfs.c).
Label = os.schos.udevd
Program = /sbin/udevd
Args = udevd
RunAtLoad = yes
KeepAlive = yes
ThrottleInterval = 3
StandardOut = /dev/tty1
StandardError = /dev/tty1

9
etc/udev-trigger Normal file
View file

@ -0,0 +1,9 @@
#!/bin/sh
# Wait for udevd to listen, then ask the kernel to announce every device again.
i=0
while [ ! -S /run/udev/control ] && [ $i -lt 100 ]; do
sleep 0.1
i=$((i + 1))
done
udevadm trigger --action=add
udevadm settle --timeout=60

View file

@ -15,15 +15,18 @@
// is the property sleep/wakeup actually requires. Thirty-two sockets carrying
// GUI messages will not notice the contention.
//
// **Names are held in a table here, not in the filesystem.** A real Unix
// socket appears in a directory listing as a special file, and binding one
// creates that node. Doing that properly means a new inode type threaded
// through both filesystems, iget, dirlookup and stat -- a VFS change, not a
// socket change. The table gives every behaviour that matters: a client
// finds a server by a name it agreed in advance, which is the whole job.
// What is lost is `ls -l /tmp/schcom` showing an `s`, and `unlink()` of a
// socket path failing with ENOENT. Both are worth fixing when the VFS next
// gets opened up; neither blocks anything.
// **Names live in the filesystem.** bind() creates a socket node
// (kernel/sys/sys_net.c) and connect() resolves a path to the inode and looks
// for the socket bound to that inode, so every spelling of a path arrives at
// the same place and the node can be stat()ed and unlink()ed.
//
// **Datagram and sequenced-packet sockets** share the ring with streams: each
// message is a 16-byte header -- length and multicast groups, sender port or
// pid, sender uid and gid -- and then its bytes, so a read takes exactly one
// message and credentials belong to the message, not to the socket. udev's
// workers share one end of a socketpair and the manager has to know which of
// them wrote what. The kernel's uevents travel the same way, to netlink
// sockets of protocol NETLINK_KOBJECT_UEVENT.
#include "core/types.h"
#include "core/defs.h"

View file

@ -121,37 +121,26 @@ cat > "$P/root/.xinitrc" <<'XEOF'
exec wmaker
XEOF
# X's input devices, named rather than found. Alpine's Xorg asks udev for
# them, SchOS has no udev, and X came up with no keyboard and no mouse: the
# pointer did not move, Ctrl+Alt+F-keys did nothing and nothing could end the
# session. SchOS has exactly two input nodes whatever the machine -- every
# keyboard on event0, every mouse on event1 (kernel/dev/input.c) -- so a
# fixed list is the whole answer. The file is SchOS's, in xorg.conf.d, which
# no Alpine package writes into.
# X's input devices are found, not named: udevd (etc/schinit/udevd.job) turns
# the kernel's uevents into /run/udev data, and Xorg asks libudev for every
# device with ID_INPUT set. That was a hand-written list of event0 and event1
# while SchOS had no udev. The file is SchOS's, in xorg.conf.d, which no Alpine
# package writes into; what is left here is the options the defaults do not
# give, and the framebuffer driver.
mkdir -p "$P/etc/X11/xorg.conf.d"
cat > "$P/etc/X11/xorg.conf.d/10-schos.conf" <<'XEOF'
# SchOS has no udev, so X is told its input devices instead of finding them.
# event0 is every keyboard and event1 every mouse (kernel/dev/input.c).
Section "ServerFlags"
Option "AutoAddDevices" "false"
# Ctrl+Alt+Backspace ends X.
Option "DontZap" "false"
EndSection
Section "InputDevice"
Identifier "keyboard"
Driver "evdev"
Option "Device" "/dev/input/event0"
Section "InputClass"
Identifier "SchOS keyboards"
MatchIsKeyboard "on"
Option "XkbRules" "evdev"
Option "XkbOptions" "terminate:ctrl_alt_bksp"
EndSection
Section "InputDevice"
Identifier "mouse"
Driver "evdev"
Option "Device" "/dev/input/event1"
EndSection
Section "Device"
Identifier "fb"
Driver "fbdev"
@ -165,8 +154,6 @@ EndSection
Section "ServerLayout"
Identifier "schos"
Screen "screen"
InputDevice "keyboard" "CoreKeyboard"
InputDevice "mouse" "CorePointer"
EndSection
XEOF