//! 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 //! name-registry ids and notification bit. 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]] //! (system/devices/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 ipc_register = 7, // ipc_register(service_id, handle): publish an endpoint by well-known id ipc_lookup = 8, // ipc_lookup(service_id) -> handle: find a published endpoint 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) -> vaddr: map a claimed device's MMIO into this AS 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) -> 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 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 _, }; /// 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 self-termination (reserved: no abort path yet) 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) /// 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) 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. pub const ServiceId = enum(u32) { vfs = 1, input = 2, ps2_bus = 3, // the 8042 owner; child device drivers attach here for raw bytes device_manager = 4, // the tree, the matcher, the supervisor (docs/device-manager.md) _, }; /// 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);