158 lines
7.1 KiB
Zig
158 lines
7.1 KiB
Zig
//! Typed system_call surface for user space — thin wrappers over the raw `system_call`
|
|
//! stubs, one per kernel call. Numbers come from `abi.SystemCall`, the single
|
|
//! source of truth shared with the kernel dispatcher.
|
|
|
|
const std = @import("std");
|
|
const abi = @import("abi");
|
|
const sc = @import("system-call.zig");
|
|
|
|
/// `mmap` protection flags (matching the usual C bit values). Grants are always
|
|
/// readable+writable today; the kernel does not yet honour finer prot.
|
|
pub const PROT_READ: usize = abi.prot_read;
|
|
pub const PROT_WRITE: usize = abi.prot_write;
|
|
pub const PROT_EXEC: usize = abi.prot_exec;
|
|
|
|
/// One `processes` entry — re-exported from the shared ABI so a user program can
|
|
/// declare its snapshot buffer without importing `abi` itself.
|
|
pub const ProcessDescriptor = abi.ProcessDescriptor;
|
|
|
|
/// Give up the rest of this quantum.
|
|
pub fn yield() void {
|
|
_ = sc.systemCall0(.yield);
|
|
}
|
|
|
|
/// Write raw bytes to the kernel log (a bring-up diagnostic; real output goes
|
|
/// through the console/VFS later). Returns the byte count, or a wrapped -1.
|
|
pub fn write(message: []const u8) usize {
|
|
return sc.systemCall2(.debug_write, @intFromPtr(message.ptr), message.len);
|
|
}
|
|
|
|
/// Block the caller for `ms` milliseconds.
|
|
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:
|
|
///
|
|
/// const deadline = clock() + timeout_ns;
|
|
/// while (clock() < deadline) { ... }
|
|
pub fn clock() u64 {
|
|
return @intCast(sc.systemCall0(.clock));
|
|
}
|
|
|
|
/// Copy bytes out of the kernel's in-memory diagnostic log — the accumulated
|
|
/// stream of everything `write` (and the kernel itself) has emitted — starting at
|
|
/// `offset`, into `out`. Returns the number of bytes copied (0 at end of buffer).
|
|
/// A program reads the whole log by looping from offset 0, advancing by the return
|
|
/// value, until it gets 0. This is how the boot log is persisted to disk on a
|
|
/// headless/real machine where serial output is otherwise lost.
|
|
pub fn klogRead(offset: usize, out: []u8) usize {
|
|
return sc.systemCall3(.klog_read, offset, @intFromPtr(out.ptr), out.len);
|
|
}
|
|
|
|
/// End the process. Never returns.
|
|
pub fn exit(code: usize) noreturn {
|
|
_ = sc.systemCall1(.exit, code);
|
|
unreachable; // the kernel never returns from exit
|
|
}
|
|
|
|
/// Start the binary bundled in the initial-ramdisk under `name` as a new ring-3
|
|
/// process, returning the child's process id (or null on failure). The child's
|
|
/// argv[0] is `name`, and the caller becomes its **supervisor** — the only process
|
|
/// allowed to `kill` it. This is how a supervisor (the device manager) launches a
|
|
/// driver it matched — danos-native, not POSIX (a spawn/exec family comes with the
|
|
/// POSIX layer later).
|
|
pub fn spawn(name: []const u8) ?u32 {
|
|
return spawnSupervised(name, &.{}, null);
|
|
}
|
|
|
|
/// Like `spawn`, but hands the child command-line arguments: they arrive as
|
|
/// argv[1..] on its System V entry stack (argv[0] is still `name`).
|
|
pub fn spawnWithArguments(name: []const u8, arguments: []const []const u8) ?u32 {
|
|
return spawnSupervised(name, arguments, null);
|
|
}
|
|
|
|
/// The full spawn: command-line arguments for the child, and an optional endpoint
|
|
/// (a handle from `ipc.createIpcEndpoint`) the kernel notifies when the child ends
|
|
/// — any way it ends: clean exit, fault, or `kill`. The notification arrives via
|
|
/// `ipc.replyWait` as a badge with the child-exit bit set and the child's id in
|
|
/// the low bits (`ipc.Received.isChildExit`/`childProcessId`), so one endpoint can
|
|
/// supervise many children. Arguments are marshalled to the kernel as one
|
|
/// NUL-separated blob; the combined arguments must fit `blob` (the kernel caps the
|
|
/// blob at 256 bytes and argc at 8 anyway). Returns the child's process id, or
|
|
/// null on failure.
|
|
pub fn spawnSupervised(name: []const u8, arguments: []const []const u8, exit_endpoint: ?usize) ?u32 {
|
|
var blob: [256]u8 = undefined;
|
|
var len: usize = 0;
|
|
for (arguments, 0..) |argument, i| {
|
|
if (i != 0) {
|
|
if (len >= blob.len) return null;
|
|
blob[len] = 0;
|
|
len += 1;
|
|
}
|
|
if (len + argument.len > blob.len) return null;
|
|
@memcpy(blob[len..][0..argument.len], argument);
|
|
len += argument.len;
|
|
}
|
|
const r = sc.systemCall5(.system_spawn, @intFromPtr(name.ptr), name.len, if (len == 0) 0 else @intFromPtr(&blob), len, exit_endpoint orelse abi.no_cap);
|
|
if (r > ~@as(usize, 0) - 4095) return null; // a wrapped -errno
|
|
return @intCast(r);
|
|
}
|
|
|
|
/// Snapshot the process table into `out` (up to its length) and return the total
|
|
/// number of live processes — which may exceed `out.len`; call again with a larger
|
|
/// buffer for the full listing. Kernel tasks are included, with an empty name.
|
|
/// The primitive `ps` is built on.
|
|
pub fn processes(out: []abi.ProcessDescriptor) usize {
|
|
return sc.systemCall2(.process_enumerate, @intFromPtr(out.ptr), out.len);
|
|
}
|
|
|
|
/// Whether a process spawned under `name` (its argv[0]) is currently alive.
|
|
pub fn isProcessRunning(name: []const u8) bool {
|
|
var table: [32]ProcessDescriptor = undefined;
|
|
const total = processes(&table);
|
|
for (table[0..@min(total, table.len)]) |descriptor| {
|
|
if (std.mem.eql(u8, descriptor.name[0..descriptor.name_length], name)) return true;
|
|
}
|
|
return false;
|
|
}
|
|
|
|
/// End process `id`. Only its supervisor — the process that spawned it — may;
|
|
/// anyone else gets false, as does a stale or unknown id (ids are never reused).
|
|
/// Delivery is prompt but asynchronous, like a signal: a target caught running on
|
|
/// another core dies at its next system call or timer tick. True means the kill
|
|
/// is accepted and irrevocable; the exit notification (if an endpoint was given
|
|
/// at spawn) confirms completion.
|
|
pub fn kill(id: u32) bool {
|
|
return sc.systemCall1(.process_kill, id) == 0;
|
|
}
|
|
|
|
/// Grant `len` bytes (rounded up to whole pages) of fresh, zeroed, writable
|
|
/// memory and return the base virtual address. On failure returns a value in the
|
|
/// top page (see `mmapFailed`). The user heap grows through this call.
|
|
pub fn mmap(len: usize, prot: usize) usize {
|
|
return sc.systemCall2(.mmap, len, prot);
|
|
}
|
|
|
|
/// Release a range previously handed out by `mmap`.
|
|
pub fn munmap(base: usize, len: usize) usize {
|
|
return sc.systemCall2(.munmap, base, len);
|
|
}
|
|
|
|
/// Whether an `mmap` return value is an error (the kernel returns a wrapped
|
|
/// -errno, which lands in the top page — no real grant base is ever that high).
|
|
pub inline fn mmapFailed(ret: usize) bool {
|
|
return ret > ~@as(usize, 0) - 4095;
|
|
}
|