310 lines
21 KiB
Zig
310 lines
21 KiB
Zig
//! The **private kernel ↔ runtime** ABI: the raw system_call contract — the call
|
|
//! numbers, `mmap` protection flags, the page size those calls work in, and the IPC
|
|
//! notification bits. Shared by the kernel dispatcher
|
|
//! (system/kernel/process.zig) and the user-space runtime library (library/runtime/),
|
|
//! so the two can never drift.
|
|
//!
|
|
//! **Application code does not speak this.** danos programs call the `runtime` library —
|
|
//! the stable, danos-native ABI — and the runtime is the one thing that issues the
|
|
//! actual system calls (POSIX code layers over the runtime, never on this directly). It
|
|
//! is the same split as libSystem on macOS or win32 over the NT syscalls: the numbers
|
|
//! here are an implementation detail the runtime hides and may renumber, not a public
|
|
//! interface. See docs/coding-standards.md and library/runtime/.
|
|
//!
|
|
//! This is the *core* contract; the device half — `DeviceDescriptor` and friends, which
|
|
//! also cross this boundary — lives with the device sub-project as [[device-abi]]
|
|
//! (library/device/model/device-abi.zig). The loader↔kernel handoff is [[boot-handoff]].
|
|
|
|
/// Page size every `mmap`/`munmap` grant and the boot memory map are measured in.
|
|
/// 4 KiB on every architecture danos targets so far. Part of the ABI because the
|
|
/// runtime aligns to it (grants are page-granular) and the kernel guarantees it.
|
|
pub const page_size = 4096;
|
|
|
|
/// The kernel system_call numbers — the single source of truth shared by the kernel
|
|
/// dispatcher (system/kernel/process.zig) and the user runtime library, so the two
|
|
/// can never drift. The set is deliberately microkernel-minimal: file/device I/O
|
|
/// is not here — it lives in user-space servers reached through the IPC calls.
|
|
/// The table grows one milestone at a time; see docs/syscall.md.
|
|
pub const SystemCall = enum(u64) {
|
|
exit = 0, // exit(code): end the calling process
|
|
yield = 1, // yield(): give up the rest of this quantum
|
|
debug_write = 2, // debug_write(ptr, len): raw bytes to the kernel log (bring-up only)
|
|
sleep = 3, // sleep(ms): block the caller for ms milliseconds
|
|
mmap = 4, // mmap(len, prot) -> base: grant zeroed, page-aligned user pages
|
|
munmap = 5, // munmap(base, len): release pages from a prior mmap
|
|
create_ipc_endpoint = 6, // create_ipc_endpoint() -> handle: a new IPC endpoint
|
|
// 7 and 8 were ipc_register/ipc_lookup — the flat ServiceId name registry,
|
|
// retired with the protocol namespace (docs/os-development/protocol-namespace.md).
|
|
// A service now binds its name at the registry (init, over /protocol) and a
|
|
// client resolves and opens that path; neither is a system call any more. The
|
|
// numbers stay vacant rather than being reused: every other entry is
|
|
// position-fixed by an explicit value, so a hole costs nothing and a reused
|
|
// number would silently mean two things across a rebuild boundary.
|
|
ipc_call = 9, // ipc_call(h, message, len, reply, cap) -> reply_len: send + block for reply
|
|
ipc_reply_wait = 10, // ipc_reply_wait(h, reply, len, receive, cap) -> receive_len (+badge in rdx)
|
|
device_enumerate = 11, // device_enumerate(buffer, maximum) -> count: snapshot the device table
|
|
device_claim = 12, // device_claim(id) -> ok: take exclusive ownership of a device
|
|
mmio_map = 13, // mmio_map(id, resource_index) -> virtual_address: map a claimed device's MMIO into this address space
|
|
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, 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) -> virtual_address (rax), physical_address (rdx): contiguous, pinned, uncacheable DMA memory
|
|
dma_free = 19, // dma_free(virtual_address, 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
|
|
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
|
|
signal_bind = 29, // signal_bind(endpoint) -> 0/-errno: nominate the endpoint this process's signals arrive on
|
|
process_signal = 30, // process_signal(id, signal) -> 0/-errno: post a signal to a child (or to yourself)
|
|
timer_bind = 31, // timer_bind(endpoint, ms) -> 0/-errno: one-shot timer — posts a notification when ms elapse
|
|
klog_read = 32, // klog_read(offset, ptr, len) -> bytes copied: copy tagged log-ring stream bytes from `offset` out to a user buffer; fails once `offset` falls behind the ring's tail (re-sync via klog_status)
|
|
wall_clock = 33, // wall_clock() -> Unix epoch seconds (UTC): the RTC wall-clock time, for filesystem timestamps (mtime). Monotonic time is `clock`.
|
|
shared_memory_create = 34, // shared_memory_create(len) -> virtual_address (rax), handle (rdx): a shareable, zeroed, cacheable RAM region mapped into this AS; the handle is a capability passed to another process as an ipc_call send_cap (docs/display-v2.md)
|
|
shared_memory_map = 35, // shared_memory_map(cap) -> virtual_address: map the shared region named by a received capability into this address space (the same physical pages the creator sees)
|
|
shared_memory_physical = 36, // shared_memory_physical(cap) -> physical_address: the guest-physical base of a shared region held by capability, so a driver can program it into a device (e.g. virtio-gpu attach_backing); the pages are contiguous (docs/display-v2.md)
|
|
thread_spawn = 37, // thread_spawn(entry, stack_top, arg, exit_endpoint) -> tid: start a task sharing the caller's address space at `entry` on `stack_top`, `arg` in rdi; exit_endpoint (a handle, or no_cap) is notified when it ends — how join waits (docs/threading.md)
|
|
thread_exit = 38, // thread_exit(): end the calling thread, dropping one reference to its address space (destroyed on the last)
|
|
current_core = 39, // current_core() -> index: the dense 0-based index of the core the caller is running on (for parallelism/affinity introspection)
|
|
futex_wait = 40, // futex_wait(addr, expected, timeout_ns) -> status: if *addr == expected, block until woken or the timeout; returns futex_woken/mismatch/timed_out (docs/threading.md)
|
|
futex_wake = 41, // futex_wake(addr, count) -> woken: wake up to `count` tasks blocked in futex_wait on `addr` in this address space
|
|
thread_self = 42, // thread_self() -> tid: the calling thread's kernel task id (runtime.Thread.getCurrentId)
|
|
thread_join = 43, // thread_join(tid) -> 0: block until the thread with id `tid` has exited (runtime.Thread.join; no per-thread IPC endpoint) (docs/threading.md)
|
|
set_thread_pointer = 44, // set_thread_pointer(addr) -> 0: set the caller's thread pointer (user-space TLS base; x86_64 IA32_FS_BASE, aarch64 TPIDR_EL0); restored per task across context switches (docs/threading-plan.md M10)
|
|
klog_status = 45, // klog_status(ptr) -> 0: copy a KlogStatus (ring cursors + the boot wall-clock anchor) out to a user buffer
|
|
fs_resolve = 46, // fs_resolve(path_ptr, path_len, flags, out_ptr, out_cap) -> route tag (rax: fs_route_*) + node token or backend handle (rdx); a backend resolve writes the rewritten mount-relative path into out (length in r8 via third result)
|
|
fs_node = 47, // fs_node(op, node_token, offset, buf_ptr, buf_len) -> bytes/0/-errno: read/status/readdir on a kernel-served node (op values mirror the vfs-protocol Operation numbers)
|
|
fs_mount = 48, // fs_mount(prefix_ptr, prefix_len, backend_handle, rewrite_ptr, rewrite_len) -> 0/-errno: mount a userspace filesystem's endpoint at an absolute prefix (possession of the handle is the capability)
|
|
fs_unmount = 49, // fs_unmount(prefix_ptr, prefix_len) -> 0/-errno: remove a backend mount
|
|
iommu_fault_drain = 50, // iommu_fault_drain() -> count: drain + log pending IOMMU translation faults (a diagnostic; the count of faults seen this call)
|
|
dma_bind = 51, // dma_bind(device_id, region_handle) -> 0/-errno: map a DMA-region capability into the claimed device's IOMMU domain (idempotent). The caller must own the device and hold the handle
|
|
dma_unbind = 52, // dma_unbind(device_id, region_handle) -> 0/-errno: unmap a previously bound region from the device's domain and invalidate
|
|
handle_close = 53, // handle_close(handle) -> 0/-errno: drop one capability handle and free its table slot (endpoints, shared-memory, DMA regions)
|
|
_,
|
|
};
|
|
|
|
/// `futex_wait` return codes (in rax).
|
|
pub const futex_woken: u64 = 0; // woken by a futex_wake
|
|
pub const futex_mismatch: u64 = 1; // *addr != expected on entry; the caller did not block
|
|
pub const futex_timed_out: u64 = 2; // the timeout elapsed before a wake
|
|
|
|
/// How a process ended — recorded by the kernel at death, queried by the
|
|
/// supervisor with `process_exit_reason`, and the input to its restart decision
|
|
/// (docs/process-lifecycle.md): a clean exit meant to stop, a fault wants a
|
|
/// restart with backoff, killed means the supervisor did it itself. The faults
|
|
/// mirror the CPU exceptions a ring-3 process can die of; they are exit reasons,
|
|
/// never delivered to the faulting process (recovery is restart, not a handler).
|
|
pub const ExitReason = enum(u8) {
|
|
exited = 0, // returned from main / called exit
|
|
aborted = 1, // deliberate FAILURE exit (exit with a nonzero code): supervisors restart these, unlike a clean .exited
|
|
segmentation_fault = 2, // page fault
|
|
illegal_instruction = 3, // invalid opcode
|
|
arithmetic_fault = 4, // divide error, x87 or SIMD fault
|
|
protection_fault = 5, // general protection fault
|
|
fault = 6, // any other CPU exception
|
|
killed = 7, // process_kill
|
|
};
|
|
|
|
/// The x86 MSI message address base (`0xFEE0_0000`): a device raises an MSI by writing
|
|
/// `data` to this address, which the Local APIC turns into an interrupt at the vector
|
|
/// in `data`. The kernel returns the concrete (address, data) from `msi_bind`; this is
|
|
/// the fixed prefix, exposed so a driver's config-space programming reads clearly.
|
|
pub const msi_address_base: u64 = 0xFEE0_0000;
|
|
|
|
/// `dma_alloc` flags. `coherent` (uncacheable) is the portable default; the others are
|
|
/// opt-in for specific hardware. `write_combining` needs PAT programming (not yet — it
|
|
/// currently falls back to coherent); see docs/driver-model.md (M14).
|
|
pub const dma_coherent: u64 = 1; // strong-uncacheable — the default, the only portable one
|
|
pub const dma_write_combining: u64 = 2; // write-combining (framebuffers); needs PAT
|
|
pub const dma_below_4g: u64 = 4; // physical address must fit 32 bits (legacy DMA engines)
|
|
pub const dma_shareable: u64 = 8; // return a capability handle (r8) so the region can be delegated + dma_bound
|
|
|
|
/// Set in the badge returned by `ipc_reply_wait` when what arrived is an
|
|
/// **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;
|
|
|
|
/// Set (alongside `notify_badge_bit`) in the badge of a **buffered message** — a payload
|
|
/// posted to an endpoint's async queue by `ipc_send`, delivered through `ipc_reply_wait`
|
|
/// like a notification (no reply owed) but carrying bytes in the receive buffer, not just
|
|
/// a badge. This is what distinguishes a payload-bearing async message from a bare IRQ /
|
|
/// child-exit notification (which sets neither this nor `notify_exit_bit`). The low bits
|
|
/// carry the sender's task id. The async counterpart of the synchronous `ipc_call`, for
|
|
/// broadcasts where a rendezvous is the wrong shape (the input service is the first user).
|
|
pub const notify_message_bit: u64 = 1 << 61;
|
|
|
|
/// Set (alongside `notify_badge_bit`) in the badge of a **signal notification** —
|
|
/// the process-lifecycle vocabulary of docs/process-lifecycle.md, delivered to the
|
|
/// endpoint the process nominated with `signal_bind`. The low bits carry the
|
|
/// coalesced pending mask (bit positions = `Signal` values): signals are
|
|
/// statements, not questions, and two pending terminates are one terminate.
|
|
pub const notify_signal_bit: u64 = 1 << 60;
|
|
|
|
/// Set (alongside `notify_badge_bit`) in the badge of a **timer notification** —
|
|
/// a one-shot `timer_bind` deadline landing. No payload bits: what to do when the
|
|
/// deadline fires is whatever the receiver armed it for (a stop-sequence
|
|
/// escalation, a restart backoff, an alarm).
|
|
pub const notify_timer_bit: u64 = 1 << 59;
|
|
|
|
/// The signal vocabulary (docs/process-lifecycle.md): POSIX's concepts, danos's
|
|
/// names, message delivery. The value is the bit position in the pending mask — a
|
|
/// private kernel/runtime detail, free to change while they ship together. Kill
|
|
/// is not here (it is `process_kill`, unhandleable by definition); faults are not
|
|
/// here (they are `ExitReason`s — recovery is restart, not a handler); liveness is
|
|
/// not here (a question, asked as the zero-length ping call, not a statement).
|
|
pub const Signal = enum(u5) {
|
|
terminate = 0, // finish up and exit (the polite half of the stop sequence)
|
|
reload = 1, // re-read configuration / re-scan
|
|
interrupt = 2, // interactive interrupt (no sender until a console exists)
|
|
quit = 3, // as interrupt, by convention more final
|
|
alarm = 4, // a timer the process armed for itself (unbuilt: no consumer yet)
|
|
user_1 = 5, // service-defined
|
|
user_2 = 6, // service-defined
|
|
};
|
|
|
|
/// 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)
|
|
leader: u32, // process-leader id: == id for a main task, the main task's id for a thread
|
|
state: u32, // a ProcessState value
|
|
priority: u32,
|
|
name_length: u32,
|
|
name: [maximum_process_name]u8, // argv[0] at spawn; empty for kernel tasks
|
|
};
|
|
|
|
// --- the tagged kernel log ring (klog) ---------------------------------------
|
|
// Every `debug_write` becomes one RECORD per payload line, stamped by the kernel
|
|
// with the sender's pid, task name (its binary path), level, a per-boot sequence
|
|
// number, and a monotonic timestamp. `klog_read` copies raw stream bytes — a
|
|
// reader parses [KlogRecordHeader][name][message] frames, each padded to
|
|
// `klog_record_alignment`. Sequence gaps tell a reader exactly how many records
|
|
// the ring overwrote while it wasn't looking.
|
|
|
|
/// Log level of a klog record — std.log's levels plus `raw` (untagged bytes:
|
|
/// kernel prints and legacy runtime.system.write output).
|
|
pub const KlogLevel = enum(u8) { err = 0, warn = 1, info = 2, debug = 3, raw = 4 };
|
|
|
|
/// "RK" — leads every record; a parser's resync/corruption guard.
|
|
pub const klog_record_magic: u16 = 0x4B52;
|
|
|
|
/// KlogRecordHeader.flags bit: the emitter truncated the payload to fit.
|
|
pub const klog_flag_truncated: u8 = 1;
|
|
|
|
/// Header of one ring record, followed by `name_len` bytes of task name and
|
|
/// `message_len` bytes of payload; the whole record is padded to 8 bytes.
|
|
pub const KlogRecordHeader = extern struct {
|
|
magic: u16, // klog_record_magic
|
|
level: KlogLevel,
|
|
name_len: u8, // 0..maximum_process_name
|
|
pid: u32, // sender process id; 0 = the kernel itself
|
|
sequence: u64, // per-boot monotonic record number (gaps = lost records)
|
|
timestamp_ns: u64, // monotonic ns since boot (the `clock` timebase)
|
|
message_len: u16, // payload bytes (excludes the record's trailing pad)
|
|
flags: u8, // klog_flag_* bits
|
|
_reserved: [5]u8,
|
|
};
|
|
|
|
pub const klog_record_header_size: usize = 32; // @sizeOf(KlogRecordHeader), pinned by a test
|
|
pub const klog_record_alignment: usize = 8;
|
|
/// Per-record payload cap (one line; longer emitter lines are truncated).
|
|
pub const klog_maximum_message: usize = 256;
|
|
|
|
// --- the kernel VFS root (resolve + redirect) --------------------------------
|
|
// fs_resolve routes a path through the kernel mount table. Kernel-backed mounts
|
|
// (the initrd at /system) resolve to a permanent node TOKEN served by fs_node;
|
|
// userspace mounts resolve to the backend's endpoint handle (installed in the
|
|
// caller's table, deduplicated) plus the rewritten mount-relative path — the
|
|
// caller then speaks the vfs-protocol to the backend directly. The kernel never
|
|
// blocks on a userspace filesystem.
|
|
|
|
/// fs_resolve result tags (rax).
|
|
pub const fs_route_kernel: u64 = 0; // rdx = node token; serve via fs_node
|
|
pub const fs_route_backend: u64 = 1; // rdx = endpoint handle; speak vfs-protocol
|
|
|
|
/// fs_node operations. These were once the vfs-protocol Operation numbers; the
|
|
/// rebase onto the envelope moved every protocol verb above the reserved range
|
|
/// (16 and up), and these did not follow — they are a *syscall* selector, not a
|
|
/// packet's verb, and renumbering a kernel ABI to track a wire format would be
|
|
/// coupling in the wrong direction. The two vocabularies are simply separate now.
|
|
pub const fs_node_read: u64 = 2;
|
|
pub const fs_node_status: u64 = 4;
|
|
pub const fs_node_readdir: u64 = 5;
|
|
|
|
/// fs_resolve flags (same values as the vfs-protocol open flags).
|
|
pub const fs_flag_create: u64 = 1;
|
|
|
|
/// FileStatus-shaped node metadata (matches the vfs-protocol payload layout).
|
|
pub const file_kind_regular: u32 = 0;
|
|
pub const file_kind_directory: u32 = 1;
|
|
pub const FileAttributes = extern struct {
|
|
size: u64,
|
|
kind: u32,
|
|
_pad: u32 = 0,
|
|
mtime: u64 = 0,
|
|
};
|
|
|
|
/// One fs_node readdir result: the header, followed by `name_len` name bytes in
|
|
/// the caller's buffer (matches the vfs-protocol DirectoryEntry layout).
|
|
pub const DirectoryEntryHeader = extern struct {
|
|
kind: u32,
|
|
name_len: u32,
|
|
size: u64,
|
|
};
|
|
|
|
/// The klog_status copy-out: the ring's live cursors plus the wall-clock time
|
|
/// of boot — the anchor a log persister names its per-boot directory with and
|
|
/// combines with record timestamps for wall-clock line stamps.
|
|
pub const KlogStatus = extern struct {
|
|
tail: u64, // oldest retained stream offset — always a record boundary
|
|
head: u64, // next byte to be written (end of stream)
|
|
next_sequence: u64, // the sequence the next record will get
|
|
boot_unix_seconds: u64, // wall-clock time of boot (RTC anchor)
|
|
};
|
|
|
|
// The `ServiceId` enum lived here: a flat, compile-time list of well-known
|
|
// service ids backed by a 16-slot kernel table. It is gone with the protocol
|
|
// namespace — names are strings resolved under `/protocol` at run time, so a
|
|
// third-party program can introduce a contract the ABI never heard of, and the
|
|
// registrar (init) decides who may claim one.
|
|
|
|
/// Protection flags for `mmap` (matching the usual C bit values).
|
|
pub const prot_read: u64 = 1;
|
|
pub const prot_write: u64 = 2;
|
|
pub const prot_exec: u64 = 4;
|
|
|
|
/// `send_cap` / `received_cap` sentinel meaning "no capability" on the `ipc_call` /
|
|
/// `ipc_reply_wait` cap-passing path (M13). `~0`, like `no_parent` — a real handle is
|
|
/// a small index, so it can never collide.
|
|
pub const no_cap: u64 = ~@as(u64, 0);
|