Add input module: broadcast keyboard events over IPC
Programs can now subscribe to keyboard events (key_down/key_up/key_press) and drivers can broadcast them, through a new user-space input service. The delivery model is forced by danos IPC: a synchronous rendezvous holds one pending reply, so a server cannot park N subscribers blocked in a "wait for next event" call — delivery must be push. But a synchronous push has no timeout and the kernel never wakes a sender parked on a dead peer's endpoint, so one dying subscriber would hang all input. So this lands the roadmap's planned asynchronous buffered send and builds the service on it: - ipc_send (syscall 26): non-blocking post to an endpoint's bounded payload ring, delivered through reply_wait as a buffered message (notify_message_bit). A full ring drops the oldest. It can never hang on a dead/slow peer. - input-protocol + runtime.input helpers (subscribe/next, connectSource/ publish) — the first real consumer of M13 capability passing: a subscriber hands the service its own endpoint as a capability. - input service (fan-out via ipc_send, dead-subscriber pruning), a synthetic input-source, and input-test; the ps2-bus keyboard driver publishes to it. Real IRQ1 scancode decoding (which must live in the bus, the PNP0303 owner) is a documented follow-up; the source is synthetic for now. - build/init wiring, an `input` QEMU case, and docs/input.md. Full QEMU suite 48/48, including the new input case and every IPC/endpoint regression (ipc, ipc-call, ipc-cap, vfs, hpet, bus, irqfree).
This commit is contained in:
+4
-1
@@ -52,7 +52,10 @@ rather than restate it. Roughly in the order things happen at runtime:
|
||||
microkernel's `ps`/`kill`/SIGCHLD: enumerate as a table snapshot, the
|
||||
supervision link as the kill authority, and child-exit notifications over the
|
||||
same endpoints IRQs arrive on.
|
||||
16. **[halting.md](halting.md) — halting.** Why a kernel can't just "exit", and
|
||||
16. **[input.md](input.md) — the input module.** Broadcasting keyboard events: why a
|
||||
synchronous rendezvous can't fan out to many listeners, the asynchronous `ipc_send`
|
||||
primitive built to fix it, and the subscribe/publish service layered on top.
|
||||
17. **[halting.md](halting.md) — halting.** Why a kernel can't just "exit", and
|
||||
how `while (true) hlt` parks the CPU safely once there's nothing left to do.
|
||||
|
||||
Start with the north star:
|
||||
|
||||
+115
@@ -0,0 +1,115 @@
|
||||
# The input module: broadcasting keyboard events
|
||||
|
||||
A keyboard driver has one keystroke and *many* programs that might want it — a shell, a
|
||||
window server, a logger. None of them owns the hardware, and the driver should not know
|
||||
who is listening. So between the driver and the listeners sits the **input service**
|
||||
(`system/services/input/`): drivers **publish** events to it, programs **subscribe**, and
|
||||
it fans each event out to every subscriber. It is an ordinary ring-3 process reached over
|
||||
IPC, like the [VFS server](../system/services/vfs/vfs.zig) — no kernel knows what a key is.
|
||||
|
||||
Three event kinds cross the wire ([protocol.zig](../system/services/input/protocol.zig)):
|
||||
`key_down` and `key_up` are the physical make/break; `key_press` is the higher-level
|
||||
"a character was produced", carrying the Unicode scalar. A `KeyEvent` also has a
|
||||
layout-independent `keycode` and a `modifiers` bitmask.
|
||||
|
||||
## Why this needed a new kernel primitive
|
||||
|
||||
The interesting part is delivery, and it runs straight into the shape of danos IPC.
|
||||
[ipc.md](ipc.md) describes a **synchronous rendezvous**: a server holds exactly one
|
||||
pending reply (`Task.ipc_client`) and *must* answer it on its next `replyWait`. Two
|
||||
consequences decide the whole design:
|
||||
|
||||
1. **You cannot block N subscribers waiting for "the next event".** A server can hold only
|
||||
one caller at a time, so the natural "subscriber calls `next_event()` and blocks" API
|
||||
is impossible for more than one subscriber. Delivery therefore has to be **push** — the
|
||||
service reaching out to subscribers — not pull.
|
||||
|
||||
2. **A synchronous push can hang the whole service.** If the service delivered with
|
||||
`ipc_call`, it would block until each subscriber replied. `ipc_call` has no timeout, and
|
||||
the kernel does **not** wake a caller parked on a *dead* peer's endpoint (it only fails a
|
||||
peer that was mid-reply — see [process.zig](../system/kernel/process.zig)
|
||||
`releaseTaskResourcesLocked`). One subscriber that exits mid-delivery would wedge input
|
||||
for everyone. That is the opposite of the resilience the microkernel is for.
|
||||
|
||||
The fix is the asynchronous send that [ipc.md](ipc.md) had already earmarked as future
|
||||
work ("asynchronous / buffered send … for notifications between servers"):
|
||||
|
||||
```
|
||||
ipc_send(handle, message_ptr, message_len) -> 0 / -errno
|
||||
```
|
||||
|
||||
`ipc_send` copies a small payload into the endpoint's **bounded queue** and wakes a
|
||||
receiver, then returns immediately — it never blocks and so can never hang on a dead or
|
||||
slow subscriber. The receiver picks it up through the same `replyWait` it already runs:
|
||||
the wake arrives as a **buffered message** — `notify_badge_bit | notify_message_bit` set in
|
||||
the badge (distinguishing it from a bare IRQ/child-exit notification), the sender's task id
|
||||
in the low bits, and the payload in the receive buffer, with no reply owed. The queue holds
|
||||
16 messages per endpoint; a full queue **drops the oldest**, because a buffered message is
|
||||
discrete data, not a coalescing "level" like an interrupt. See
|
||||
[ipc-synchronous.zig](../system/kernel/ipc-synchronous.zig) (`sendLocked`, `popPost`, and
|
||||
the `replyWait` receive loop).
|
||||
|
||||
This is the async counterpart of `ipc_call`, and the input service is its first consumer.
|
||||
|
||||
## How the pieces fit
|
||||
|
||||
```
|
||||
keyboard driver / input-source input service subscriber(s)
|
||||
-------------------------------- ------------- -------------
|
||||
connectSource(); loop: replyWait: subscribe():
|
||||
publish(event) ── ipc_call ──▶ publish → broadcast: createIpcEndpoint()
|
||||
for each sub: callCap(subscribe,
|
||||
ipc_send(sub_ep) ──────────▶ send_cap = ep)
|
||||
reply ok loop: next()
|
||||
subscribe → store sub_ep cap └─ replyWait(ep)
|
||||
(from the call's capability) → KeyEvent
|
||||
```
|
||||
|
||||
- A **subscriber** calls `input.subscribe()`
|
||||
([library/runtime/input.zig](../library/runtime/input.zig)): it creates its own endpoint
|
||||
and hands it to the service as a **capability** (M13 capability passing — the input
|
||||
service is that feature's first real user). Then it loops on `Subscriber.next()`, which
|
||||
is a `replyWait` on that endpoint returning each pushed `KeyEvent`.
|
||||
- A **source** (a keyboard driver) calls `input.connectSource()` and
|
||||
`Publisher.publish(event)`. Publishing is a short synchronous `ipc_call` the service
|
||||
answers at once; the service's own fan-out is asynchronous, so publishing never blocks on
|
||||
a slow subscriber.
|
||||
- The **service** ([input.zig](../system/services/input/input.zig)) keeps a small
|
||||
subscriber table (endpoint handle + owning task id). On `publish` it `ipc_send`s the event
|
||||
to every subscriber. On `subscribe` it stores the passed capability and, as housekeeping,
|
||||
prunes any slot whose owning process has exited (checked against `process_enumerate`) —
|
||||
not for correctness (an async send to an orphaned endpoint is harmless) but to reclaim
|
||||
the slot.
|
||||
|
||||
Publisher and subscriber must be **separate processes**: a single thread that both
|
||||
published and serviced its own subscription would deadlock (its `publish` call blocks until
|
||||
the service delivers to its endpoint, which only the same thread could receive).
|
||||
|
||||
## Status and follow-ups
|
||||
|
||||
- **Synthetic source, for now.** The `ps2-bus` driver owns PNP0303, which carries *both*
|
||||
the 0x60/0x64 ports and IRQ1, so reading real scancodes has to live in the bus, not in
|
||||
[keyboard.zig](../system/drivers/ps2-bus/keyboard.zig). Until that lands, the keyboard
|
||||
driver (and the hardware-free `input-source` used by the test) publish a synthetic rolling
|
||||
`A..E` stream via `input.syntheticEvent`. The fan-out path is real; only the bytes are
|
||||
placeholder. **Follow-up:** the bus binds IRQ1, reads port 0x60, and `ps2-library`
|
||||
translates scan-set-1 → keycodes; the keyboard driver publishes decoded events.
|
||||
- **Drop-oldest under overflow** is a defined loss; the 16-slot ring absorbs normal bursts.
|
||||
Real backpressure/flow-control is future work.
|
||||
- **`publish` is unauthenticated** — any process may publish, consistent with the current
|
||||
bring-up trust model (see [driver-model.md](driver-model.md)). A source capability is
|
||||
future work.
|
||||
|
||||
## Verifying it
|
||||
|
||||
The `input` case (`python3 test/qemu_test.py input`, in
|
||||
[tests.zig](../system/kernel/tests.zig) `inputTest`) boots the real kernel and spawns the
|
||||
service, the synthetic source, and a subscriber. It passes only when the subscriber
|
||||
heartbeats `input-test: ok` — proof that an event travelled source → service → subscriber
|
||||
over IPC, exercising both `ipc_send` and capability-passing subscription.
|
||||
|
||||
## See also
|
||||
|
||||
- [ipc.md](ipc.md) — the synchronous rendezvous and the notification path `ipc_send` extends.
|
||||
- [syscall.md](syscall.md) — the system-call surface, including `ipc_send`.
|
||||
- [driver-model.md](driver-model.md) — class drivers, capability passing (M13), the trust model.
|
||||
+6
-1
@@ -95,6 +95,11 @@ This is what makes a user-space driver possible at all, and it's the subject of
|
||||
every capability is either well-known (the registry) or inherited — there's no way
|
||||
to delegate one.
|
||||
- **Asynchronous / buffered send** for the cases where a rendezvous is the wrong
|
||||
shape (logging, notifications between servers).
|
||||
shape (logging, notifications between servers). *Landed as `ipc_send`* — a
|
||||
non-blocking post to an endpoint's bounded payload queue, delivered through
|
||||
`reply_wait` as a buffered message (badge bit `notify_message_bit`). Built for, and
|
||||
first used by, the [input service](input.md)'s keyboard-event broadcast, where a
|
||||
synchronous push would let one dead subscriber hang the fan-out. A full queue drops
|
||||
the oldest (discrete messages, not a coalescing level like the notification ring).
|
||||
- **A bounded reply.** `MSG_MAX` is 256 bytes and the copy runs under the big kernel
|
||||
lock; a bulk transfer wants shared pages, not a copy.
|
||||
|
||||
@@ -47,6 +47,8 @@ Everything else---including`read()`,`write()`,`malloc()`, and`fork()`---will run
|
||||
- **What it does:**Used strictly by your background user-space servers (like your disk driver or filesystem). It sends a reply to the last client that called it, and immediately puts the server to sleep until the next request arrives.[[1](https://news.ycombinator.com/item?id=33078441)]
|
||||
3. **`Yield()`/`Thread_Ctrl()`**
|
||||
- **What it does:**Allows a thread to voluntarily give up its CPU time slice, or allows a root task to spawn/kill threads.
|
||||
4. **`ipc_send(endpoint, message_buffer)`(Asynchronous Send)**
|
||||
- **What it does:**Posts a small payload to an endpoint's bounded queue and returns *without* blocking — no rendezvous, no reply. The receiver picks it up through the same `IPC_ReplyWait`, as a buffered message. It is the async counterpart of `IPC_Call`, for one-to-many broadcasts where a synchronous rendezvous would let one dead or slow receiver hang the sender. The [input service](input.md) — keyboard-event fan-out — is its first user. A full queue drops the oldest message (a buffered message is discrete data, unlike a coalescing interrupt notification).
|
||||
|
||||
* * * * *
|
||||
|
||||
|
||||
Reference in New Issue
Block a user