Merge feat/process-lifecycle: the process lifecycle (M17.1-M17.4)
Claim release on death, exit reasons, published exit events with the VFS as first subscriber, signals over IPC with one-shot timers and the service harness — docs/process-lifecycle.md increments 1-4, all built.
This commit is contained in:
commit
be83a42d42
19
docs/ipc.md
19
docs/ipc.md
|
|
@ -103,3 +103,22 @@ This is what makes a user-space driver possible at all, and it's the subject of
|
|||
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.
|
||||
|
||||
## Lifecycle conventions over IPC (M17)
|
||||
|
||||
Three conventions from [process-lifecycle.md](process-lifecycle.md) ride the
|
||||
notification mechanism:
|
||||
|
||||
- **Signals** arrive as notifications on the endpoint a process nominated with
|
||||
`signal_bind` (`runtime.process.bindSignals`): badge = the signal bit plus the
|
||||
coalesced pending mask (`runtime.process.signalsFrom` decodes). Statements,
|
||||
never questions; no payload, no reply.
|
||||
- **One-shot timers** (`timer_bind`, `runtime.system.timerOnce`) land as a
|
||||
timer-bit notification — the timed wait: a service arms a deadline and keeps
|
||||
serving, instead of blocking in sleep.
|
||||
- **The universal ping**: a **zero-length request is the liveness probe**,
|
||||
answered with a zero-length reply by the service harness itself
|
||||
(`runtime.service.run`). No protocol's requests start at length zero, so the
|
||||
encoding cannot collide, and a wedged service simply fails to answer — which
|
||||
is the diagnosis. Deep health ("can I reach my hardware?") stays a per-service
|
||||
protocol message.
|
||||
|
|
|
|||
|
|
@ -29,10 +29,21 @@ only when its definition of green holds.
|
|||
- [x] **Phase 0** — baseline: docs committed, feat/usb merged to main, pushed;
|
||||
`usb-xhci-libary.zig` renamed to `usb-xhci-library.zig`; existing QEMU
|
||||
suite green from the worktree (48/48, 2026-07-12).
|
||||
- [ ] **M17.1** — kernel releases claims/MSI on death (branch `feat/process-lifecycle`)
|
||||
- [ ] **M17.2** — exit reasons
|
||||
- [ ] **M17.3** — published exit events + VFS subscriber
|
||||
- [ ] **M17.4** — signals, timer notifications, `runtime.process`, the service harness
|
||||
- [x] **M17.1** — kernel releases claims/MSI on death (claims: `releaseAllOwnedBy`
|
||||
in the reap; MSI was already swept by `irq.releaseOwner`; `claim-release`
|
||||
test; suite 49/49)
|
||||
- [x] **M17.2** — exit reasons (`ExitReason` recorded at exit/fault/kill before
|
||||
the notification; `process_exit_reason` supervisor-gated;
|
||||
`runtime.process.exitReason`; kernel + ring-3 assertions; suite 49/49)
|
||||
- [x] **M17.3** — published exit events + VFS subscriber (`process_subscribe`,
|
||||
bounded ref-counted table, publish on every death;
|
||||
`runtime.process.subscribeExits`; VFS handles carry owners and are swept on
|
||||
the owner's death; `vfs-client-death` test; suite 50/50)
|
||||
- [x] **M17.4** — signals, timer notifications, `runtime.process`, the service
|
||||
harness (signal_bind/process_signal + coalescing pending mask; timer_bind
|
||||
on the tick; bindSignals/signalsFrom/sendSignal/stop + timerOnce;
|
||||
runtime.service.run with the zero-length ping; VFS converted; `signals`
|
||||
scenario; suite 51/51)
|
||||
- [ ] **merge** `feat/process-lifecycle` → main, push
|
||||
- [ ] **M18.1** — device-manager protocol: hello + restart policy (branch `feat/device-manager`)
|
||||
- [ ] **merge** `feat/device-manager` → main, push
|
||||
|
|
|
|||
|
|
@ -1,6 +1,9 @@
|
|||
# Process lifecycle: signals over IPC
|
||||
|
||||
**Status: design.** The primitives underneath are built ([process-management.md](process-management.md):
|
||||
**Status: increments 1–4 built** (2026-07-12): claim release on death, exit
|
||||
reasons, published exit events, and signals + one-shot timers + the service
|
||||
harness are all in — the interface below is as-built. The primitives underneath
|
||||
predate this design ([process-management.md](process-management.md):
|
||||
spawn, the supervision link, kill, child-exit notifications); this document designs
|
||||
the layer above them — the standard vocabulary a danos process speaks about its own
|
||||
life, and the stable `runtime.process` interface that carries it. Nothing here is
|
||||
|
|
@ -245,13 +248,16 @@ pub fn stop(id: u32, deadline_ms: u64) void { ... }
|
|||
/// process_enumerate.
|
||||
pub fn subscribeExits(endpoint: usize) bool { ... }
|
||||
|
||||
/// How a process ended — from the exit notification. What restart policy reads.
|
||||
pub const ExitReason = enum {
|
||||
/// How a process ended — queried after the exit notification (the kernel records
|
||||
/// it first, so the two never race). What restart policy reads. (Built in M17.2.)
|
||||
pub const ExitReason = enum(u8) {
|
||||
exited, // returned from main / clean exit
|
||||
aborted, // abort() — deliberate self-termination (SIGABRT's ghost)
|
||||
aborted, // abort() — deliberate self-termination (SIGABRT's ghost; reserved)
|
||||
segmentation_fault, // SIGSEGV's ghost
|
||||
illegal_instruction, // SIGILL's ghost
|
||||
arithmetic_fault, // SIGFPE's ghost
|
||||
protection_fault, // general protection fault
|
||||
fault, // any other CPU exception
|
||||
killed, // process_kill
|
||||
};
|
||||
```
|
||||
|
|
|
|||
|
|
@ -95,11 +95,18 @@ the architecture layer calls up into `tick`.
|
|||
|
||||
## Known gaps (bring-up honesty)
|
||||
|
||||
- Device **claims** are not released on death (pre-existing: the fault path has
|
||||
the same gap) — a killed driver's device stays claimed until reboot.
|
||||
- ~~Device claims are not released on death~~ Closed (M17.1): every path out of a
|
||||
process releases its device claims alongside its IRQ and MSI bindings
|
||||
(`releaseTaskResourcesLocked`), so a restarted driver can claim its hardware
|
||||
again — the cleanup half of [process-lifecycle.md](process-lifecycle.md)'s iron
|
||||
rule 1. The `claim-release` test proves the kill → release → re-claim cycle.
|
||||
- Kernel stacks of dead tasks are leaked, as on every exit path (no reaper yet).
|
||||
- There is no exit *status* in the notification, only the id; a supervisor that
|
||||
needs the code can grow a wait-style call later.
|
||||
- ~~There is no exit status in the notification~~ Closed (M17.2): the kernel
|
||||
records how every process ends — exited, a fault class, or killed — before it
|
||||
posts the exit notification, and the supervisor reads it with
|
||||
`process_exit_reason` (`runtime.process.exitReason`). This is the input to
|
||||
restart policy ([process-lifecycle.md](process-lifecycle.md)); an exit *code*
|
||||
for the clean case can still ride alongside later.
|
||||
- Enumerate writes through the caller's raw pointer under the bring-up trust
|
||||
model, like `device_enumerate` (an unmapped page is a self-DoS, not an
|
||||
isolation break).
|
||||
|
|
@ -109,4 +116,5 @@ the architecture layer calls up into `tick`.
|
|||
`process-list` (enumerate), `process-kill` (kernel-level kill paths, refusals,
|
||||
notifications), `supervision` (the whole user-side surface via the process-test
|
||||
service: spawn supervised → enumerate → kill blocked and spinning children →
|
||||
notifications → gone). See test/qemu_test.py.
|
||||
notifications → gone), `claim-release` (a killed claim-holder's device is
|
||||
claimable again). See test/qemu_test.py.
|
||||
|
|
|
|||
|
|
@ -94,6 +94,15 @@ pub fn send(h: Handle, message: []const u8) bool {
|
|||
/// GSI. See `isNotification`.
|
||||
pub const notify_badge_bit: u64 = abi.notify_badge_bit;
|
||||
|
||||
/// Set alongside `notify_badge_bit` when the notification is a **signal** — the
|
||||
/// lifecycle vocabulary of docs/process-lifecycle.md, delivered to the endpoint
|
||||
/// nominated with `process.bindSignals`. Decode with `process.signalsFrom`.
|
||||
pub const notify_signal_bit: u64 = abi.notify_signal_bit;
|
||||
|
||||
/// Set alongside `notify_badge_bit` when the notification is a **one-shot timer**
|
||||
/// landing (`system.timerOnce`).
|
||||
pub const notify_timer_bit: u64 = abi.notify_timer_bit;
|
||||
|
||||
/// Set alongside `notify_badge_bit` when the notification is a **child-exit
|
||||
/// notice** — a process this one spawned (with an exit endpoint) has ended —
|
||||
/// rather than a device interrupt. The low bits carry the child's process id.
|
||||
|
|
@ -133,6 +142,17 @@ pub const Received = struct {
|
|||
}
|
||||
|
||||
/// The task id of whoever posted a buffered message, meaningful only when
|
||||
/// Whether this arrival is a signal notification — decode the set with
|
||||
/// `process.signalsFrom(badge)`.
|
||||
pub fn isSignal(self: Received) bool {
|
||||
return self.isNotification() and self.badge & notify_signal_bit != 0;
|
||||
}
|
||||
|
||||
/// Whether this arrival is a one-shot timer landing (`system.timerOnce`).
|
||||
pub fn isTimer(self: Received) bool {
|
||||
return self.isNotification() and self.badge & notify_timer_bit != 0;
|
||||
}
|
||||
|
||||
/// `isMessage`. (The badge's low bits, with the three high marker bits masked off.)
|
||||
pub fn senderTaskId(self: Received) u32 {
|
||||
return @intCast(self.badge & ~(notify_badge_bit | notify_exit_bit | notify_message_bit));
|
||||
|
|
|
|||
|
|
@ -1,8 +1,15 @@
|
|||
//! Process-level runtime types: what a user program receives at entry. Mirrors
|
||||
//! Process-level runtime types: what a user program receives at entry (`Init`,
|
||||
//! the argv contract) and the process end of the lifecycle
|
||||
//! (docs/process-lifecycle.md) — today the exit reason a supervisor reads to
|
||||
//! decide restart; signals and the stop sequence land here with M17.4. Mirrors
|
||||
//! the spirit of `std.process.Init.Minimal` in danos terms — std's `Args` holds
|
||||
//! no data on freestanding targets, so the type is danos's own.
|
||||
|
||||
const std = @import("std");
|
||||
const abi = @import("abi");
|
||||
const sc = @import("system-call.zig");
|
||||
const ipc = @import("ipc.zig");
|
||||
const system = @import("system.zig");
|
||||
|
||||
/// Everything a program receives at entry. Passed to
|
||||
/// `pub fn main(init: runtime.process.Init)`; programs that need nothing keep
|
||||
|
|
@ -45,3 +52,86 @@ pub const Arguments = struct {
|
|||
}
|
||||
};
|
||||
};
|
||||
|
||||
/// How a process ended — what a supervisor's restart policy reads: a clean exit
|
||||
/// meant to stop, a fault wants a restart with backoff, killed means the
|
||||
/// supervisor did it itself (docs/process-lifecycle.md).
|
||||
pub const ExitReason = abi.ExitReason;
|
||||
|
||||
/// How dead child `id` ended. Ask after the exit notification arrives — the
|
||||
/// kernel records the reason before it posts the notification, so this never
|
||||
/// races it. Returns null for an id that never lived, is still alive, was
|
||||
/// evicted from the kernel's bounded record, or is not this process's child
|
||||
/// (the same authority gate as `kill`).
|
||||
pub fn exitReason(id: u32) ?ExitReason {
|
||||
const r = sc.systemCall1(.process_exit_reason, id);
|
||||
if (r > ~@as(usize, 0) - 4095) return null; // a wrapped -errno
|
||||
return @enumFromInt(r);
|
||||
}
|
||||
|
||||
/// The signal vocabulary (docs/process-lifecycle.md): POSIX's concepts, danos's
|
||||
/// names, message delivery. A signal is a one-way coalescing statement — never a
|
||||
/// question (liveness is the zero-length ping call) and never kill (that is
|
||||
/// `system.kill`, unhandleable by definition).
|
||||
pub const Signal = abi.Signal;
|
||||
|
||||
/// The coalesced set of signals one notification delivered: two pending
|
||||
/// terminates arrive as one. Decode a received badge with `signalsFrom`.
|
||||
pub const SignalSet = struct {
|
||||
pending: u32,
|
||||
|
||||
pub fn has(set: SignalSet, signal: Signal) bool {
|
||||
return set.pending & (@as(u32, 1) << @intFromEnum(signal)) != 0;
|
||||
}
|
||||
};
|
||||
|
||||
/// Nominate `endpoint` as this process's signal endpoint. Signals posted while
|
||||
/// unbound have pended; they are delivered immediately on bind, coalesced.
|
||||
pub fn bindSignals(endpoint: usize) bool {
|
||||
return sc.systemCall1(.signal_bind, endpoint) == 0;
|
||||
}
|
||||
|
||||
/// Decode a received badge into the signals it delivered, or null if it is not
|
||||
/// a signal notification.
|
||||
pub fn signalsFrom(badge: u64) ?SignalSet {
|
||||
if (badge & abi.notify_badge_bit == 0 or badge & abi.notify_signal_bit == 0) return null;
|
||||
return .{ .pending = @truncate(badge & ~(abi.notify_badge_bit | abi.notify_signal_bit)) };
|
||||
}
|
||||
|
||||
/// Post `signal` to child `id` (or to yourself). Supervisor-gated, like kill;
|
||||
/// non-blocking, always — a statement, not a conversation.
|
||||
pub fn sendSignal(id: u32, signal: Signal) bool {
|
||||
return sc.systemCall2(.process_signal, id, @intFromEnum(signal)) == 0;
|
||||
}
|
||||
|
||||
/// The standard stop sequence (docs/process-lifecycle.md): terminate, wait up to
|
||||
/// `deadline_ms` for the exit notification on `exit_endpoint` (the endpoint the
|
||||
/// child was spawned with), then kill. Any *other* notifications arriving on
|
||||
/// that endpoint while stopping are consumed and dropped — a supervisor with
|
||||
/// concurrent traffic implements the same sequence inside its own event loop
|
||||
/// (arm `system.timerOnce`, keep serving) instead of calling this.
|
||||
pub fn stop(id: u32, deadline_ms: u64, exit_endpoint: usize) void {
|
||||
_ = sendSignal(id, .terminate);
|
||||
_ = system.timerOnce(exit_endpoint, deadline_ms);
|
||||
var receive: [8]u8 = undefined;
|
||||
while (true) {
|
||||
const got = ipc.replyWait(exit_endpoint, &.{}, &receive, null);
|
||||
if (got.isChildExit() and got.childProcessId() == id) return;
|
||||
if (got.isTimer()) break; // the deadline passed first — escalate
|
||||
}
|
||||
_ = system.kill(id);
|
||||
while (true) {
|
||||
const got = ipc.replyWait(exit_endpoint, &.{}, &receive, null);
|
||||
if (got.isChildExit() and got.childProcessId() == id) return;
|
||||
}
|
||||
}
|
||||
|
||||
/// Subscribe `endpoint` to published exit events: every process death posts an
|
||||
/// asynchronous notification with the same badge encoding as a supervisor's exit
|
||||
/// notice (decode with `ipc.Received.isChildExit`/`childProcessId`). For stateful
|
||||
/// services: release what the dead client held — file handles, subscriptions —
|
||||
/// because a service must never depend on clients cleaning up after themselves
|
||||
/// (docs/process-lifecycle.md). Ungated, like `system.processes`.
|
||||
pub fn subscribeExits(endpoint: usize) bool {
|
||||
return sc.systemCall1(.process_subscribe, endpoint) == 0;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -35,5 +35,9 @@ pub const panic = start.panic;
|
|||
/// Process entry types: the `Init` handed to `main`, and its `Arguments`.
|
||||
pub const process = @import("process.zig");
|
||||
|
||||
/// The service harness: one replyWait loop folding requests, signals, and
|
||||
/// notifications into callbacks (docs/process-lifecycle.md).
|
||||
pub const service = @import("service.zig");
|
||||
|
||||
/// The heap as a `std.mem.Allocator`, for Zig `std` containers in user code.
|
||||
pub const allocator = heap.allocator;
|
||||
|
|
|
|||
|
|
@ -0,0 +1,81 @@
|
|||
//! The service harness (docs/process-lifecycle.md): one replyWait loop that
|
||||
//! folds protocol requests, signals, and subscribed notifications into
|
||||
//! callbacks — so the lifecycle contract ("answers ping, exits on terminate")
|
||||
//! is satisfied by construction and a service author writes domain logic only.
|
||||
//! Nothing is asynchronous inside the process: a callback runs at a point the
|
||||
//! loop chose, never on a hijacked stack — the whole reason signals are
|
||||
//! messages.
|
||||
//!
|
||||
//! The liveness probe: a **zero-length request is the universal ping**, answered
|
||||
//! with a zero-length reply by the harness itself. No protocol's requests start
|
||||
//! at length zero, so the encoding cannot collide, and there is nothing for a
|
||||
//! service author to implement — a wedged service simply fails to answer, which
|
||||
//! is the diagnosis (see docs/ipc.md).
|
||||
|
||||
const abi = @import("abi");
|
||||
const ipc = @import("ipc.zig");
|
||||
const process = @import("process.zig");
|
||||
|
||||
pub const Callbacks = struct {
|
||||
/// Called once with the service's endpoint before the loop starts — the
|
||||
/// place to subscribe to exit events, bind IRQs, or announce readiness.
|
||||
/// Return false to abort startup (the process exits).
|
||||
init: ?*const fn (endpoint: ipc.Handle) bool = null,
|
||||
/// One protocol request from `sender` (a task id): write the reply into
|
||||
/// `reply`, return its length. The zero-length ping never reaches this.
|
||||
on_message: *const fn (message: []const u8, reply: []u8, sender: u32) usize,
|
||||
/// A notification that is not a signal — a subscribed exit event, a bound
|
||||
/// IRQ, a timer landing. The raw badge; decode with the ipc helpers.
|
||||
on_notification: ?*const fn (badge: u64) void = null,
|
||||
/// The reload signal. Default: ignored.
|
||||
on_reload: ?*const fn () void = null,
|
||||
/// The terminate signal, called before the loop returns. The clean exit is
|
||||
/// the return itself — never put *necessary* work here (iron rule 1: a kill
|
||||
/// arrives with no warning; this is for graceful extras only).
|
||||
on_terminate: ?*const fn () void = null,
|
||||
/// Publish the endpoint under a well-known service id at startup.
|
||||
service: ?abi.ServiceId = null,
|
||||
};
|
||||
|
||||
/// Run the service: create and (optionally) register the endpoint, bind signals
|
||||
/// to it, call `init`, then serve until `terminate` arrives — at which point the
|
||||
/// loop returns and main's return is the clean exit the supervisor reads as
|
||||
/// `ExitReason.exited`. `maximum_message` sizes the receive and reply buffers
|
||||
/// (a service passes its protocol's message maximum).
|
||||
pub fn run(comptime maximum_message: usize, callbacks: Callbacks) void {
|
||||
const endpoint = ipc.createIpcEndpoint() orelse return;
|
||||
if (callbacks.service) |id| {
|
||||
if (!ipc.register(id, endpoint)) return;
|
||||
}
|
||||
_ = process.bindSignals(endpoint);
|
||||
if (callbacks.init) |initialise| {
|
||||
if (!initialise(endpoint)) return;
|
||||
}
|
||||
|
||||
var reply_buffer: [maximum_message]u8 = undefined;
|
||||
var reply_len: usize = 0;
|
||||
var receive: [maximum_message]u8 = undefined;
|
||||
while (true) {
|
||||
const got = ipc.replyWait(endpoint, reply_buffer[0..reply_len], &receive, null);
|
||||
if (got.isNotification()) {
|
||||
reply_len = 0; // nothing owed for a notification
|
||||
if (process.signalsFrom(got.badge)) |signals| {
|
||||
if (signals.has(.reload)) {
|
||||
if (callbacks.on_reload) |onReload| onReload();
|
||||
}
|
||||
if (signals.has(.terminate)) {
|
||||
if (callbacks.on_terminate) |onTerminate| onTerminate();
|
||||
return; // the loop's return IS the clean exit
|
||||
}
|
||||
continue;
|
||||
}
|
||||
if (callbacks.on_notification) |onNotification| onNotification(got.badge);
|
||||
continue;
|
||||
}
|
||||
if (got.len == 0) {
|
||||
reply_len = 0; // the universal ping: a zero-length reply, from the harness
|
||||
continue;
|
||||
}
|
||||
reply_len = callbacks.on_message(receive[0..got.len], &reply_buffer, got.senderTaskId());
|
||||
}
|
||||
}
|
||||
|
|
@ -32,6 +32,15 @@ pub fn sleep(ms: usize) void {
|
|||
_ = sc.systemCall1(.sleep, ms);
|
||||
}
|
||||
|
||||
/// Arm a one-shot timer: after `ms` milliseconds the kernel posts a timer
|
||||
/// notification (`ipc.Received.isTimer`) to `endpoint`. The timed wait of
|
||||
/// docs/process-lifecycle.md — a service arms a deadline and keeps serving,
|
||||
/// instead of blocking in sleep; what stop-sequence escalation, hello deadlines,
|
||||
/// and restart backoff are built from.
|
||||
pub fn timerOnce(endpoint: usize, ms: u64) bool {
|
||||
return sc.systemCall2(.timer_bind, endpoint, ms) == 0;
|
||||
}
|
||||
|
||||
/// Monotonic nanoseconds since boot — a time source for timeouts and short delays. It
|
||||
/// only ever moves forward. This is *not* wall-clock time (no date, no timezone — that
|
||||
/// is a user-space service layered on top). Deadline pattern for a bounded poll loop:
|
||||
|
|
|
|||
|
|
@ -53,9 +53,31 @@ pub const SystemCall = enum(u64) {
|
|||
process_enumerate = 24, // process_enumerate(buffer, maximum) -> total: snapshot the task table
|
||||
process_kill = 25, // process_kill(id) -> 0/-errno: end a process this process spawned
|
||||
ipc_send = 26, // ipc_send(handle, message_ptr, message_len) -> 0/-errno: post a payload to an endpoint's async queue without blocking
|
||||
process_exit_reason = 27, // process_exit_reason(id) -> ExitReason/-errno: how a dead child ended (its supervisor only)
|
||||
process_subscribe = 28, // process_subscribe(endpoint) -> 0/-errno: subscribe to published exit events — every death posts a notification
|
||||
signal_bind = 29, // signal_bind(endpoint) -> 0/-errno: nominate the endpoint this process's signals arrive on
|
||||
process_signal = 30, // process_signal(id, signal) -> 0/-errno: post a signal to a child (or to yourself)
|
||||
timer_bind = 31, // timer_bind(endpoint, ms) -> 0/-errno: one-shot timer — posts a notification when ms elapse
|
||||
_,
|
||||
};
|
||||
|
||||
/// How a process ended — recorded by the kernel at death, queried by the
|
||||
/// supervisor with `process_exit_reason`, and the input to its restart decision
|
||||
/// (docs/process-lifecycle.md): a clean exit meant to stop, a fault wants a
|
||||
/// restart with backoff, killed means the supervisor did it itself. The faults
|
||||
/// mirror the CPU exceptions a ring-3 process can die of; they are exit reasons,
|
||||
/// never delivered to the faulting process (recovery is restart, not a handler).
|
||||
pub const ExitReason = enum(u8) {
|
||||
exited = 0, // returned from main / called exit
|
||||
aborted = 1, // deliberate self-termination (reserved: no abort path yet)
|
||||
segmentation_fault = 2, // page fault
|
||||
illegal_instruction = 3, // invalid opcode
|
||||
arithmetic_fault = 4, // divide error, x87 or SIMD fault
|
||||
protection_fault = 5, // general protection fault
|
||||
fault = 6, // any other CPU exception
|
||||
killed = 7, // process_kill
|
||||
};
|
||||
|
||||
/// The x86 MSI message address base (`0xFEE0_0000`): a device raises an MSI by writing
|
||||
/// `data` to this address, which the Local APIC turns into an interrupt at the vector
|
||||
/// in `data`. The kernel returns the concrete (address, data) from `msi_bind`; this is
|
||||
|
|
@ -93,6 +115,35 @@ pub const notify_exit_bit: u64 = 1 << 62;
|
|||
/// broadcasts where a rendezvous is the wrong shape (the input service is the first user).
|
||||
pub const notify_message_bit: u64 = 1 << 61;
|
||||
|
||||
/// Set (alongside `notify_badge_bit`) in the badge of a **signal notification** —
|
||||
/// the process-lifecycle vocabulary of docs/process-lifecycle.md, delivered to the
|
||||
/// endpoint the process nominated with `signal_bind`. The low bits carry the
|
||||
/// coalesced pending mask (bit positions = `Signal` values): signals are
|
||||
/// statements, not questions, and two pending terminates are one terminate.
|
||||
pub const notify_signal_bit: u64 = 1 << 60;
|
||||
|
||||
/// Set (alongside `notify_badge_bit`) in the badge of a **timer notification** —
|
||||
/// a one-shot `timer_bind` deadline landing. No payload bits: what to do when the
|
||||
/// deadline fires is whatever the receiver armed it for (a stop-sequence
|
||||
/// escalation, a restart backoff, an alarm).
|
||||
pub const notify_timer_bit: u64 = 1 << 59;
|
||||
|
||||
/// The signal vocabulary (docs/process-lifecycle.md): POSIX's concepts, danos's
|
||||
/// names, message delivery. The value is the bit position in the pending mask — a
|
||||
/// private kernel/runtime detail, free to change while they ship together. Kill
|
||||
/// is not here (it is `process_kill`, unhandleable by definition); faults are not
|
||||
/// here (they are `ExitReason`s — recovery is restart, not a handler); liveness is
|
||||
/// not here (a question, asked as the zero-length ping call, not a statement).
|
||||
pub const Signal = enum(u5) {
|
||||
terminate = 0, // finish up and exit (the polite half of the stop sequence)
|
||||
reload = 1, // re-read configuration / re-scan
|
||||
interrupt = 2, // interactive interrupt (no sender until a console exists)
|
||||
quit = 3, // as interrupt, by convention more final
|
||||
alarm = 4, // a timer the process armed for itself (unbuilt: no consumer yet)
|
||||
user_1 = 5, // service-defined
|
||||
user_2 = 6, // service-defined
|
||||
};
|
||||
|
||||
/// Capacity of `ProcessDescriptor.name` — matches the longest name `system_spawn`
|
||||
/// accepts, so a process's recorded name (its argv[0]) is never truncated.
|
||||
pub const maximum_process_name = 64;
|
||||
|
|
|
|||
|
|
@ -104,6 +104,19 @@ pub fn ownerOf(id: u64) ?u32 {
|
|||
return claimed[@intCast(id)];
|
||||
}
|
||||
|
||||
/// Release every claim held by `owner` — called by the process layer on every
|
||||
/// path out of a process (exit, fault, kill), so a restarted driver can claim its
|
||||
/// hardware again (docs/process-lifecycle.md iron rule 1: cleanup is the kernel's
|
||||
/// job). The devices stay in the table — they describe hardware, which did not go
|
||||
/// away — only their ownership clears.
|
||||
pub fn releaseAllOwnedBy(owner: u32) void {
|
||||
for (claimed[0..count]) |*slot| {
|
||||
if (slot.*) |o| {
|
||||
if (o == owner) slot.* = null;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Resource `index` of device `id`, or null if out of range.
|
||||
pub fn resourceOf(id: u64, index: u64) ?device_abi.ResourceDescriptor {
|
||||
if (id >= count) return null;
|
||||
|
|
|
|||
|
|
@ -412,13 +412,26 @@ fn recoverableFault(vector: u64) bool {
|
|||
/// plus a POST code and a persistent breadcrumb. (A ring-3 fault on a *borrowed*
|
||||
/// kernel thread — process.run, the user-pf isolation probe — also lands here: there
|
||||
/// is no scheduled process to kill.)
|
||||
/// Classify a CPU exception vector as the ExitReason a supervisor reads — the
|
||||
/// fault classes of docs/process-lifecycle.md. Faults are exit reasons, never
|
||||
/// signals delivered to the faulting process: recovery is restart, not a handler.
|
||||
fn exitReasonForVector(vector: u64) abi.ExitReason {
|
||||
return switch (vector) {
|
||||
14 => .segmentation_fault, // page fault
|
||||
6 => .illegal_instruction, // invalid opcode
|
||||
0, 16, 19 => .arithmetic_fault, // divide error, x87, SIMD
|
||||
13 => .protection_fault, // general protection
|
||||
else => .fault,
|
||||
};
|
||||
}
|
||||
|
||||
fn onException(state: *const architecture.CpuState) noreturn {
|
||||
if (architecture.fromUser(state) and scheduler.currentIsUserProcess() and recoverableFault(state.vector)) {
|
||||
statusPrint("\ndanos: process {d} ({s}) killed by {s} (vector {d}) on core {d}\n", .{ scheduler.currentId(), scheduler.current().name(), architecture.exceptionName(state.vector), state.vector, scheduler.currentCpuIndex() });
|
||||
statusPrint(" error code : 0x{x}\n", .{state.error_code});
|
||||
statusPrint(" IP : 0x{x:0>16}\n", .{architecture.instructionPointer(state)});
|
||||
if (architecture.faultAddress(state)) |address| statusPrint(" fault addr : 0x{x:0>16}\n", .{address});
|
||||
process.killCurrentProcess(); // reclaims everything, reschedules; never returns
|
||||
process.killCurrentProcess(exitReasonForVector(state.vector)); // reclaims everything, reschedules; never returns
|
||||
}
|
||||
|
||||
log.checkpoint(cp_exception);
|
||||
|
|
|
|||
|
|
@ -137,6 +137,7 @@ pub fn init() void {
|
|||
architecture.setSystemCallHandler(system_call);
|
||||
scheduler.terminate_current_hook = terminateCurrentLocked;
|
||||
scheduler.reap_task_hook = reapTaskLocked;
|
||||
scheduler.timer_tick_hook = timerSweepLocked;
|
||||
}
|
||||
|
||||
/// Return -1 (as an unsigned bit pattern) in the system_call result register.
|
||||
|
|
@ -164,6 +165,7 @@ fn system_call(state: *architecture.CpuState) void {
|
|||
// A scheduled process tears down fully (terminateCurrent); a borrowed
|
||||
// test thread unwinds back to the kernel that entered it.
|
||||
if (scheduler.currentIsUserProcess()) {
|
||||
scheduler.current().exit_reason = .exited;
|
||||
terminateCurrent();
|
||||
} else architecture.userExit();
|
||||
},
|
||||
|
|
@ -199,6 +201,11 @@ fn system_call(state: *architecture.CpuState) void {
|
|||
.clock => systemClock(state),
|
||||
.process_enumerate => systemProcessEnumerate(state),
|
||||
.process_kill => systemProcessKill(state),
|
||||
.process_exit_reason => systemProcessExitReason(state),
|
||||
.process_subscribe => systemProcessSubscribe(state),
|
||||
.signal_bind => systemSignalBind(state),
|
||||
.process_signal => systemProcessSignal(state),
|
||||
.timer_bind => systemTimerBind(state),
|
||||
_ => fail(state),
|
||||
}
|
||||
}
|
||||
|
|
@ -552,7 +559,11 @@ pub var fault_kill_count: u64 = 0;
|
|||
/// endpoint reference destroys the Endpoint, and a still-bound GSI would have an
|
||||
/// ISR call notifyFromIsr on freed memory the next time the device fired.
|
||||
/// `releaseOwner` also leaves the line masked, so a dead driver's device goes
|
||||
/// quiet rather than storming.
|
||||
/// quiet rather than storming. (It drops MSI vectors by the same owner sweep.)
|
||||
/// - Device claims are released with the IRQ bindings, so a restarted driver can
|
||||
/// claim the same hardware again — the cleanup half of process-lifecycle.md's
|
||||
/// iron rule 1. Claims hold no pointers, so ordering is free; they go here so
|
||||
/// the exit notification (below, last) observes a fully-released child.
|
||||
/// - A client this task still owes a reply to (it died between receive and reply)
|
||||
/// is failed with -EPEER rather than left blocked forever — a dead server must
|
||||
/// not hang its callers.
|
||||
|
|
@ -565,7 +576,33 @@ pub var fault_kill_count: u64 = 0;
|
|||
/// reference taken at spawn is dropped with it.
|
||||
/// Precondition: the big kernel lock is held.
|
||||
fn releaseTaskResourcesLocked(t: *scheduler.Task) void {
|
||||
recordExitLocked(t);
|
||||
irq.releaseOwner(t.id);
|
||||
devices_broker.releaseAllOwnedBy(t.id);
|
||||
// The dying task's signal endpoint and one-shot timers go with it.
|
||||
if (t.signal_endpoint) |raw| {
|
||||
ipc.dropRef(@ptrCast(@alignCast(raw)));
|
||||
t.signal_endpoint = null;
|
||||
}
|
||||
t.pending_signals = 0;
|
||||
for (&one_shot_timers) |*slot| {
|
||||
if (slot.*) |timer| {
|
||||
if (timer.owner == t.id) {
|
||||
ipc.dropRef(timer.endpoint);
|
||||
slot.* = null;
|
||||
}
|
||||
}
|
||||
}
|
||||
// A dead subscriber's own subscriptions go first: it must not hear about
|
||||
// itself, and the slots' endpoint references drop with it.
|
||||
for (&exit_subscribers) |*slot| {
|
||||
if (slot.*) |subscriber| {
|
||||
if (subscriber.owner == t.id) {
|
||||
ipc.dropRef(subscriber.endpoint);
|
||||
slot.* = null;
|
||||
}
|
||||
}
|
||||
}
|
||||
if (t.ipc_client) |client| {
|
||||
t.ipc_client = null;
|
||||
client.ipc_status = -ipc.EPEER;
|
||||
|
|
@ -575,6 +612,12 @@ fn releaseTaskResourcesLocked(t: *scheduler.Task) void {
|
|||
scheduler.removeFromWaitQueueLocked(t);
|
||||
scheduler.forgetIpcClientLocked(t);
|
||||
ipc.closeHandles(t);
|
||||
// Publish the exit to every subscriber (docs/process-lifecycle.md): the same
|
||||
// badge encoding as the supervisor's notification, and equally late, so a
|
||||
// subscriber also observes a fully-released child.
|
||||
for (&exit_subscribers) |*slot| {
|
||||
if (slot.*) |subscriber| ipc.notifyLocked(subscriber.endpoint, abi.notify_exit_bit | t.id);
|
||||
}
|
||||
if (t.exit_endpoint) |raw| {
|
||||
const endpoint: *ipc.Endpoint = @ptrCast(@alignCast(raw));
|
||||
t.exit_endpoint = null;
|
||||
|
|
@ -628,6 +671,7 @@ pub fn killProcess(caller_id: u32, target_id: u32) i64 {
|
|||
const target = scheduler.taskByIdLocked(target_id) orelse return -ipc.ESRCH;
|
||||
if (target.aspace == 0) return -ipc.ESRCH; // kernel tasks are not processes
|
||||
if (target.supervisor != caller_id) return -ipc.EPERM;
|
||||
target.exit_reason = .killed;
|
||||
if (target.state == .running) {
|
||||
target.kill_pending = true;
|
||||
} else {
|
||||
|
|
@ -640,12 +684,170 @@ pub fn killProcess(caller_id: u32, target_id: u32) i64 {
|
|||
/// The fault is confined to the process — the kernel trapped it on the task's own
|
||||
/// kernel stack and is intact — so everything the process held is reclaimed and the
|
||||
/// core reschedules. The system keeps running; only the faulting process dies
|
||||
/// (docs/resilience.md: fault -> kill -> continue).
|
||||
pub fn killCurrentProcess() noreturn {
|
||||
/// (docs/resilience.md: fault -> kill -> continue). `reason` is the fault class
|
||||
/// (from the vector), recorded for the supervisor's `process_exit_reason`.
|
||||
pub fn killCurrentProcess(reason: abi.ExitReason) noreturn {
|
||||
scheduler.current().exit_reason = reason;
|
||||
fault_kill_count += 1;
|
||||
terminateCurrent();
|
||||
}
|
||||
|
||||
/// The bounded record of recent deaths, for `process_exit_reason`: ids are never
|
||||
/// reused, so a ring keyed by id is enough — a record evicted by wraparound reads
|
||||
/// as -ESRCH, the same as an id that never lived, which a supervisor treats as
|
||||
/// "too late to ask". Written under the big kernel lock by the reap.
|
||||
const exit_record_capacity = 64;
|
||||
const ExitRecord = struct { id: u32 = 0, supervisor: u32 = 0, reason: abi.ExitReason = .exited, valid: bool = false };
|
||||
var exit_records: [exit_record_capacity]ExitRecord = .{ExitRecord{}} ** exit_record_capacity;
|
||||
var exit_record_next: usize = 0;
|
||||
|
||||
/// Record a dying task's (id, supervisor, reason) — called by the reap before the
|
||||
/// exit notification is posted, so a supervisor that hears the notification can
|
||||
/// always still query the reason. Precondition: the big kernel lock is held.
|
||||
fn recordExitLocked(t: *scheduler.Task) void {
|
||||
exit_records[exit_record_next] = .{ .id = t.id, .supervisor = t.supervisor, .reason = t.exit_reason, .valid = true };
|
||||
exit_record_next = (exit_record_next + 1) % exit_record_capacity;
|
||||
}
|
||||
|
||||
/// How dead process `id` ended, for `caller` — the kernel half of the
|
||||
/// process_exit_reason system call. Returns the ExitReason value, -ESRCH (never
|
||||
/// lived, still alive, or evicted from the ring), or -EPERM (the caller was not
|
||||
/// its supervisor — the same authority gate as process_kill).
|
||||
pub fn exitReasonOf(caller_id: u32, target_id: u32) i64 {
|
||||
const flags = sync.enter();
|
||||
defer sync.leave(flags);
|
||||
for (&exit_records) |*record| {
|
||||
if (record.valid and record.id == target_id) {
|
||||
if (record.supervisor != caller_id) return -ipc.EPERM;
|
||||
return @intFromEnum(record.reason);
|
||||
}
|
||||
}
|
||||
return -ipc.ESRCH;
|
||||
}
|
||||
|
||||
/// The published exit events' subscribers (docs/process-lifecycle.md "Who learns
|
||||
/// of a death"): stateful services — the VFS's file handles, input's
|
||||
/// subscriptions — that must release what a dead client held and cannot learn it
|
||||
/// any other way (a client that simply never calls again looks like silence).
|
||||
/// Bounded like every kernel table; each entry holds its own endpoint reference.
|
||||
const exit_subscriber_capacity = 8;
|
||||
const ExitSubscriber = struct { endpoint: *ipc.Endpoint, owner: u32 };
|
||||
var exit_subscribers: [exit_subscriber_capacity]?ExitSubscriber = .{null} ** exit_subscriber_capacity;
|
||||
|
||||
/// process_subscribe(endpoint): subscribe the caller's endpoint to published exit
|
||||
/// events. Ungated, like process_enumerate — what is running (and dying) is not a
|
||||
/// secret between cooperating processes. -ENOSPC when the table is full.
|
||||
fn systemProcessSubscribe(state: *architecture.CpuState) void {
|
||||
const t = scheduler.current();
|
||||
if (t.aspace == 0) return fail(state);
|
||||
const endpoint = ipc.resolveHandle(t, architecture.systemCallArg(state, 0)) orelse return failErr(state, ipc.EBADF);
|
||||
const flags = sync.enter();
|
||||
defer sync.leave(flags);
|
||||
for (&exit_subscribers) |*slot| {
|
||||
if (slot.* == null) {
|
||||
endpoint.refcount += 1; // the slot's own reference, dropped on unsubscribe-by-death
|
||||
slot.* = .{ .endpoint = endpoint, .owner = t.id };
|
||||
return architecture.setSystemCallResult(state, 0);
|
||||
}
|
||||
}
|
||||
failErr(state, ipc.ENOSPC);
|
||||
}
|
||||
|
||||
/// signal_bind(endpoint): nominate where this process's signals arrive — the
|
||||
/// IRQ-as-IPC pattern a fourth time (docs/process-lifecycle.md). Replacing a
|
||||
/// binding drops the old reference; signals that pended while unbound are
|
||||
/// delivered immediately on bind, coalesced into one notification.
|
||||
fn systemSignalBind(state: *architecture.CpuState) void {
|
||||
const t = scheduler.current();
|
||||
if (t.aspace == 0) return fail(state);
|
||||
const endpoint = ipc.resolveHandle(t, architecture.systemCallArg(state, 0)) orelse return failErr(state, ipc.EBADF);
|
||||
const flags = sync.enter();
|
||||
defer sync.leave(flags);
|
||||
if (t.signal_endpoint) |raw| ipc.dropRef(@ptrCast(@alignCast(raw)));
|
||||
endpoint.refcount += 1;
|
||||
t.signal_endpoint = @ptrCast(endpoint);
|
||||
if (t.pending_signals != 0) {
|
||||
ipc.notifyLocked(endpoint, abi.notify_signal_bit | t.pending_signals);
|
||||
t.pending_signals = 0;
|
||||
}
|
||||
architecture.setSystemCallResult(state, 0);
|
||||
}
|
||||
|
||||
/// process_signal(id, signal): post a signal — a one-way, coalescing statement,
|
||||
/// never a question (docs/process-lifecycle.md). The authority gate is the
|
||||
/// supervision link, like kill; a process may also signal itself. Unbound
|
||||
/// targets accumulate the signal in their pending mask.
|
||||
fn systemProcessSignal(state: *architecture.CpuState) void {
|
||||
const t = scheduler.current();
|
||||
if (t.aspace == 0) return fail(state);
|
||||
const id = architecture.systemCallArg(state, 0);
|
||||
const signal = architecture.systemCallArg(state, 1);
|
||||
if (id > std.math.maxInt(u32)) return failErr(state, ipc.ESRCH);
|
||||
if (signal > 31) return failErr(state, ipc.EBADF); // not a Signal bit position
|
||||
const flags = sync.enter();
|
||||
defer sync.leave(flags);
|
||||
const target = scheduler.taskByIdLocked(@intCast(id)) orelse return failErr(state, ipc.ESRCH);
|
||||
if (target.aspace == 0) return failErr(state, ipc.ESRCH);
|
||||
if (target.supervisor != t.id and target.id != t.id) return failErr(state, ipc.EPERM);
|
||||
target.pending_signals |= @as(u32, 1) << @intCast(signal);
|
||||
if (target.signal_endpoint) |raw| {
|
||||
const endpoint: *ipc.Endpoint = @ptrCast(@alignCast(raw));
|
||||
ipc.notifyLocked(endpoint, abi.notify_signal_bit | target.pending_signals);
|
||||
target.pending_signals = 0;
|
||||
}
|
||||
architecture.setSystemCallResult(state, 0);
|
||||
}
|
||||
|
||||
/// The one-shot timers of timer_bind: the missing timed wait. A service arms a
|
||||
/// deadline and keeps serving; the expiry arrives in the same replyWait as
|
||||
/// everything else (notify_timer_bit). What stop-sequence escalation, hello
|
||||
/// deadlines, and restart backoff are built from — and later, `alarm`.
|
||||
const timer_capacity = 16;
|
||||
const OneShotTimer = struct { deadline: u64, endpoint: *ipc.Endpoint, owner: u32 };
|
||||
var one_shot_timers: [timer_capacity]?OneShotTimer = .{null} ** timer_capacity;
|
||||
|
||||
/// Sweep expired timers — hung on scheduler.timer_tick_hook, so it runs on every
|
||||
/// tick with the big kernel lock held, like the sleeper wake it rides beside.
|
||||
fn timerSweepLocked() void {
|
||||
const now = architecture.millis();
|
||||
for (&one_shot_timers) |*slot| {
|
||||
if (slot.*) |timer| {
|
||||
if (now >= timer.deadline) {
|
||||
ipc.notifyLocked(timer.endpoint, abi.notify_timer_bit);
|
||||
ipc.dropRef(timer.endpoint);
|
||||
slot.* = null;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// timer_bind(endpoint, ms): arm a one-shot timer. -ENOSPC when the table is full.
|
||||
fn systemTimerBind(state: *architecture.CpuState) void {
|
||||
const t = scheduler.current();
|
||||
if (t.aspace == 0) return fail(state);
|
||||
const endpoint = ipc.resolveHandle(t, architecture.systemCallArg(state, 0)) orelse return failErr(state, ipc.EBADF);
|
||||
const ms = architecture.systemCallArg(state, 1);
|
||||
const flags = sync.enter();
|
||||
defer sync.leave(flags);
|
||||
for (&one_shot_timers) |*slot| {
|
||||
if (slot.* == null) {
|
||||
endpoint.refcount += 1;
|
||||
slot.* = .{ .deadline = architecture.millis() + ms, .endpoint = endpoint, .owner = t.id };
|
||||
return architecture.setSystemCallResult(state, 0);
|
||||
}
|
||||
}
|
||||
failErr(state, ipc.ENOSPC);
|
||||
}
|
||||
|
||||
fn systemProcessExitReason(state: *architecture.CpuState) void {
|
||||
const t = scheduler.current();
|
||||
if (t.aspace == 0) return fail(state);
|
||||
const id = architecture.systemCallArg(state, 0);
|
||||
if (id > std.math.maxInt(u32)) return failErr(state, ipc.ESRCH);
|
||||
const r = exitReasonOf(t.id, @intCast(id));
|
||||
architecture.setSystemCallResult(state, @bitCast(r));
|
||||
}
|
||||
|
||||
/// Resolve `(device_id, resource_index)` to a GSI this process is entitled to bind, or null.
|
||||
/// The two checks are the whole security story: the device must be *claimed* by the
|
||||
/// caller, and the resource must be one of that device's `irq` resources as recorded
|
||||
|
|
|
|||
|
|
@ -50,6 +50,17 @@ pub const Task = struct {
|
|||
// null. Holds its own reference, dropped when the notification is posted.
|
||||
// Opaque here for the same reason as `handles` below.
|
||||
exit_endpoint: ?*anyopaque = null,
|
||||
// How this process ended — set by the death paths (exit, fault, kill) just
|
||||
// before the reap records it for `process_exit_reason`. Meaningless while
|
||||
// the task lives.
|
||||
exit_reason: abi.ExitReason = .exited,
|
||||
// Endpoint this process's signals arrive on (signal_bind), or null — same
|
||||
// ownership rules as exit_endpoint (holds a reference; opaque here).
|
||||
signal_endpoint: ?*anyopaque = null,
|
||||
// Signals posted but not yet delivered: the coalescing pending mask
|
||||
// (docs/process-lifecycle.md). Bits are abi.Signal values. Signals pend here
|
||||
// until an endpoint is bound; two pending terminates are one terminate.
|
||||
pending_signals: u32 = 0,
|
||||
// Set by process_kill on a task that is running on another core; the kernel
|
||||
// finishes the kill at that task's next system call or timer tick.
|
||||
kill_pending: bool = false,
|
||||
|
|
@ -643,9 +654,15 @@ fn reapKillPendingLocked() void {
|
|||
/// other critical section, but releases it *without* touching the interrupt flag
|
||||
/// — the handler's `iretq` restores the interrupted context's flags, so
|
||||
/// re-enabling here would open a nested-interrupt window before the return.
|
||||
/// Called from the tick with the big kernel lock held — process.zig hangs the
|
||||
/// one-shot timer sweep here (timer_bind), the same call-up pattern as the
|
||||
/// teardown hooks below.
|
||||
pub var timer_tick_hook: ?*const fn () void = null;
|
||||
|
||||
pub fn tick() void {
|
||||
_ = sync.enter();
|
||||
wakeExpired();
|
||||
if (timer_tick_hook) |hook| hook();
|
||||
reapKillPendingLocked();
|
||||
if (preemption_enabled) schedule();
|
||||
sync.leaveIsr();
|
||||
|
|
|
|||
|
|
@ -132,6 +132,12 @@ pub fn run(case: []const u8, boot_information: *const BootInformation) void {
|
|||
processKillTest(boot_information);
|
||||
} else if (eql(case, "supervision")) {
|
||||
supervisionTest(boot_information);
|
||||
} else if (eql(case, "claim-release")) {
|
||||
claimReleaseTest(boot_information);
|
||||
} else if (eql(case, "vfs-client-death")) {
|
||||
vfsClientDeathTest(boot_information);
|
||||
} else if (eql(case, "signals")) {
|
||||
signalsTest(boot_information);
|
||||
} else if (eql(case, "initial-ramdisk")) {
|
||||
initialRamdiskTest(boot_information);
|
||||
} else if (eql(case, "vfs")) {
|
||||
|
|
@ -1207,15 +1213,15 @@ fn userPfTest() void {
|
|||
/// hand — address space, code page RO+X, stack page RW+NX — because the blob is a
|
||||
/// raw code fragment, not an ELF `spawnProcess` could load. Returns false if any
|
||||
/// allocation fails.
|
||||
fn spawnFaultingProcess() bool {
|
||||
fn spawnFaultingProcess() ?u32 {
|
||||
const blob = process.pfBlob();
|
||||
const flags = sync.enter();
|
||||
defer sync.leave(flags);
|
||||
|
||||
const aspace = architecture.createAddressSpace() orelse return false;
|
||||
const aspace = architecture.createAddressSpace() orelse return null;
|
||||
const code_frame = pmm.alloc() orelse {
|
||||
architecture.destroyAddressSpace(aspace);
|
||||
return false;
|
||||
return null;
|
||||
};
|
||||
// Fill through the physmap (the user mapping is read-only); pad with int3 so a
|
||||
// stray jump traps instead of sliding.
|
||||
|
|
@ -1226,15 +1232,16 @@ fn spawnFaultingProcess() bool {
|
|||
|
||||
const stack_frame = pmm.alloc() orelse {
|
||||
architecture.destroyAddressSpace(aspace); // frees code_frame too — it's mapped
|
||||
return false;
|
||||
return null;
|
||||
};
|
||||
architecture.mapUserPageInto(aspace, process.stack_base_virtual, stack_frame, true, false); // RW + NX
|
||||
|
||||
if (scheduler.spawnUserLocked(aspace, process.code_virtual, process.stack_base_virtual + abi.page_size, 4, "fault-probe", 0, null) == null) {
|
||||
// Supervised by the calling test task, so exitReasonOf can read the verdict.
|
||||
const id = scheduler.spawnUserLocked(aspace, process.code_virtual, process.stack_base_virtual + abi.page_size, 4, "fault-probe", scheduler.currentId(), null) orelse {
|
||||
architecture.destroyAddressSpace(aspace);
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
return null;
|
||||
};
|
||||
return id;
|
||||
}
|
||||
|
||||
/// Fault recovery (docs/resilience.md step 2): a scheduled ring-3 process that
|
||||
|
|
@ -1263,7 +1270,8 @@ fn faultRecoveryTest(boot_information: *const BootInformation) void {
|
|||
scheduler.setPriority(4);
|
||||
check("init heartbeat before the fault", process.write_count >= 1);
|
||||
|
||||
check("faulting process spawned", spawnFaultingProcess());
|
||||
const probe = spawnFaultingProcess() orelse 0;
|
||||
check("faulting process spawned", probe != 0);
|
||||
|
||||
// The kill: the faulting process #PFs on its first instruction and the kernel
|
||||
// reaps it instead of halting.
|
||||
|
|
@ -1272,6 +1280,7 @@ fn faultRecoveryTest(boot_information: *const BootInformation) void {
|
|||
while (process.fault_kill_count < 1 and architecture.millis() < deadline) scheduler.yield();
|
||||
scheduler.setPriority(4);
|
||||
check("faulting process was killed (not the machine)", process.fault_kill_count == 1);
|
||||
check("the probe's reason reads segmentation_fault", process.exitReasonOf(scheduler.currentId(), probe) == @intFromEnum(abi.ExitReason.segmentation_fault));
|
||||
|
||||
// Life after the kill: init must keep beating on the same core.
|
||||
const beats_at_kill = process.write_count;
|
||||
|
|
@ -1423,6 +1432,12 @@ fn processKillTest(boot_information: *const BootInformation) void {
|
|||
check("the sleeper's exit notification arrived (length 0)", r == 0);
|
||||
check("its badge carries the exit bit and the child id", badge == abi.notify_badge_bit | abi.notify_exit_bit | sleeper);
|
||||
|
||||
// M17.2: the recorded reason — the notification is the fence, so it is
|
||||
// already readable, and gated by the same supervisor check as the kill.
|
||||
check("the sleeper's reason reads killed", process.exitReasonOf(me, sleeper) == @intFromEnum(abi.ExitReason.killed));
|
||||
check("a non-supervisor may not read the reason (-EPERM)", process.exitReasonOf(me + 12345, sleeper) == -ipcsync.EPERM);
|
||||
check("an unknown id has no reason (-ESRCH)", process.exitReasonOf(me, 0xFFFF_FF00) == -ipcsync.ESRCH);
|
||||
|
||||
const beats_at_kill = process.write_count;
|
||||
scheduler.sleep(1500); // more than one heartbeat period
|
||||
check("the heartbeat stopped with the kill", process.write_count == beats_at_kill);
|
||||
|
|
@ -1443,6 +1458,21 @@ fn processKillTest(boot_information: *const BootInformation) void {
|
|||
check("the spinner's exit notification arrived (length 0)", r == 0);
|
||||
check("its badge carries the exit bit and the child id", badge == abi.notify_badge_bit | abi.notify_exit_bit | spinner);
|
||||
|
||||
// M17.2: a child that ends on its own must read exited, not killed —
|
||||
// args-echo with arguments echoes once and returns from main.
|
||||
var clean: u32 = 0;
|
||||
i = 0;
|
||||
while (i < rd.count) : (i += 1) {
|
||||
const item = rd.entry(i) orelse continue;
|
||||
if (!eql(item.name, "args-echo")) continue;
|
||||
clean = process.spawnProcessSupervised(item.blob, 4, &.{ "args-echo", "clean-exit" }, me, endpoint) catch 0;
|
||||
break;
|
||||
}
|
||||
check("args-echo spawned as the clean-exit child", clean != 0);
|
||||
r = ipcsync.replyWait(endpoint, 0, 0, 0, 0, abi.no_cap, &badge, &received_cap);
|
||||
check("the clean child's exit notification arrived", badge == abi.notify_badge_bit | abi.notify_exit_bit | clean);
|
||||
check("the clean child's reason reads exited", process.exitReasonOf(me, clean) == @intFromEnum(abi.ExitReason.exited));
|
||||
|
||||
var table: [32]abi.ProcessDescriptor = undefined;
|
||||
const total = scheduler.enumerate(&table);
|
||||
var still_listed = false;
|
||||
|
|
@ -1454,6 +1484,176 @@ fn processKillTest(boot_information: *const BootInformation) void {
|
|||
result();
|
||||
}
|
||||
|
||||
/// M17.1: a dead process's device claims are released by the reap, so a restarted
|
||||
/// driver can claim its hardware again (docs/process-lifecycle.md iron rule 1).
|
||||
/// First the broker release in isolation — two owners, one released, the other's
|
||||
/// claim must survive. Then the death-path wiring with a real child: the claim is
|
||||
/// made on the child's behalf (the broker is kernel-callable), the child is
|
||||
/// killed, and once the exit notification arrives — posted last, after release —
|
||||
/// the device must be unclaimed and claimable again.
|
||||
fn claimReleaseTest(boot_information: *const BootInformation) void {
|
||||
log("DANOS-TEST-BEGIN: claim-release\n", .{});
|
||||
|
||||
var buffer: [2]device_abi.DeviceDescriptor = undefined;
|
||||
const total = devices_broker.enumerate(&buffer);
|
||||
check("the device tree is seeded (>= 2 devices)", total >= 2);
|
||||
if (total < 2) {
|
||||
result();
|
||||
return;
|
||||
}
|
||||
|
||||
// The broker release in isolation.
|
||||
check("device 0 claimed by owner 111", devices_broker.claim(0, 111));
|
||||
check("device 1 claimed by owner 222", devices_broker.claim(1, 222));
|
||||
devices_broker.releaseAllOwnedBy(111);
|
||||
check("owner 111's claim is released", devices_broker.ownerOf(0) == null);
|
||||
check("owner 222's claim survives", (devices_broker.ownerOf(1) orelse 0) == 222);
|
||||
devices_broker.releaseAllOwnedBy(222);
|
||||
check("cleanup released owner 222", devices_broker.ownerOf(1) == null);
|
||||
|
||||
// The death-path wiring: a real process dies holding a claim.
|
||||
check("bootloader handed over /system/services/init", boot_information.init_len != 0);
|
||||
if (boot_information.init_len == 0) {
|
||||
result();
|
||||
return;
|
||||
}
|
||||
const image = @as([*]const u8, @ptrFromInt(boot_handoff.physicalToVirtual(boot_information.init_base)))[0..boot_information.init_len];
|
||||
const me = scheduler.currentId();
|
||||
const endpoint = ipcsync.createIpcEndpoint() orelse {
|
||||
check("exit endpoint allocated", false);
|
||||
result();
|
||||
return;
|
||||
};
|
||||
const child = process.spawnProcessSupervised(image, 4, &.{"/system/services/init"}, me, endpoint) catch 0;
|
||||
check("supervised child spawned", child != 0);
|
||||
check("device 0 claimed on the child's behalf", devices_broker.claim(0, child));
|
||||
|
||||
check("the kill is accepted", process.killProcess(me, child) == 0);
|
||||
var badge: u64 = 0;
|
||||
var received_cap: u64 = 0;
|
||||
_ = ipcsync.replyWait(endpoint, 0, 0, 0, 0, abi.no_cap, &badge, &received_cap);
|
||||
check("the exit notification arrived", badge == abi.notify_badge_bit | abi.notify_exit_bit | child);
|
||||
check("death released the child's claim", devices_broker.ownerOf(0) == null);
|
||||
check("the device is claimable again", devices_broker.claim(0, me));
|
||||
devices_broker.releaseAllOwnedBy(me);
|
||||
result();
|
||||
}
|
||||
|
||||
/// M17.3: the published exit events, proven by their first subscriber. The VFS
|
||||
/// subscribes at startup; a client opens a file and parks holding the handle;
|
||||
/// the kill posts the exit event to the VFS's endpoint; the VFS releases the
|
||||
/// dead client's handle and says so — the service-side mirror of iron rule 1
|
||||
/// (a service must never depend on clients cleaning up after themselves).
|
||||
fn vfsClientDeathTest(boot_information: *const BootInformation) void {
|
||||
log("DANOS-TEST-BEGIN: vfs-client-death\n", .{});
|
||||
if (boot_information.initial_ramdisk_len == 0) {
|
||||
check("bootloader handed over an initial_ramdisk", false);
|
||||
result();
|
||||
return;
|
||||
}
|
||||
const image = @as([*]const u8, @ptrFromInt(boot_handoff.physicalToVirtual(boot_information.initial_ramdisk_base)))[0..boot_information.initial_ramdisk_len];
|
||||
const rd = initial_ramdisk.Reader.init(image) orelse {
|
||||
check("initial_ramdisk image is valid", false);
|
||||
result();
|
||||
return;
|
||||
};
|
||||
|
||||
process.write_count = 0;
|
||||
check("vfs spawned", spawnNamed(rd, "vfs"));
|
||||
|
||||
const me = scheduler.currentId();
|
||||
const endpoint = ipcsync.createIpcEndpoint() orelse {
|
||||
check("exit endpoint allocated", false);
|
||||
result();
|
||||
return;
|
||||
};
|
||||
var client: u32 = 0;
|
||||
var i: u32 = 0;
|
||||
while (i < rd.count) : (i += 1) {
|
||||
const item = rd.entry(i) orelse continue;
|
||||
if (!eql(item.name, "vfs-test")) continue;
|
||||
client = process.spawnProcessSupervised(item.blob, 4, &.{ "vfs-test", "park" }, me, endpoint) catch 0;
|
||||
break;
|
||||
}
|
||||
check("parked client spawned (supervised)", client != 0);
|
||||
|
||||
// Its heartbeat is the fence: once it beats, the handle is open.
|
||||
const parked = "vfstest: parked";
|
||||
scheduler.setPriority(1);
|
||||
var deadline = architecture.millis() + 10000;
|
||||
while (architecture.millis() < deadline) {
|
||||
if (process.write_len >= parked.len and eql(process.write_buffer[0..parked.len], parked)) break;
|
||||
scheduler.yield();
|
||||
}
|
||||
scheduler.setPriority(4);
|
||||
check("client parked holding an open handle", process.write_len >= parked.len and eql(process.write_buffer[0..parked.len], parked));
|
||||
|
||||
check("the kill is accepted", process.killProcess(me, client) == 0);
|
||||
var badge: u64 = 0;
|
||||
var received_cap: u64 = 0;
|
||||
_ = ipcsync.replyWait(endpoint, 0, 0, 0, 0, abi.no_cap, &badge, &received_cap);
|
||||
check("the exit notification arrived", badge == abi.notify_badge_bit | abi.notify_exit_bit | client);
|
||||
|
||||
// The VFS heard the same published event; its release line is the proof.
|
||||
const released = "vfs: released 1 handle(s) for dead client";
|
||||
scheduler.setPriority(1);
|
||||
deadline = architecture.millis() + 10000;
|
||||
while (architecture.millis() < deadline) {
|
||||
if (process.write_len >= released.len and eql(process.write_buffer[0..released.len], released)) break;
|
||||
scheduler.yield();
|
||||
}
|
||||
scheduler.setPriority(4);
|
||||
check("the VFS released the dead client's handle", process.write_len >= released.len and eql(process.write_buffer[0..released.len], released));
|
||||
result();
|
||||
}
|
||||
|
||||
/// M17.4 from ring 3: process-test's signal-run role drives the whole lifecycle
|
||||
/// surface — the zero-length ping (answered by the harness), signals as
|
||||
/// statements (reload logged, terminate = clean exit), the one-shot timer, and
|
||||
/// both endings of the stop sequence (polite -> exited, deaf -> killed at the
|
||||
/// deadline). Its "process-test: signals ok" is the pass marker.
|
||||
fn signalsTest(boot_information: *const BootInformation) void {
|
||||
log("DANOS-TEST-BEGIN: signals\n", .{});
|
||||
if (boot_information.initial_ramdisk_len == 0) {
|
||||
check("bootloader handed over an initial_ramdisk", false);
|
||||
result();
|
||||
return;
|
||||
}
|
||||
const image = @as([*]const u8, @ptrFromInt(boot_handoff.physicalToVirtual(boot_information.initial_ramdisk_base)))[0..boot_information.initial_ramdisk_len];
|
||||
const rd = initial_ramdisk.Reader.init(image) orelse {
|
||||
check("initial_ramdisk image is valid", false);
|
||||
result();
|
||||
return;
|
||||
};
|
||||
|
||||
process.setInitialRamdisk(image); // the parent system_spawns its children by name
|
||||
process.write_count = 0;
|
||||
var runner: u32 = 0;
|
||||
var i: u32 = 0;
|
||||
while (i < rd.count) : (i += 1) {
|
||||
const item = rd.entry(i) orelse continue;
|
||||
if (!eql(item.name, "process-test")) continue;
|
||||
runner = process.spawnProcessSupervised(item.blob, 4, &.{ "process-test", "signal-run" }, scheduler.currentId(), null) catch 0;
|
||||
break;
|
||||
}
|
||||
check("signal-run parent spawned", runner != 0);
|
||||
|
||||
const pass_marker = "process-test: signals ok";
|
||||
const fail_marker = "process-test: FAIL";
|
||||
scheduler.setPriority(1);
|
||||
const deadline = architecture.millis() + 15000;
|
||||
var saw_pass = false;
|
||||
var saw_fail = false;
|
||||
while (architecture.millis() < deadline and !saw_pass and !saw_fail) {
|
||||
if (process.write_len >= pass_marker.len and eql(process.write_buffer[0..pass_marker.len], pass_marker)) saw_pass = true;
|
||||
if (process.write_len >= fail_marker.len and eql(process.write_buffer[0..fail_marker.len], fail_marker)) saw_fail = true;
|
||||
scheduler.yield();
|
||||
}
|
||||
scheduler.setPriority(4);
|
||||
check("the signal-run parent reported ok", saw_pass and !saw_fail);
|
||||
result();
|
||||
}
|
||||
|
||||
/// The whole user-side surface at once: spawn process-test's supervisor role,
|
||||
/// which — entirely from ring 3 — creates an exit endpoint, spawns its two
|
||||
/// children supervised, sees them in process_enumerate, kills them (one blocked,
|
||||
|
|
|
|||
|
|
@ -48,11 +48,91 @@ fn awaitChildExit(endpoint: runtime.ipc.Handle) u32 {
|
|||
return received.childProcessId();
|
||||
}
|
||||
|
||||
/// The harness-run child of the signals test: echoes requests, logs the two
|
||||
/// signals it handles. Terminate makes run() return, and returning from main is
|
||||
/// the clean exit the parent reads as ExitReason.exited.
|
||||
fn echo(message: []const u8, reply: []u8, sender: u32) usize {
|
||||
_ = sender;
|
||||
const n = @min(message.len, reply.len);
|
||||
@memcpy(reply[0..n], message[0..n]);
|
||||
return n;
|
||||
}
|
||||
|
||||
fn onReload() void {
|
||||
_ = runtime.system.write("process-test: reloaded\n");
|
||||
}
|
||||
|
||||
fn onTerminate() void {
|
||||
_ = runtime.system.write("process-test: terminating\n");
|
||||
}
|
||||
|
||||
/// The parent of the signals test: drives ping, echo, reload, the one-shot
|
||||
/// timer, and both endings of the stop sequence (polite -> exited; deaf ->
|
||||
/// killed at the deadline). Prints "process-test: signals ok" as the marker.
|
||||
fn signalRun() void {
|
||||
const endpoint = runtime.ipc.createIpcEndpoint() orelse fail("create exit endpoint");
|
||||
const child = runtime.system.spawnSupervised("process-test", &.{"service"}, endpoint) orelse fail("spawn service child");
|
||||
|
||||
// Reach the child's endpoint through the registry (retry: it may not be up).
|
||||
var service_handle: ?runtime.ipc.Handle = null;
|
||||
var tries: u32 = 0;
|
||||
while (service_handle == null and tries < 200) : (tries += 1) {
|
||||
service_handle = runtime.ipc.lookup(.input);
|
||||
if (service_handle == null) runtime.system.sleep(20);
|
||||
}
|
||||
const h = service_handle orelse fail("service child never registered");
|
||||
|
||||
// The universal ping: a zero-length call answered zero-length by the harness.
|
||||
var reply: [16]u8 = undefined;
|
||||
const pong = runtime.ipc.call(h, &.{}, &reply) catch fail("ping call failed");
|
||||
if (pong != 0) fail("ping reply not empty");
|
||||
|
||||
// An ordinary request still reaches on_message.
|
||||
const n = runtime.ipc.call(h, "echo!", &reply) catch fail("echo call failed");
|
||||
if (n != 5 or !std.mem.eql(u8, reply[0..5], "echo!")) fail("echo mismatch");
|
||||
|
||||
// reload: a statement — the child logs it; the kernel test reads the serial.
|
||||
if (!runtime.process.sendSignal(child, .reload)) fail("send reload");
|
||||
runtime.system.sleep(200);
|
||||
|
||||
// The one-shot timer: armed on our endpoint, lands as isTimer.
|
||||
if (!runtime.system.timerOnce(endpoint, 100)) fail("arm timer");
|
||||
var scratch: [8]u8 = undefined;
|
||||
const landing = runtime.ipc.replyWait(endpoint, scratch[0..0], &scratch, null);
|
||||
if (!landing.isTimer()) fail("expected the timer landing");
|
||||
|
||||
// The stop sequence, polite path: terminate, clean exit inside the deadline.
|
||||
runtime.process.stop(child, 2000, endpoint);
|
||||
if ((runtime.process.exitReason(child) orelse .killed) != .exited) fail("service child reason not exited");
|
||||
|
||||
// The deaf child: binds nothing, hears nothing — the deadline kills it.
|
||||
const deaf = runtime.system.spawnSupervised("process-test", &.{"sleeper"}, endpoint) orelse fail("spawn deaf child");
|
||||
runtime.system.sleep(50); // let it reach its sleep
|
||||
runtime.process.stop(deaf, 300, endpoint);
|
||||
if ((runtime.process.exitReason(deaf) orelse .exited) != .killed) fail("deaf child reason not killed");
|
||||
|
||||
_ = runtime.system.write("process-test: signals ok\n");
|
||||
}
|
||||
|
||||
pub fn main(init: runtime.process.Init) void {
|
||||
const role = init.arguments.get(1) orelse return; // spawned bare (ramdisk sweep): stay silent
|
||||
if (std.mem.eql(u8, role, "sleeper")) {
|
||||
while (true) runtime.system.sleep(500);
|
||||
}
|
||||
if (std.mem.eql(u8, role, "service")) {
|
||||
// Borrowed well-known id: the input service is not part of this scenario.
|
||||
runtime.service.run(64, .{
|
||||
.service = .input,
|
||||
.on_message = echo,
|
||||
.on_reload = onReload,
|
||||
.on_terminate = onTerminate,
|
||||
});
|
||||
return; // terminate arrived; returning is the clean exit
|
||||
}
|
||||
if (std.mem.eql(u8, role, "signal-run")) {
|
||||
signalRun();
|
||||
return;
|
||||
}
|
||||
if (std.mem.eql(u8, role, "spinner")) {
|
||||
var beat: u64 = 0;
|
||||
const touch: *volatile u64 = &beat;
|
||||
|
|
@ -89,6 +169,12 @@ pub fn main(init: runtime.process.Init) void {
|
|||
if (listed(sleeper, "process-test")) fail("sleeper still listed after kill");
|
||||
if (listed(spinner, "process-test")) fail("spinner still listed after kill");
|
||||
|
||||
// M17.2: both children were killed by us, and the reason says so — the whole
|
||||
// restart-policy input, read through the runtime like a real supervisor would.
|
||||
if ((runtime.process.exitReason(sleeper) orelse .exited) != .killed) fail("sleeper reason not killed");
|
||||
if ((runtime.process.exitReason(spinner) orelse .exited) != .killed) fail("spinner reason not killed");
|
||||
if (runtime.process.exitReason(0xFFFF_FFF0) != null) fail("unknown id had a reason");
|
||||
|
||||
_ = runtime.system.write("process-test: ok\n");
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -6,10 +6,30 @@
|
|||
const std = @import("std");
|
||||
const runtime = @import("runtime");
|
||||
|
||||
pub fn main() void {
|
||||
pub fn main(init: runtime.process.Init) void {
|
||||
const u = @import("posix").unistd;
|
||||
const payload = "hello-vfs";
|
||||
|
||||
// The "park" role (the vfs-client-death test): open a file, then hold the
|
||||
// handle forever without closing — the kill and the VFS's release-on-death
|
||||
// are the point.
|
||||
if (init.arguments.count > 1) {
|
||||
var fd: i32 = -1;
|
||||
var tries: u32 = 0;
|
||||
while (fd < 0 and tries < 200) : (tries += 1) {
|
||||
fd = u.open("parked", u.O_CREAT);
|
||||
if (fd < 0) runtime.system.sleep(20);
|
||||
}
|
||||
if (fd < 0) {
|
||||
_ = runtime.system.write("vfstest: park open failed\n");
|
||||
return;
|
||||
}
|
||||
while (true) {
|
||||
_ = runtime.system.write("vfstest: parked\n");
|
||||
runtime.system.sleep(500);
|
||||
}
|
||||
}
|
||||
|
||||
// The VFS server may not have registered yet — retry open until it's up.
|
||||
var fd: i32 = -1;
|
||||
var tries: u32 = 0;
|
||||
|
|
|
|||
|
|
@ -23,6 +23,10 @@ const Node = struct {
|
|||
const OpenFile = struct {
|
||||
used: bool = false,
|
||||
node: usize = 0,
|
||||
// The client (task id — an IPC badge is one) that opened this handle. What
|
||||
// release-on-death sweeps by: a service must never depend on its clients
|
||||
// cleaning up after themselves (docs/process-lifecycle.md).
|
||||
owner: u32 = 0,
|
||||
};
|
||||
|
||||
var nodes = [_]Node{.{}} ** 8;
|
||||
|
|
@ -65,8 +69,29 @@ fn fail(out: []u8) usize {
|
|||
return writeReply(out, .{ .status = -1 }, &.{});
|
||||
}
|
||||
|
||||
/// Handle one request; write the reply into `out`, return its length.
|
||||
fn handle(message: []const u8, out: []u8) usize {
|
||||
/// Format one whole log line and emit it in a single `debug_write`, so lines from
|
||||
/// concurrent processes can never land in the middle of it.
|
||||
fn writeLine(comptime fmt: []const u8, arguments: anytype) void {
|
||||
var line: [96]u8 = undefined;
|
||||
_ = runtime.system.write(std.fmt.bufPrint(&line, fmt, arguments) catch return);
|
||||
}
|
||||
|
||||
/// Release every open handle `client` held — called on that client's published
|
||||
/// exit event. The nodes (the files) stay: ramfs contents outlive their writers,
|
||||
/// only the dead client's handles go.
|
||||
fn releaseClientHandles(client: u32) void {
|
||||
var released: u32 = 0;
|
||||
for (&opens) |*o| {
|
||||
if (o.used and o.owner == client) {
|
||||
o.used = false;
|
||||
released += 1;
|
||||
}
|
||||
}
|
||||
if (released != 0) writeLine("vfs: released {d} handle(s) for dead client {d}\n", .{ released, client });
|
||||
}
|
||||
|
||||
/// Handle one request from `sender`; write the reply into `out`, return its length.
|
||||
fn handle(message: []const u8, out: []u8, sender: u32) usize {
|
||||
if (message.len < protocol.request_size) return fail(out);
|
||||
const request = std.mem.bytesToValue(protocol.Request, message[0..protocol.request_size]);
|
||||
const payload = message[protocol.request_size..];
|
||||
|
|
@ -77,7 +102,7 @@ fn handle(message: []const u8, out: []u8) usize {
|
|||
const ni = findNode(name) orelse createNode(name) orelse return fail(out);
|
||||
for (&opens, 0..) |*o, i| {
|
||||
if (!o.used) {
|
||||
o.* = .{ .used = true, .node = ni };
|
||||
o.* = .{ .used = true, .node = ni, .owner = sender };
|
||||
return writeReply(out, .{ .status = 0, .node = i }, &.{});
|
||||
}
|
||||
}
|
||||
|
|
@ -113,27 +138,36 @@ fn handle(message: []const u8, out: []u8) usize {
|
|||
}
|
||||
}
|
||||
|
||||
pub fn main() void {
|
||||
const endpoint = runtime.ipc.createIpcEndpoint() orelse {
|
||||
_ = runtime.system.write("vfs: no endpoint\n");
|
||||
return;
|
||||
};
|
||||
if (!runtime.ipc.register(.vfs, endpoint)) {
|
||||
_ = runtime.system.write("vfs: register failed\n");
|
||||
return;
|
||||
/// Startup, under the harness: subscribe to the published exit events — when a
|
||||
/// client dies holding open handles, the exit notification is how the VFS learns
|
||||
/// to release them (docs/process-lifecycle.md).
|
||||
fn initialise(endpoint: runtime.ipc.Handle) bool {
|
||||
if (!runtime.process.subscribeExits(endpoint)) {
|
||||
_ = runtime.system.write("vfs: exit subscription failed\n");
|
||||
}
|
||||
_ = runtime.system.write("vfs: ready\n");
|
||||
return true;
|
||||
}
|
||||
|
||||
var reply_buffer: [protocol.message_maximum]u8 = undefined;
|
||||
var reply_len: usize = 0;
|
||||
var receive: [protocol.message_maximum]u8 = undefined;
|
||||
while (true) {
|
||||
const got = runtime.ipc.replyWait(endpoint, reply_buffer[0..reply_len], &receive, null);
|
||||
// Ignore notifications (none expected here); handle a request.
|
||||
reply_len = handle(receive[0..got.len], &reply_buffer);
|
||||
/// A non-signal notification: the only kind the VFS subscribes to is exit events.
|
||||
fn onNotification(badge: u64) void {
|
||||
if (badge & runtime.ipc.notify_exit_bit != 0) {
|
||||
releaseClientHandles(@intCast(badge & ~(runtime.ipc.notify_badge_bit | runtime.ipc.notify_exit_bit)));
|
||||
}
|
||||
}
|
||||
|
||||
pub fn main() void {
|
||||
// The harness owns the loop: requests dispatch to handle(), exit events to
|
||||
// onNotification(), ping and terminate are answered for free — this service
|
||||
// gained the whole lifecycle contract by deleting its hand-rolled loop.
|
||||
runtime.service.run(protocol.message_maximum, .{
|
||||
.service = .vfs,
|
||||
.init = initialise,
|
||||
.on_message = handle,
|
||||
.on_notification = onNotification,
|
||||
});
|
||||
}
|
||||
|
||||
pub const panic = runtime.panic;
|
||||
comptime {
|
||||
_ = &runtime.start._start;
|
||||
|
|
|
|||
|
|
@ -240,6 +240,24 @@ CASES = [
|
|||
"smp": 4,
|
||||
"expect": r"DANOS-TEST-RESULT: PASS",
|
||||
"fail": r"DANOS-TEST-RESULT: FAIL"},
|
||||
# M17.1: a dead process's device claims are released by the reap — kill a child
|
||||
# holding a claim, the device must be claimable again (process-lifecycle.md).
|
||||
{"name": "claim-release",
|
||||
"smp": 4,
|
||||
"expect": r"DANOS-TEST-RESULT: PASS",
|
||||
"fail": r"DANOS-TEST-RESULT: FAIL"},
|
||||
# M17.3: published exit events — the VFS subscribes, a client dies holding an
|
||||
# open handle, and the VFS releases it (process-lifecycle.md "Who learns of a death").
|
||||
{"name": "vfs-client-death",
|
||||
"smp": 4,
|
||||
"expect": r"DANOS-TEST-RESULT: PASS",
|
||||
"fail": r"DANOS-TEST-RESULT: FAIL"},
|
||||
# M17.4: signals over IPC — ping, reload, terminate (clean exit), the one-shot
|
||||
# timer, and the stop sequence's two endings, all driven from ring 3.
|
||||
{"name": "signals",
|
||||
"smp": 4,
|
||||
"expect": r"DANOS-TEST-RESULT: PASS",
|
||||
"fail": r"DANOS-TEST-RESULT: FAIL"},
|
||||
# The initial_ramdisk: the loader ferries a bundle of user binaries; the kernel parses
|
||||
# it and spawns each as a ring-3 process (here the VFS-server stub heartbeats).
|
||||
{"name": "initial-ramdisk",
|
||||
|
|
|
|||
Loading…
Reference in New Issue