142 lines
8.0 KiB
Markdown
142 lines
8.0 KiB
Markdown
# The power service: events and shutdown
|
|
|
|
A laptop lid closes, a battery drains, someone presses the power button — and
|
|
several parts of the system might care: a session manager dims the screen, a
|
|
logger notes it, and ultimately *something* has to turn the machine off. None of
|
|
them owns the hardware that reported the event, and the reporter should not know
|
|
who is listening. So system power is a **service**: an event source **publishes**
|
|
button/lid/battery/AC events, interested processes **subscribe**, and one
|
|
privileged caller — init — can ask it to power the machine off. It is the same
|
|
publish/subscribe shape as the [input service](../device-driver-development/input.md), applied to power.
|
|
|
|
## Why a service, and why it is named for the domain, not the firmware
|
|
|
|
Where the events come from is firmware-specific — on x86 they ride the ACPI SCI
|
|
([acpi.md](acpi.md)); on a Raspberry Pi they would come from PSCI or a mailbox.
|
|
What subscribers want is not: *the lid closed* means the same thing regardless of
|
|
who noticed. So the surface is **domain-named**. There is a `power-protocol`
|
|
module and a contract named `/protocol/power`; on x86 the **acpi service**
|
|
binds it, and on ARM a PSCI/mailbox service will bind the *same* name.
|
|
Subscribers open `/protocol/power` and never learn which firmware they
|
|
are on — the neutrality the whole [discovery](discovery.md) migration exists to
|
|
preserve, carried one layer up into a running-system surface.
|
|
|
|
This is why the protocol is `power`, not "ACPI events": naming a cross-firmware
|
|
surface after one firmware would leak x86 into code the ARM port must reuse
|
|
unchanged.
|
|
|
|
## The protocol
|
|
|
|
The `power-protocol` module ([library/protocol/power/power-protocol.zig](../../library/protocol/power/power-protocol.zig))
|
|
is defined through the [envelope](protocol-namespace.md), so every packet begins
|
|
with the folded `Header`. `Header.target` is unused in both directions: the
|
|
provider is the only object either side addresses.
|
|
|
|
| Direction | Packet | Purpose |
|
|
|---|---|---|
|
|
| subscriber → service | `subscribe` (reserved verb 2) | receive published events; the subscriber's endpoint rides as the call's **capability** (the input/device-manager pattern) |
|
|
| init → service | `shutdown` (verb 16) | orderly shutdown's last step: enter S5 (soft off) |
|
|
| service → subscriber | one event per kind | a published `Notice`, `ipc_send`t as a buffered packet (never sent *to* the service) |
|
|
|
|
`subscribe` is not one of this protocol's own verbs: a synchronous call whose
|
|
attached capability is the subscriber's endpoint is exactly what the envelope's
|
|
reserved `subscribe` means everywhere, so power adopts it wholesale. And no
|
|
packet carries a version — the reserved `describe` verb is the version handshake,
|
|
asked once at connect time rather than out of every packet's budget.
|
|
|
|
Events are published, not polled: like the input service, the service holds
|
|
subscriber endpoints as capabilities and `ipc_send`s each event as a buffered
|
|
packet, so a slow or dead subscriber can never wedge the source. **The kind is
|
|
the packet's operation** — one declared event per named kind, exactly as the
|
|
input service delivers one per device class — so a subscriber reads *what
|
|
happened* out of the header rather than out of a tag inside the payload. The
|
|
vocabulary is hardware-neutral:
|
|
|
|
- `power_button` (event 16) — the button was pressed (a fixed ACPI event on x86).
|
|
- `lid` (17), `ac` (18), `battery` (19) — the named GPE-driven events.
|
|
- `notify` (20) — a device notification that maps to none of the above; its
|
|
`code` (the ACPI `Notify` argument) and the notifying device's `hid` say which
|
|
device and what happened.
|
|
|
|
The payload every one of them carries is a `Notice`: `code` plus an 8-byte `hid`,
|
|
so a generic `notify` is fully described without a second round trip, and the
|
|
four named kinds leave both fields zero because the verb already said it all.
|
|
|
|
**`shutdown` is authority, not information.** It is the only operation that
|
|
*does* something irreversible, so it is gated: the contract is that only init
|
|
(PID 1) may request it, because init is the process that has already run the stop
|
|
sequence over everything else. The acpi service implements this as a **soft
|
|
gate** — it honors `shutdown` only from a process that is a *subscriber*, and
|
|
init is the one subscriber. That stands in for "only the system supervisor may
|
|
power off" without hard-coding a pid, so it still holds under tests where PID 1
|
|
is not init.
|
|
|
|
## Orderly shutdown
|
|
|
|
Powering off cleanly is where the power service, the [process
|
|
lifecycle](process-lifecycle.md), and [ACPI events](acpi.md) compose. init
|
|
already supervises the services it starts; for shutdown it runs **one event loop
|
|
over one endpoint** that carries three things at once: its children's exit
|
|
notifications, the lifecycle **signals** it can receive (`terminate`), and the
|
|
**power events** it subscribes to — plus a re-arming heartbeat timer proving PID
|
|
1 is alive. (init subscribes with retries, because the power service registers
|
|
`.power` well after init starts; a missing power service is not fatal — a
|
|
`terminate` signal drives the same path.)
|
|
|
|
On a `power_button` event or a `terminate` signal, init:
|
|
|
|
1. logs that it is shutting down,
|
|
2. runs the standard stop sequence — `process.stop(child, deadline,
|
|
endpoint)` — over its children **in reverse spawn order**, so the VFS stops
|
|
last (other services may flush through it), each child getting the
|
|
*terminate → deadline → kill* escalation from
|
|
[process-lifecycle.md](process-lifecycle.md), and
|
|
3. requests `.power` `shutdown`.
|
|
|
|
The service then enters **S5** (soft off) by writing `SLP_TYP | SLP_EN` to the
|
|
PM1 control register(s) from ring 3, with the `SLP_TYP` values taken from its own
|
|
AML parse of the `_S5` object — the kernel has no S5 path of its own. If the
|
|
write returns instead of powering the machine off, it logs loudly so a test
|
|
fails rather than hangs.
|
|
|
|
**No new system call was needed for S5.** The broad io_port grant on the
|
|
`acpi-tables` node ([discovery.md](discovery.md)) already put the PM1 control
|
|
ports in the acpi service's hands, so writing S5 from ring 3 is something it
|
|
could physically already do; formalizing it as a protocol operation added a
|
|
contract, not authority. The kernel keeps only **reboot** (`acpi.reboot` in
|
|
`system/kernel/acpi.zig` — the FADT reset register plus the legacy fallbacks, which
|
|
need no AML); it has no poweroff path at all — S5 is not a kernel operation.
|
|
|
|
## Verifying it
|
|
|
|
Two QEMU scenarios exercise the path, both injecting a real ACPI power-button
|
|
press via QMP `system_powerdown` (there is no other deterministic power event on
|
|
this config):
|
|
|
|
- `power-button` proves the source: the acpi service's SCI handler logs the
|
|
press and publishes `power_button` (the ACPI half is in [acpi.md](acpi.md)).
|
|
- `orderly-shutdown` proves the whole composition: button → init logs shutting
|
|
down → children stopped → the service enters S5 → QEMU exits. The ordered
|
|
regex is the proof, and QEMU's self-exit through S5 is the pass.
|
|
|
|
## Scope
|
|
|
|
Interface-complete but validated on real hardware (the author's laptop) later,
|
|
because QEMU does not emulate them: battery `_BST`/`_BIF` evaluation beyond the
|
|
interface stubs, lid and AC events, and the embedded controller's `_Qxx`
|
|
queries. Deliberately out of scope for now: reboot over the power protocol, S3
|
|
sleep, per-device D-states (a future lifecycle-vocabulary extension, since
|
|
"suspend" has the shape of a signal every driver must answer and has no consumer
|
|
until laptop sleep), and thermal zones.
|
|
|
|
## See also
|
|
|
|
- [acpi.md](acpi.md) — where the events come from on x86: the SCI, the power
|
|
button fixed event, and GPE/Notify dispatch in the acpi service.
|
|
- [discovery.md](discovery.md) — why the surface is domain-named, and the
|
|
firmware neutrality that makes a PSCI backend drop-in on ARM.
|
|
- [process-lifecycle.md](process-lifecycle.md) — the stop sequence
|
|
(`terminate → deadline → kill`) and signals init composes into shutdown.
|
|
- [device-manager.md](../device-driver-development/device-manager.md) — the supervision model init mirrors for
|
|
its own children.
|