Publish exit events to subscribers; the VFS releases dead clients' handles (M17.3)
process_subscribe adds an endpoint to a bounded, ref-counted subscriber table; every death posts the same badge encoding a supervisor's exit notification uses, equally late, so subscribers observe a fully-released child. A dying subscriber's own subscriptions are removed first — it never hears about itself. The VFS is the first subscriber: open handles now record their owner and are swept when the owner dies, because a service must never depend on clients cleaning up after themselves (docs/process-lifecycle.md). Proven by the vfs-client-death scenario.
This commit is contained in:
parent
2ebfb0c3b0
commit
d8c55c6f2f
|
|
@ -35,7 +35,10 @@ only when its definition of green holds.
|
|||
- [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)
|
||||
- [ ] **M17.3** — published exit events + VFS subscriber
|
||||
- [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)
|
||||
- [ ] **M17.4** — signals, timer notifications, `runtime.process`, the service harness
|
||||
- [ ] **merge** `feat/process-lifecycle` → main, push
|
||||
- [ ] **M18.1** — device-manager protocol: hello + restart policy (branch `feat/device-manager`)
|
||||
|
|
|
|||
|
|
@ -66,3 +66,13 @@ pub fn exitReason(id: u32) ?ExitReason {
|
|||
if (r > ~@as(usize, 0) - 4095) return null; // a wrapped -errno
|
||||
return @enumFromInt(r);
|
||||
}
|
||||
|
||||
/// 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;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -54,6 +54,7 @@ pub const SystemCall = enum(u64) {
|
|||
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
|
||||
_,
|
||||
};
|
||||
|
||||
|
|
|
|||
|
|
@ -201,6 +201,7 @@ fn system_call(state: *architecture.CpuState) void {
|
|||
.process_enumerate => systemProcessEnumerate(state),
|
||||
.process_kill => systemProcessKill(state),
|
||||
.process_exit_reason => systemProcessExitReason(state),
|
||||
.process_subscribe => systemProcessSubscribe(state),
|
||||
_ => fail(state),
|
||||
}
|
||||
}
|
||||
|
|
@ -574,6 +575,16 @@ fn releaseTaskResourcesLocked(t: *scheduler.Task) void {
|
|||
recordExitLocked(t);
|
||||
irq.releaseOwner(t.id);
|
||||
devices_broker.releaseAllOwnedBy(t.id);
|
||||
// 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;
|
||||
|
|
@ -583,6 +594,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;
|
||||
|
|
@ -690,6 +707,34 @@ pub fn exitReasonOf(caller_id: u32, target_id: u32) i64 {
|
|||
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);
|
||||
}
|
||||
|
||||
fn systemProcessExitReason(state: *architecture.CpuState) void {
|
||||
const t = scheduler.current();
|
||||
if (t.aspace == 0) return fail(state);
|
||||
|
|
|
|||
|
|
@ -134,6 +134,8 @@ pub fn run(case: []const u8, boot_information: *const BootInformation) void {
|
|||
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, "initial-ramdisk")) {
|
||||
initialRamdiskTest(boot_information);
|
||||
} else if (eql(case, "vfs")) {
|
||||
|
|
@ -1535,6 +1537,74 @@ fn claimReleaseTest(boot_information: *const BootInformation) void {
|
|||
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();
|
||||
}
|
||||
|
||||
/// 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,
|
||||
|
|
|
|||
|
|
@ -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 }, &.{});
|
||||
}
|
||||
}
|
||||
|
|
@ -122,6 +147,12 @@ pub fn main() void {
|
|||
_ = runtime.system.write("vfs: register failed\n");
|
||||
return;
|
||||
}
|
||||
// First subscriber of the published exit events (docs/process-lifecycle.md):
|
||||
// when a client dies holding open handles, the notification below is how the
|
||||
// VFS learns to release them.
|
||||
if (!runtime.process.subscribeExits(endpoint)) {
|
||||
_ = runtime.system.write("vfs: exit subscription failed\n");
|
||||
}
|
||||
_ = runtime.system.write("vfs: ready\n");
|
||||
|
||||
var reply_buffer: [protocol.message_maximum]u8 = undefined;
|
||||
|
|
@ -129,8 +160,17 @@ pub fn main() void {
|
|||
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);
|
||||
if (got.isChildExit()) {
|
||||
// A published exit event: a process died; drop whatever it held.
|
||||
releaseClientHandles(got.childProcessId());
|
||||
reply_len = 0;
|
||||
continue;
|
||||
}
|
||||
if (got.isNotification()) {
|
||||
reply_len = 0; // not a request; nothing owed
|
||||
continue;
|
||||
}
|
||||
reply_len = handle(receive[0..got.len], &reply_buffer, got.senderTaskId());
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -246,6 +246,12 @@ CASES = [
|
|||
"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"},
|
||||
# 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