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` - **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, 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. 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 - **udev sees the kernel's own devices only.** eudev runs and Xorg finds its input devices
finding them; `/sys` has no device tree and no uevent socket. 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 - **WebKit browsers do not start.** NetSurf does; Epiphany's web process dies at
start-up, and it has not been run to ground. start-up, and it has not been run to ground.
- **USB 2.0 controllers (EHCI)** have no driver. On older machines the ports - **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. | | [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. | | [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. | | [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. | | [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. | | [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. | | [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. `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 - **`/root/.xinitrc`** is `exec wmaker`. Without it xinit falls back to twm and an xterm, which
are not in the image. are not in the image.
- **`/etc/X11/xorg.conf.d/10-schos.conf`** names the input devices. Alpine's Xorg asks udev for - **`/etc/X11/xorg.conf.d/10-schos.conf`** holds what the defaults do not give: Ctrl+Alt+Backspace
them, SchOS has no udev, and X came up with no keyboard and no mouse: the pointer did not enabled, the keyboard options, and the framebuffer driver. Input devices are *found*, not
move and nothing could end the session. SchOS has exactly two input nodes whatever the named: Xorg asks libudev, which reads what `udevd` wrote to `/run/udev` ([udev.md](udev.md)).
machine, so a fixed list is the whole answer (`AutoAddDevices` off, Ctrl+Alt+Backspace
enabled).
- **`/etc/asound.conf`** is for sound, not X ([audio.md](audio.md)). - **`/etc/asound.conf`** is for sound, not X ([audio.md](audio.md)).
## Using it ## 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 - **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)). 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` - **udev covers the kernel's own devices only.** Input, terminals, memory devices, the
tree and no uevent socket. 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. - **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 - **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. `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 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. 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), `bus/*/devices` entries are directories rather than links into it),
`/sys/class` beyond `dmi` and `power_supply`, `/sys/block`, a uevent socket (so `/sys/class` entries for them, `/sys/block`, and most of what is writable. PCI `config` is
no udev), and anything writable. PCI `config` is
256 bytes, because config space is read through port `0xCF8`, which reaches 256 bytes, because config space is read through port `0xCF8`, which reaches
no further. 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 `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`. real size; `SO_PASSCRED` on Unix sockets) and refuses the rest with `ENOPROTOOPT`.
Notably absent: `SO_RCVTIMEO`/`SO_SNDTIMEO`. 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 - **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. 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 - **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) | | `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 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 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) | | 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) | | Touchpads are mice | no two-finger scroll — [desktop.md](../architecture/desktop.md) |
| `ru_stime` is zero | [kernel.md](../architecture/kernel.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 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: 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 1. **udev — done for the kernel's own devices** ([udev.md](../architecture/udev.md)). eudev runs from `schinit`, the uevent
Alpine's `eudev` (or `udev`) running on SchOS, which wants: a **uevent netlink socket** (`NETLINK_KOBJECT_UEVENT`, with the netlink socket and the `/sys/devices` tree exist, devtmpfs takes udev's links, and Xorg finds its keyboard and mouse
`add`/`remove`/`change` messages and their environment), a real **`/sys/devices` tree** with `/sys/bus/*/devices/*` and through it. **Left:** PCI and USB devices in the same tree, with `add`/`remove` events from the hotplug thread, so a USB
`/sys/class/*/*` as symlinks into it, `uevent` files that can be written to replay events (`udevadm trigger`), and devtmpfs keyboard or a GPU appears to udev; libinput then enumerates evdev devices through it.
that udev can create nodes in. 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 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. 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 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 // is the property sleep/wakeup actually requires. Thirty-two sockets carrying
// GUI messages will not notice the contention. // GUI messages will not notice the contention.
// //
// **Names are held in a table here, not in the filesystem.** A real Unix // **Names live in the filesystem.** bind() creates a socket node
// socket appears in a directory listing as a special file, and binding one // (kernel/sys/sys_net.c) and connect() resolves a path to the inode and looks
// creates that node. Doing that properly means a new inode type threaded // for the socket bound to that inode, so every spelling of a path arrives at
// through both filesystems, iget, dirlookup and stat -- a VFS change, not a // the same place and the node can be stat()ed and unlink()ed.
// 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. // **Datagram and sequenced-packet sockets** share the ring with streams: each
// What is lost is `ls -l /tmp/schcom` showing an `s`, and `unlink()` of a // message is a 16-byte header -- length and multicast groups, sender port or
// socket path failing with ENOENT. Both are worth fixing when the VFS next // pid, sender uid and gid -- and then its bytes, so a read takes exactly one
// gets opened up; neither blocks anything. // 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/types.h"
#include "core/defs.h" #include "core/defs.h"

View file

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