join no longer needs a per-thread IPC endpoint. New thread_join(tid) syscall blocks the caller until the task with id tid exits; the exit paths call wakeJoinersLocked. join only reclaims the joined thread's USER stack, which the thread vacates the instant it enters the kernel to exit, so waking at exit time (not reap time) is safe — no reaper/aspace juggling or user-memory write, and equally std-shaped (like pthread_join). thread_spawn drops the exit-endpoint arg (runtime passes no_cap). thread-test's join mode runs 40 spawn+join cycles that would exhaust the 16-slot handle table under the old endpoint scheme. Also harden the M8 reaper: its single per-core reap slot could be overwritten by a second death on that core before draining (a fresh-task/SMP timing window), an intermittent one-stack leak that made task-reap ~20% flaky. Replace it with a per-core reap LIST plus a .reaping task state so a pending slot can't be reused before its stack is freed. task-reap now 11/11 isolated. Deferred: detached-thread user-stack reclaim (still at process exit, as in M3). Gate thread-join PASS (3x); full guardrail 26/26; build + host tests clean.
216 lines
16 KiB
Zig
216 lines
16 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
|
|
//! 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
|
|
klog_read = 32, // klog_read(offset, ptr, len) -> bytes copied: copy the kernel RAM log buffer out to a user buffer (for persisting the boot log to disk)
|
|
wall_clock = 33, // wall_clock() -> Unix epoch seconds (UTC): the RTC wall-clock time, for filesystem timestamps (mtime). Monotonic time is `clock`.
|
|
shm_create = 34, // shm_create(len) -> vaddr (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)
|
|
shm_map = 35, // shm_map(cap) -> vaddr: map the shared region named by a received capability into this AS (the same physical pages the creator sees)
|
|
shm_physical = 36, // shm_physical(cap) -> paddr: 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)
|
|
_,
|
|
};
|
|
|
|
/// `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 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)
|
|
power = 5, // system power: events (button, lid, battery) + shutdown (docs/power.md; domain-named per docs/discovery.md — the acpi service registers it on x86, a PSCI service will on ARM)
|
|
usb_bus = 6, // the xHCI host-controller driver's transfer endpoint; USB class drivers look it up and `callCap`-open their device to get a private per-device transfer channel (docs/driver-model.md)
|
|
block = 7, // a block-device driver (USB mass storage today): read/write of fixed-size blocks, the storage a filesystem sits on
|
|
fat = 8, // the FAT filesystem server; the VFS mounts it and forwards paths under its mount point (/mnt/usb) to it
|
|
display = 9, // the display service: owns the framebuffer, composites a layer stack, presents frames (docs/display.md)
|
|
shm_test = 10, // the shm test server (V2): a client passes it a shared-memory capability, it maps + verifies (docs/display-v2.md)
|
|
scanout = 11, // a native scanout driver (virtio-gpu): the compositor finds it here to upgrade off the GOP framebuffer (docs/display-v2.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);
|