docs: architecture describes what is built now
kernel (threads, demand paging, TLB shootdowns, dynamic linking, RAM direct map), syscalls, filesystem (/proc, /sys, /dev), terminals (12, VT handshake, ptys), networking (r8169 through LDLS), LDLS (module sets; Linux code may ship), devices (what stays in the kernel; hybrid.md's lasting content), boot (the loader's placement of the kernel), graphics (fbdev, GMMOS, QR 1-40), audio (ALSA over OSS), debugging, usb. New: packages.md (the apk root) and desktop.md (Xorg on the framebuffer). Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01EVRhxi9zpdW2uDUN4FVcao
This commit is contained in:
parent
655c7d8d01
commit
2556a955b3
15 changed files with 921 additions and 341 deletions
|
|
@ -1,7 +1,9 @@
|
|||
# Networking
|
||||
|
||||
ARP, IPv4, ICMP, UDP, TCP and DHCP, over a driver-agnostic interface class,
|
||||
reached through the same socket syscalls as Unix domain sockets.
|
||||
reached through the same socket syscalls as Unix domain sockets — plus DNS
|
||||
(resolved in the process, by musl), HTTPS (`wget`, Alpine's TLS libraries) and a
|
||||
small netlink responder for `getifaddrs`.
|
||||
|
||||
Verified end to end rather than by inspection: `bootcheck -Net` starts a
|
||||
listener and a UDP responder on the host, tells the guest to talk to them, and
|
||||
|
|
@ -25,7 +27,9 @@ that completes a handshake and drops every byte.
|
|||
└────────────────┬───────────────────┘
|
||||
│ struct netdev_ops
|
||||
┌────────────────┴───────────────────┐
|
||||
│ e1000, and any future NIC driver │
|
||||
│ e1000 rtl8139 r8169 — or a │ in the kernel
|
||||
│ Linux driver in a process, which │ (LDLS, via /dev/netdev)
|
||||
│ is a netdev like any other │
|
||||
└────────────────────────────────────┘
|
||||
```
|
||||
|
||||
|
|
@ -86,50 +90,55 @@ covers two decades of silicon — and why the driver prints the chip's XID on
|
|||
probe, since the PCI device id tells you almost nothing about which part
|
||||
arrived.
|
||||
|
||||
### Why it is native, when the plan said LDLS
|
||||
### Native first, then Linux's own
|
||||
|
||||
The first plan was to run Linux's `r8169.ko` under [ldls.md](ldls.md). Measuring
|
||||
it is what changed the answer: the module has **183 undefined symbols, 130 of
|
||||
them not implemented**, and 35 of those 130 are **phylib** —
|
||||
`phy_connect_direct`, `phy_start`, `mdiobus_read`, `genphy_soft_reset`.
|
||||
The first plan was to run Linux's `r8169.ko` under [ldls.md](ldls.md), and
|
||||
measuring it changed that: the module has **183 undefined symbols, 130 of them not
|
||||
implemented**, and 35 of those 130 are **phylib** — `phy_connect_direct`,
|
||||
`phy_start`, `mdiobus_read`, `genphy_soft_reset`. phylib is a subsystem with a link
|
||||
state machine, not a set of leaf calls, and LDLS's usual answer for an
|
||||
unimplemented symbol — a stub that reports itself and returns zero — is precisely
|
||||
wrong for it: `phy_start` returning zero means the link never comes up and nothing
|
||||
says why. So a **native driver** came first: it reaches the PHY through the
|
||||
chip's own MII window (`PHYAR`, one register that is a whole MII bus) and skips
|
||||
the subsystem, in the shape of the e1000 beside it.
|
||||
|
||||
phylib is a subsystem with a link state machine, not a set of leaf calls, and
|
||||
LDLS's usual answer for an unimplemented symbol — a stub that reports itself
|
||||
and returns zero — is precisely wrong for it. `phy_start` returning zero means
|
||||
the link never comes up and there is nothing on screen to say why. The 8139too
|
||||
that ldls.md uses as its model has no phylib dependency, which is why that one
|
||||
was tractable.
|
||||
It has since been done the other way as well. LDLS grew a driver core, devres,
|
||||
firmware loading and phylib's tables ([ldls.md](ldls.md), *A set of modules*),
|
||||
and the ASUS TUF A15's RTL8168H is now driven by **Linux's r8169, unmodified, in
|
||||
a process**: `make ldls-r8169` stages it with libphy, mdio-bus and the PHY driver,
|
||||
and `etc/schinit/ldls-r8169.job` starts it at boot. The native driver is **out of
|
||||
the registry** — it would claim the device first — and stays in the tree, with
|
||||
Linux's 8168H setup, as a fallback.
|
||||
|
||||
A native driver reaches the PHY through the chip's own MII window (`PHYAR`,
|
||||
one register that is a whole MII bus) and skips the subsystem entirely. It is
|
||||
less work than the port, it carries no GPL entanglement, it needs no firmware
|
||||
blob, and it is not coupled to one Linux version.
|
||||
|
||||
The register layout is a *fact about the hardware*, taken the way
|
||||
[ldls.md](ldls.md) §3 says facts may be taken. The expression is this tree's:
|
||||
a descriptor-ring driver in the same shape as the e1000 beside it.
|
||||
On that laptop the interface is `ldls0`, and it appears *after* boot, which is why
|
||||
`net_attach_late()` starts the stack when the first interface arrives instead of
|
||||
turning it away.
|
||||
|
||||
### The Chromebox has one of these
|
||||
|
||||
The Asus Chromebox CN62 "Guado" turns out to carry an RTL8111 rather than the
|
||||
Intel part its spec sheet suggests, so this driver covers it as well as the
|
||||
laptop that prompted it — which is the argument for the family match in one
|
||||
line rather than a device id per board.
|
||||
The Asus Chromebox CN62 "Guado" carries an RTL8111 rather than the Intel part its
|
||||
spec sheet suggests, so the family match (`10EC:8168`) covers it as well as the
|
||||
laptop — which is the argument for matching a family in one line rather than a
|
||||
device id per board.
|
||||
|
||||
### It has not run on hardware
|
||||
### It has not been shown to work on hardware
|
||||
|
||||
**Stated plainly because it cannot be tested here.** QEMU emulates `rtl8139`
|
||||
and nothing else from Realtek, and the 8139 is a different programming model —
|
||||
a circular receive buffer rather than descriptors — so it is deliberately *not*
|
||||
in the match table. Claiming it would break a NIC that works.
|
||||
**Stated plainly because it cannot be tested here.** QEMU emulates `rtl8139` and
|
||||
nothing else from Realtek, and the 8139 is a different programming model — a
|
||||
circular receive buffer rather than descriptors — so it is deliberately *not* in
|
||||
the match table. Claiming it would break a NIC that works.
|
||||
|
||||
What has been verified: the driver builds, its presence changes nothing on a
|
||||
machine without the device, and with an `rtl8139` on the bus it correctly
|
||||
leaves it unclaimed (`10ec:8139 network controller [-]`).
|
||||
What has been verified: both drivers build, their presence changes nothing on a
|
||||
machine without the device, and Linux's r8169 under LDLS loads, links and
|
||||
initialises in QEMU, registering and finding no device. On the laptop it reached
|
||||
probe and read the PHY (after a bug in LDLS that made every MDIO read return
|
||||
`0x02460246` was fixed); the native 8168H bring-up — the PLL, the receive gate, the
|
||||
PHY through the OCP window — has not run on a part.
|
||||
|
||||
What has not: every register write, on a real part.
|
||||
What has not: traffic on real hardware, from either.
|
||||
|
||||
So the probe says everything a first bug report would need, with `cprintf`
|
||||
The native probe says everything a first bug report would need, with `cprintf`
|
||||
rather than `kinfo` so it reaches the screen on a machine with no serial port:
|
||||
|
||||
```
|
||||
|
|
@ -141,6 +150,9 @@ register 1, where bit 2 is link and bit 5 is autonegotiation complete. Those
|
|||
four facts separate "the chip is not there", "the chip is there and the PHY
|
||||
sees nothing", and "everything is up and the rings are wrong".
|
||||
|
||||
The kernel turns **ASPM off** on Realtek NIC links at boot, as Linux's driver
|
||||
does through `pci_disable_link_state` — which a process under LDLS cannot do.
|
||||
|
||||
### Two constants that were wrong first
|
||||
|
||||
Recorded because both would have worked, quietly:
|
||||
|
|
@ -262,6 +274,12 @@ to. Measured across three pings: `rx_frames` and `wake_rx` both went 2 → 6.
|
|||
table, and it returns immediately: everything happens in that thread, which
|
||||
cannot run until the scheduler is entered.
|
||||
|
||||
**The thread follows each device's link into lwIP.** The link used to be read once
|
||||
at attach, 0.02 s into boot; real autonegotiation takes seconds, so on hardware
|
||||
the interface stayed down for good. It also starts the stack when an interface
|
||||
appears after boot (a driver in a process, a USB NIC), and attaches every device
|
||||
there is as its first act; `net_eth_attach()` refuses one it already has.
|
||||
|
||||
DHCP obtains an address at boot. `/proc/netif` reports each interface's address,
|
||||
netmask, gateway, MAC and frame counts, taken at the lwIP seam.
|
||||
|
||||
|
|
@ -326,19 +344,20 @@ The creation lives in `sys_bind()` rather than here, because that is the layer
|
|||
with both: `kernel/net/unix.c` knows about sockets and has no business calling
|
||||
`create()`.
|
||||
|
||||
**`flock()` is still missing.** libwayland locks `wayland-0.lock` beside the
|
||||
socket so that two compositors cannot claim one display. The lock file can be
|
||||
created and opened now that paths are real; the advisory lock itself is a
|
||||
separate mechanism with no other caller yet.
|
||||
`flock()` and `fcntl` record locks exist (`kernel/fs/lock.c`), so the lock files
|
||||
next to a socket work the way they do on Linux. Names in
|
||||
the **abstract namespace** (a `sun_path` starting with a NUL) name no file; they
|
||||
work, and `getsockname` reports them unnamed. `SO_PASSCRED` is honoured, and
|
||||
`SO_PEERCRED` answers (tmux asks).
|
||||
|
||||
### Descriptor passing
|
||||
|
||||
`sendmsg`/`recvmsg` with `SCM_RIGHTS`, which is the one thing a socket can carry
|
||||
that a pipe cannot: a reference to an object the kernel already knows, rather
|
||||
than a copy of its contents. Wayland's `wl_shm_create_pool` passes a descriptor
|
||||
and has no integer-shaped alternative, and
|
||||
[hybrid.md](../plan/hybrid.md)'s mechanism 1 is how a driver process is handed
|
||||
the device it is allowed to touch. Both wanted the same primitive.
|
||||
and has no integer-shaped alternative, and the same mechanism is how a driver
|
||||
process is handed the device it is allowed to touch ([devices.md](devices.md)).
|
||||
Both wanted the same primitive.
|
||||
|
||||
**A batch of descriptors is pinned to the stream position of the first byte of
|
||||
the message that carried it.** That single rule is the whole delivery model, and
|
||||
|
|
@ -374,9 +393,9 @@ descriptors to a position that never arrives.
|
|||
**There is no garbage collector.** An AF_UNIX socket descriptor, sent over that
|
||||
same socket and never received, holds a reference to the socket that holds the
|
||||
reference to it, and neither close can reach zero. Linux carries several hundred
|
||||
lines to break these cycles. Nothing on either plan passes a socket over a
|
||||
socket — Wayland passes memory, the driver work passes devices — so this is a
|
||||
known and bounded leak (thirty-two sockets) rather than a silent one.
|
||||
lines to break these cycles. Nothing passes a socket over a
|
||||
socket — display servers pass memory, the driver work passes devices — so this is
|
||||
a known and bounded leak (thirty-two sockets) rather than a silent one.
|
||||
|
||||
### AF_INET
|
||||
|
||||
|
|
@ -593,59 +612,47 @@ information was there and the code did not ask.
|
|||
|
||||
## What it does not do
|
||||
|
||||
- **`ping` is a first-party program**, `cmd/ping`, not toybox's. `recvmsg` now
|
||||
exists, so half of what stopped toybox's is gone; what remains is
|
||||
`IP_RECVTTL`, an `IPPROTO_IP` control message that would have to be generated
|
||||
on the receive path rather than merely carried. Ours has no `-I`, no flood
|
||||
mode and no TTL display.
|
||||
- **`SCM_RIGHTS` is the only control message.** `SCM_CREDENTIALS` is refused
|
||||
with `EINVAL` rather than ignored: a caller told its credentials were passed
|
||||
would have the receiver read a uid out of uninitialised memory and act on it.
|
||||
- **`ping` is a first-party program**, `cmd/ping`, not toybox's or busybox's. Ours
|
||||
has no `-I`, no flood mode and no TTL display (`IP_RECVTTL` is an `IPPROTO_IP`
|
||||
control message that would have to be generated on the receive path), and takes a
|
||||
dotted quad or `localhost` — it does not link musl's resolver. Busybox's `ping`,
|
||||
from the Alpine root, works over a `SOCK_RAW` ICMP socket.
|
||||
- **Only `SCM_RIGHTS` and `SCM_CREDENTIALS` travel in a control message.** Anything
|
||||
else is refused rather than ignored.
|
||||
- **A message may carry eight descriptors, and a socket eight such messages.**
|
||||
Past that `sendmsg` answers `ENOBUFS`. The cap exists because the kernel holds
|
||||
a reference to each one until it is received; a sender eight unreceived
|
||||
messages ahead of its receiver is not rescued by a ninth slot.
|
||||
- **`ping` still takes a dotted quad**, even though names now work. It is a
|
||||
first-party program that does not link musl's resolver, and `localhost` is
|
||||
special-cased in it rather than resolved. Every *ported* program resolves
|
||||
names properly; ours is the one that does not.
|
||||
- **Nothing re-runs `resolvconf`.** The nameserver is written once at boot. A
|
||||
lease that changes the DNS server, or a `/dev/pci` handover that brings up a
|
||||
new interface, leaves a stale `/etc/resolv.conf` until somebody runs it again.
|
||||
- **lwIP's own DNS client is still unused.** It is compiled in and only
|
||||
`dns_getserver()` is ever called; resolution happens entirely in the process.
|
||||
Nothing in the kernel can resolve a name, which matters the day something in
|
||||
the kernel wants to.
|
||||
- **`httpd` does not serve HTTP/1.1.** Every response is HTTP/1.0 with
|
||||
`Connection: close`, because the directory listing is generated straight into
|
||||
the socket and so has no `Content-Length` to give. One request per connection
|
||||
is the cost.
|
||||
- **Nothing re-runs `resolvconf`.** The nameserver is written once at boot
|
||||
(`etc/schinit/resolvconf.job`). A lease that changes the DNS server, or a
|
||||
`/dev/pci` handover that brings up a new interface, leaves a stale
|
||||
`/etc/resolv.conf` until somebody runs it again.
|
||||
- **lwIP's own DNS client is unused.** Resolution happens entirely in the process
|
||||
(musl); nothing in the kernel can resolve a name.
|
||||
- **`httpd` serves HTTP/1.0** with `Connection: close`, one request per connection,
|
||||
and has no timeouts: a client that connects and says nothing ties up a child
|
||||
process until it goes away, and `SO_RCVTIMEO` does not exist here.
|
||||
- **A queued-but-never-accepted connection is closed without clearing its
|
||||
callbacks.** `inet_close()` clears the argument and closes, which is enough
|
||||
because the pcb is freed — but it is a narrower guarantee than the one the
|
||||
main path makes, and it relies on `tcp_close()` succeeding.
|
||||
- **`httpd` has no timeouts.** A client that connects and says nothing ties up
|
||||
a child process until it goes away; `SO_RCVTIMEO` does not exist here (see
|
||||
below), so there is nothing to set.
|
||||
- **No routing table**, and no `/proc/net/route` to report one. With two
|
||||
interfaces up, the first is the default route and the second is reachable only
|
||||
by an address on its own subnet. That is a rule rather than an accident, and it
|
||||
is not routing. The missing *file* is the more immediately visible half: it is
|
||||
how a program asks which interface carries the default route, and without it
|
||||
anything that filters on "the default one" sees nothing.
|
||||
- **Netlink serves two message types.** No `RTM_GETROUTE`, no `RTM_NEWROUTE`, no
|
||||
event stream, no multicast groups. A program that expects to *subscribe* to
|
||||
link changes gets a socket that answers dumps and never speaks again.
|
||||
- **No `getsockname`/`getpeername`**, and `accept()` does not report the peer's
|
||||
address for an AF_INET socket. A server that wants to log who connected cannot.
|
||||
- **`setsockopt` covers six options**, and refuses the rest with `ENOPROTOOPT`
|
||||
rather than pretending: `SO_REUSEADDR`, `SO_KEEPALIVE`, `SO_BROADCAST` and
|
||||
`TCP_NODELAY` reach lwIP's pcb; `SO_SNDBUF`/`SO_RCVBUF` are accepted and
|
||||
report the real size on read. Notably absent: `SO_RCVTIMEO`/`SO_SNDTIMEO`.
|
||||
because the pcb is freed, but it is a narrower guarantee than the main path
|
||||
makes and relies on `tcp_close()` succeeding.
|
||||
- **No routing table**, and no `/proc/net/route`. With two interfaces up, the first
|
||||
is the default route and the second is reachable only by an address on its own
|
||||
subnet. That is a rule rather than an accident, and it is not routing.
|
||||
- **Netlink is a responder**: `RTM_GETLINK` and `RTM_GETADDR` dumps (what
|
||||
`getifaddrs` needs), `MSG_PEEK|MSG_TRUNC` sizing (GLib's network monitor measures
|
||||
each datagram that way and spun at 100% when it answered 0), and nothing else. No
|
||||
`RTM_GETROUTE`, no event stream, no multicast groups; a program that expects to
|
||||
*subscribe* to link changes gets a socket that answers dumps and never speaks
|
||||
again.
|
||||
- **`setsockopt` applies what it understands** (`SO_REUSEADDR`, `SO_KEEPALIVE`,
|
||||
`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.
|
||||
- **AF_UNIX names are not filesystem paths**, so nothing can `stat()` or
|
||||
`unlink()` a socket.
|
||||
- **One NIC driver exists.** The class is proven with two e1000 cards, not with
|
||||
two different drivers.
|
||||
- **No IPv6**, no DNS resolver in the kernel (lwIP's is compiled but unused by
|
||||
anything), and no firewall or packet filter of any kind.
|
||||
- **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
|
||||
proven with two e1000 cards and an LDLS handover, not on a wide range of
|
||||
hardware. The Realtek parts have never carried a packet on real silicon from this
|
||||
tree.
|
||||
|
|
|
|||
Loading…
Reference in a new issue