Add process management: enumerate, supervisor-gated kill, exit notifications
process_enumerate snapshots the task table (the device_enumerate shape, so ps is a user program); system_spawn returns the child id, records the caller as supervisor, and takes an exit endpoint; process_kill is allowed only for the supervisor. Every death — exit, fault, or kill — posts a child-exit badge to that endpoint (the IRQ-as-IPC pattern as SIGCHLD). A target caught off-CPU is reaped in place; a running one is condemned and finished at its next system call or tick, guarded so teardown never lands mid-kernel-operation. Tested by process-list, process-kill, and supervision (a ring-3 supervisor exercising the whole surface); design notes in docs/process-management.md.
This commit is contained in:
@@ -0,0 +1,100 @@
|
||||
//! process-test — a test fixture for process management (bundled in the
|
||||
//! initial-ramdisk, driven by the `supervision` test case). One binary, three
|
||||
//! roles picked by argv, so the whole user-side surface is exercised end to end:
|
||||
//!
|
||||
//! - `process-test run` — the supervisor: spawns the two children below with an
|
||||
//! exit-notification endpoint, sees them in `process_enumerate`, kills them,
|
||||
//! receives both exit notifications, and confirms they are gone. Prints
|
||||
//! "process-test: ok" for the kernel test to match, or a FAIL line naming the
|
||||
//! step that broke.
|
||||
//! - `process-test sleeper` — a child that blocks in `sleep` forever: its kill
|
||||
//! exercises the immediate reap of a blocked task.
|
||||
//! - `process-test spinner` — a child that spins in user mode making no system
|
||||
//! calls: its kill exercises the deferred path (kill_pending, finished by the
|
||||
//! timer tick).
|
||||
//!
|
||||
//! Spawned with no arguments (the initial-ramdisk sweep test starts every bundled
|
||||
//! binary bare), it exits silently so it cannot derange other tests' output.
|
||||
|
||||
const std = @import("std");
|
||||
const runtime = @import("runtime");
|
||||
|
||||
fn fail(step: []const u8) noreturn {
|
||||
_ = runtime.system.write("process-test: FAIL ");
|
||||
_ = runtime.system.write(step);
|
||||
_ = runtime.system.write("\n");
|
||||
runtime.system.exit(1);
|
||||
}
|
||||
|
||||
/// Whether process `id` appears in a fresh `process_enumerate` snapshot, named
|
||||
/// `name` (an id present under the wrong name is a table mix-up, not a pass).
|
||||
fn listed(id: u32, name: []const u8) bool {
|
||||
var table: [32]runtime.system.ProcessDescriptor = undefined;
|
||||
const total = runtime.system.processes(&table);
|
||||
for (table[0..@min(total, table.len)]) |descriptor| {
|
||||
if (descriptor.id != id) continue;
|
||||
return std.mem.eql(u8, descriptor.name[0..descriptor.name_length], name);
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
/// Block on the exit endpoint until a child-exit notification arrives; returns
|
||||
/// the ended child's id. A wrong wake-up (there should be none — nothing else
|
||||
/// knows this endpoint) fails the test rather than looping forever.
|
||||
fn awaitChildExit(endpoint: runtime.ipc.Handle) u32 {
|
||||
var scratch: [8]u8 = undefined;
|
||||
const received = runtime.ipc.replyWait(endpoint, scratch[0..0], &scratch, null);
|
||||
if (!received.isChildExit()) fail("expected a child-exit notification");
|
||||
return received.childProcessId();
|
||||
}
|
||||
|
||||
pub fn main() void {
|
||||
if (runtime.argumentCount() <= 1) return; // spawned bare (ramdisk sweep): stay silent
|
||||
|
||||
const role = runtime.argument(1);
|
||||
if (std.mem.eql(u8, role, "sleeper")) {
|
||||
while (true) runtime.system.sleep(500);
|
||||
}
|
||||
if (std.mem.eql(u8, role, "spinner")) {
|
||||
var beat: u64 = 0;
|
||||
const touch: *volatile u64 = &beat;
|
||||
while (true) touch.* +%= 1; // user mode only — no system calls to die at
|
||||
}
|
||||
|
||||
// The supervisor ("run").
|
||||
const endpoint = runtime.ipc.createIpcEndpoint() orelse fail("create exit endpoint");
|
||||
|
||||
const sleeper = runtime.system.spawnSupervised("process-test", &.{"sleeper"}, endpoint) orelse fail("spawn sleeper");
|
||||
const spinner = runtime.system.spawnSupervised("process-test", &.{"spinner"}, endpoint) orelse fail("spawn spinner");
|
||||
|
||||
runtime.system.sleep(100); // let the sleeper block and the spinner get a core
|
||||
if (!listed(sleeper, "process-test")) fail("sleeper not in process_enumerate");
|
||||
if (!listed(spinner, "process-test")) fail("spinner not in process_enumerate");
|
||||
|
||||
// Kills that must be refused: a kernel task (id 0), and an id that was never
|
||||
// issued — both -ESRCH. (-EPERM needs a second supervisor; the kernel-level
|
||||
// `process-kill` test covers it.)
|
||||
if (runtime.system.kill(0)) fail("killing a kernel task was allowed");
|
||||
if (runtime.system.kill(0xFFFF_FFF0)) fail("killing an unknown id was allowed");
|
||||
|
||||
// The blocked child: usually reaped on the spot (it sits in `sleep`). The
|
||||
// notification is the fence — after it, the child is certainly gone, so the
|
||||
// second kill must miss (its id is never reused).
|
||||
if (!runtime.system.kill(sleeper)) fail("kill sleeper");
|
||||
if (awaitChildExit(endpoint) != sleeper) fail("sleeper exit notification");
|
||||
if (runtime.system.kill(sleeper)) fail("double kill was allowed");
|
||||
|
||||
// The running child: the deferred path — condemned now, dead by the next tick.
|
||||
if (!runtime.system.kill(spinner)) fail("kill spinner");
|
||||
if (awaitChildExit(endpoint) != spinner) fail("spinner exit notification");
|
||||
|
||||
if (listed(sleeper, "process-test")) fail("sleeper still listed after kill");
|
||||
if (listed(spinner, "process-test")) fail("spinner still listed after kill");
|
||||
|
||||
_ = runtime.system.write("process-test: ok\n");
|
||||
}
|
||||
|
||||
pub const panic = runtime.panic;
|
||||
comptime {
|
||||
_ = &runtime.start._start; // pull the runtime entry shim into the image
|
||||
}
|
||||
Reference in New Issue
Block a user