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:
+190
-1
@@ -126,6 +126,12 @@ pub fn run(case: []const u8, boot_information: *const BootInformation) void {
|
||||
initTest(boot_information);
|
||||
} else if (eql(case, "process")) {
|
||||
processTest(boot_information);
|
||||
} else if (eql(case, "process-list")) {
|
||||
processListTest(boot_information);
|
||||
} else if (eql(case, "process-kill")) {
|
||||
processKillTest(boot_information);
|
||||
} else if (eql(case, "supervision")) {
|
||||
supervisionTest(boot_information);
|
||||
} else if (eql(case, "initial-ramdisk")) {
|
||||
initialRamdiskTest(boot_information);
|
||||
} else if (eql(case, "vfs")) {
|
||||
@@ -1222,7 +1228,7 @@ fn spawnFaultingProcess() bool {
|
||||
};
|
||||
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")) {
|
||||
if (scheduler.spawnUserLocked(aspace, process.code_virtual, process.stack_base_virtual + abi.page_size, 4, "fault-probe", 0, null) == null) {
|
||||
architecture.destroyAddressSpace(aspace);
|
||||
return false;
|
||||
}
|
||||
@@ -1311,6 +1317,189 @@ fn initTest(boot_information: *const BootInformation) void {
|
||||
result();
|
||||
}
|
||||
|
||||
/// process_enumerate's kernel half: spawn two init processes next to the kernel
|
||||
/// tasks and snapshot the table. The snapshot must list both by name with distinct,
|
||||
/// kernel-supervised ids, include the kernel tasks (id 0, empty name), and report
|
||||
/// the same total through a too-small buffer (the truncation contract: the caller
|
||||
/// learns how big a buffer to bring).
|
||||
fn processListTest(boot_information: *const BootInformation) void {
|
||||
log("DANOS-TEST-BEGIN: process-list\n", .{});
|
||||
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];
|
||||
|
||||
var spawned: u32 = 0;
|
||||
if (process.spawnProcess(image, 4, &.{"/system/services/init"})) spawned += 1 else |_| {}
|
||||
if (process.spawnProcess(image, 4, &.{"/system/services/init"})) spawned += 1 else |_| {}
|
||||
check("two init processes spawned", spawned == 2);
|
||||
|
||||
var table: [32]abi.ProcessDescriptor = undefined;
|
||||
const total = scheduler.enumerate(&table);
|
||||
check("enumerate counts the boot task and both processes (>=3)", total >= 3);
|
||||
|
||||
var inits: u32 = 0;
|
||||
var init_ids: [2]u32 = .{ 0, 0 };
|
||||
var kernel_task_seen = false;
|
||||
var states_sane = true;
|
||||
for (table[0..@min(total, table.len)]) |descriptor| {
|
||||
if (descriptor.state > @intFromEnum(abi.ProcessState.blocked)) states_sane = false;
|
||||
if (descriptor.name_length == 0) kernel_task_seen = true;
|
||||
if (eql(descriptor.name[0..descriptor.name_length], "/system/services/init")) {
|
||||
if (inits < 2) init_ids[inits] = descriptor.id;
|
||||
inits += 1;
|
||||
check("init entry is kernel-supervised (supervisor 0)", descriptor.supervisor == 0);
|
||||
}
|
||||
}
|
||||
check("both init processes listed by name", inits == 2);
|
||||
check("listed processes carry distinct ids", init_ids[0] != init_ids[1]);
|
||||
check("kernel tasks are listed too (empty name)", kernel_task_seen);
|
||||
check("every state is a ProcessState value", states_sane);
|
||||
|
||||
var one: [1]abi.ProcessDescriptor = undefined;
|
||||
check("a too-small buffer still learns the true total", scheduler.enumerate(&one) == total);
|
||||
result();
|
||||
}
|
||||
|
||||
/// process_kill + the exit notification, kernel half. Two victims, two paths:
|
||||
/// - init, which heartbeats and sleeps: caught blocked, reaped on the killer's
|
||||
/// own call — and its heartbeat must stop.
|
||||
/// - process-test's spinner role (from the initial ramdisk), which loops in user
|
||||
/// mode making no system calls: with more cores it is caught running, taking
|
||||
/// the deferred path (kill_pending, finished by the victim core's next tick).
|
||||
/// Each death must post one exit notification badge (exit bit + the child's id)
|
||||
/// on the endpoint given at spawn; wrong-supervisor and unknown-id kills must be
|
||||
/// refused. The waits block in replyWait, so a lost notification times the
|
||||
/// harness out rather than passing vacuously.
|
||||
fn processKillTest(boot_information: *const BootInformation) void {
|
||||
log("DANOS-TEST-BEGIN: process-kill\n", .{});
|
||||
check("bootloader handed over /system/services/init", boot_information.init_len != 0);
|
||||
if (boot_information.init_len == 0 or boot_information.initial_ramdisk_len == 0) {
|
||||
check("bootloader handed over an initial_ramdisk", boot_information.initial_ramdisk_len != 0);
|
||||
result();
|
||||
return;
|
||||
}
|
||||
const image = @as([*]const u8, @ptrFromInt(boot_handoff.physicalToVirtual(boot_information.init_base)))[0..boot_information.init_len];
|
||||
const ramdisk = @as([*]const u8, @ptrFromInt(boot_handoff.physicalToVirtual(boot_information.initial_ramdisk_base)))[0..boot_information.initial_ramdisk_len];
|
||||
const rd = initial_ramdisk.Reader.init(ramdisk) orelse {
|
||||
check("initial_ramdisk image is valid", false);
|
||||
result();
|
||||
return;
|
||||
};
|
||||
|
||||
const me = scheduler.currentId();
|
||||
const endpoint = ipcsync.createIpcEndpoint() orelse {
|
||||
check("exit endpoint allocated", false);
|
||||
result();
|
||||
return;
|
||||
};
|
||||
|
||||
process.write_count = 0;
|
||||
const sleeper = process.spawnProcessSupervised(image, 4, &.{"/system/services/init"}, me, endpoint) catch 0;
|
||||
check("init spawned as the supervised sleeper victim", sleeper != 0);
|
||||
|
||||
// Let it reach its heartbeat loop (write, then a 1 s sleep) so the kill most
|
||||
// likely catches it blocked.
|
||||
scheduler.setPriority(1);
|
||||
const deadline = architecture.millis() + 8000;
|
||||
while (process.write_count < 1 and architecture.millis() < deadline) scheduler.yield();
|
||||
scheduler.setPriority(4);
|
||||
check("victim heartbeat before the kill", process.write_count >= 1);
|
||||
|
||||
// Kills that must be refused, before the one that must not be.
|
||||
check("a non-supervisor may not kill (-EPERM)", process.killProcess(me + 12345, sleeper) == -ipcsync.EPERM);
|
||||
check("an unknown id misses (-ESRCH)", process.killProcess(me, 0xFFFF_FF00) == -ipcsync.ESRCH);
|
||||
check("a kernel task is not a killable process (-ESRCH)", process.killProcess(me, 0) == -ipcsync.ESRCH);
|
||||
|
||||
check("the supervisor's kill is accepted", process.killProcess(me, sleeper) == 0);
|
||||
|
||||
var badge: u64 = 0;
|
||||
var received_cap: u64 = 0;
|
||||
var r = ipcsync.replyWait(endpoint, 0, 0, 0, 0, abi.no_cap, &badge, &received_cap);
|
||||
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);
|
||||
|
||||
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);
|
||||
|
||||
// The spinner: no system calls, so only the tick can deliver a deferred kill.
|
||||
var spinner: 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;
|
||||
spinner = process.spawnProcessSupervised(item.blob, 4, &.{ "process-test", "spinner" }, me, endpoint) catch 0;
|
||||
break;
|
||||
}
|
||||
check("process-test spawned as the supervised spinner victim", spinner != 0);
|
||||
scheduler.sleep(100); // give another core a chance to be running it
|
||||
check("the spinner's kill is accepted", process.killProcess(me, spinner) == 0);
|
||||
r = ipcsync.replyWait(endpoint, 0, 0, 0, 0, abi.no_cap, &badge, &received_cap);
|
||||
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);
|
||||
|
||||
var table: [32]abi.ProcessDescriptor = undefined;
|
||||
const total = scheduler.enumerate(&table);
|
||||
var still_listed = false;
|
||||
for (table[0..@min(total, table.len)]) |descriptor| {
|
||||
if (descriptor.id == sleeper or descriptor.id == spinner) still_listed = true;
|
||||
}
|
||||
check("neither victim is listed after its kill", !still_listed);
|
||||
check("a killed id stays dead (-ESRCH on a second kill)", process.killProcess(me, sleeper) == -ipcsync.ESRCH);
|
||||
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,
|
||||
/// one spinning), collects both exit notifications, and confirms they are gone.
|
||||
/// Its "process-test: ok" is the pass marker; any FAIL line is specific.
|
||||
fn supervisionTest(boot_information: *const BootInformation) void {
|
||||
log("DANOS-TEST-BEGIN: supervision\n", .{});
|
||||
check("bootloader handed over an initial_ramdisk", boot_information.initial_ramdisk_len != 0);
|
||||
if (boot_information.initial_ramdisk_len == 0) {
|
||||
result();
|
||||
return;
|
||||
}
|
||||
const ramdisk = @as([*]const u8, @ptrFromInt(boot_handoff.physicalToVirtual(boot_information.initial_ramdisk_base)))[0..boot_information.initial_ramdisk_len];
|
||||
const rd = initial_ramdisk.Reader.init(ramdisk) orelse {
|
||||
check("initial_ramdisk image is valid", false);
|
||||
result();
|
||||
return;
|
||||
};
|
||||
process.setInitialRamdisk(ramdisk); // the supervisor system_spawns its children by name
|
||||
|
||||
process.write_count = 0;
|
||||
process.write_from_user = false;
|
||||
var started = false;
|
||||
var i: u32 = 0;
|
||||
while (i < rd.count) : (i += 1) {
|
||||
const item = rd.entry(i) orelse continue;
|
||||
if (!eql(item.name, "process-test")) continue;
|
||||
started = if (process.spawnProcess(item.blob, 4, &.{ "process-test", "run" })) true else |_| false;
|
||||
break;
|
||||
}
|
||||
check("process-test spawned as the user-space supervisor", started);
|
||||
|
||||
const marker = "process-test: ok";
|
||||
scheduler.setPriority(1);
|
||||
const deadline = architecture.millis() + 10000;
|
||||
while (architecture.millis() < deadline) {
|
||||
if (process.write_len >= marker.len and eql(process.write_buffer[0..marker.len], marker)) break;
|
||||
scheduler.yield();
|
||||
}
|
||||
scheduler.setPriority(4);
|
||||
|
||||
const ok = process.write_len >= marker.len and eql(process.write_buffer[0..marker.len], marker);
|
||||
if (!ok and process.write_len > 0) log("DANOS-SUPERVISION: got \"{s}\"\n", .{process.write_buffer[0..process.write_len]});
|
||||
check("the supervisor completed every step (spawn/list/kill/notify)", ok);
|
||||
check("it ran in user mode (CPL 3)", process.write_from_user);
|
||||
result();
|
||||
}
|
||||
|
||||
/// The initial_ramdisk path: the bootloader handed over an image bundling extra user
|
||||
/// binaries; parse it, spawn every program, and confirm one (the vfs stub)
|
||||
/// reaches ring 3 and heartbeats — proving the whole ferry-parse-spawn pipeline.
|
||||
|
||||
Reference in New Issue
Block a user