131 lines
7.1 KiB
Markdown
131 lines
7.1 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))
|
|
follows the vfs-protocol pattern — extern-struct messages, a version, reserved
|
|
fields. Three operations:
|
|
|
|
| Direction | Operation | Purpose |
|
|
|---|---|---|
|
|
| subscriber → service | `subscribe` | receive published events; the subscriber's endpoint rides as the call's **capability** (the input/device-manager pattern) |
|
|
| init → service | `shutdown` | orderly shutdown's last step: enter S5 (soft off) |
|
|
| service → subscriber | `event` | a published `EventMessage`, delivered as a buffered message (never sent *to* the service) |
|
|
|
|
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
|
|
message, so a slow or dead subscriber can never wedge the source. The event
|
|
vocabulary is hardware-neutral:
|
|
|
|
- `power_button` — the button was pressed (a fixed ACPI event on x86).
|
|
- `lid`, `ac`, `battery` — the named GPE-driven events.
|
|
- `notify` — 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.
|
|
|
|
An `EventMessage` carries the `event` tag plus `code` and an 8-byte `hid`, so a
|
|
generic `notify` is fully described without a second round trip.
|
|
|
|
**`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.
|