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:
Daniel Samson
2026-07-11 09:32:25 +01:00
parent a5fe63c1dd
commit d218d93f79
13 changed files with 949 additions and 68 deletions
+40 -5
View File
@@ -43,13 +43,15 @@ pub const SystemCall = enum(u64) {
irq_bind = 14, // irq_bind(id, resource_index, endpoint): deliver a device IRQ as an IPC notification
irq_ack = 15, // irq_ack(id, resource_index): re-arm a bound IRQ after servicing it
device_register = 16, // device_register(parent_id, descriptor) -> id: publish a child of a device you claimed
system_spawn = 17, // system_spawn(name_ptr, name_len) -> 0: start a named initial-ramdisk binary as a new ring-3 process
system_spawn = 17, // system_spawn(name_ptr, name_len, arguments_ptr, arguments_len, exit_endpoint) -> child process id: start a named initial-ramdisk binary as a new ring-3 process
dma_alloc = 18, // dma_alloc(len, flags) -> vaddr (rax), paddr (rdx): contiguous, pinned, uncacheable DMA memory
dma_free = 19, // dma_free(vaddr, len) -> 0: release a prior dma_alloc
msi_bind = 20, // msi_bind(device_id, endpoint) -> address (rax), data (rdx): a per-device MSI vector for a claimed device
io_read = 21, // io_read(device_id, resource_index, offset, width) -> value: read a port in a claimed device's io_port resource
io_write = 22, // io_write(device_id, resource_index, offset, width, value) -> 0: write a port in a claimed device's io_port resource
clock = 23, // clock() -> nanoseconds since boot: a monotonic time source (for timeouts/delays)
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
_,
};
@@ -67,12 +69,45 @@ pub const dma_write_combining: u64 = 2; // write-combining (framebuffers); needs
pub const dma_below_4g: u64 = 4; // physical address must fit 32 bits (legacy DMA engines)
/// Set in the badge returned by `ipc_reply_wait` when what arrived is an
/// **asynchronous notification** (today: a device interrupt bound with `irq_bind`)
/// rather than a message from a client. There is no payload and no reply owed; the
/// low bits carry the source, a GSI. Shared so the kernel's ISR and the driver's
/// event loop can't disagree about which bit means "the hardware spoke".
/// **asynchronous notification** (a device interrupt bound with `irq_bind`, or a
/// child-exit notice — see `notify_exit_bit`) rather than a message from a client.
/// There is no payload and no reply owed; the low bits carry the source. Shared so
/// the kernel's ISR and the driver's event loop can't disagree about which bit
/// means "the hardware spoke".
pub const notify_badge_bit: u64 = 1 << 63;
/// Set (alongside `notify_badge_bit`) in the badge of a **child-exit notification**:
/// posted to the endpoint a supervisor passed to `system_spawn` when that child ends
/// — by clean exit, by a fault, or by `process_kill`. The low bits carry the child's
/// process id, so one endpoint can supervise many children (and even share with IRQ
/// notifications, which never set this bit). The microkernel's SIGCHLD.
pub const notify_exit_bit: u64 = 1 << 62;
/// 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;
/// What a process is doing right now, as reported by `process_enumerate`. Crosses
/// the system_call boundary as `ProcessDescriptor.state`.
pub const ProcessState = enum(u32) {
ready = 0, // runnable, waiting for a core
running = 1, // executing on a core right now
blocked = 2, // waiting (sleeping, or blocked in IPC)
};
/// One `process_enumerate` entry — the kernel's view of a live task, kernel tasks
/// included (they carry an empty name and id 0 is the boot task). Fixed layout
/// (extern) because it crosses the kernel↔user boundary by memory copy, like
/// `DeviceDescriptor` in the device ABI.
pub const ProcessDescriptor = extern struct {
id: u32, // kernel-assigned process id; never reused (monotonic)
supervisor: u32, // id of the process that spawned it (0 = the kernel)
state: u32, // a ProcessState value
priority: u32,
name_length: u32,
name: [maximum_process_name]u8, // argv[0] at spawn; empty for kernel tasks
};
/// Well-known IPC service ids for the bootstrap name registry (create_ipc_endpoint +
/// ipc_register/ipc_lookup). Small integers, so no string interning is needed
/// during bring-up. The VFS server registers under `vfs`; clients look it up.