Time is a kernel concern in danos: the kernel owns the scheduling timer and already exposes monotonic time via the clock/sleep/timer_bind syscalls, so a userspace time service would be a redundant, slower path. This adds the generic runtime.time module over those syscalls, retires the two demonstration drivers, reorganizes the milestone docs, and makes the monotonic clock correct on Intel, AMD, and inside any VM. runtime.time (library/runtime/time.zig) - Instant/Duration interface: now, sleep, spin, after, monotonicNanos, available - a thin layer over system.clock/sleep/timerOnce; unit-tested arithmetic Remove the demo drivers hpet and bus (a teaching example belongs in the docs, not shipped in the tree) - system/drivers/ now holds only real drivers: pci-bus, ps2-bus, usb-xhci-bus - device-manager end-to-end test repointed to pci-bus (asserts on kernel state: the process table and the device tree, not a racy serial marker) - device_register containment moved to a new in-kernel `containment` test - the driver-model worked example moved inline into docs/drivers.md Reorganize milestone docs into topic docs - m17-m18 / m19-m20 / m21 plans dissolved into process-lifecycle, device-manager, discovery, and acpi docs; new docs/power.md and docs/timers.md; ~20 citations repointed; plan docs deleted TSC reliability (apic.zig, smp.zig, cpu.zig, kernel.zig) - check the invariant-TSC bit (CPUID 0x80000007 EDX[8]) on Intel and AMD - cross-core "warp" check at SMP bring-up, pairwise BSP<->AP as each core comes up - fall back to the HPET clocksource when the TSC is not invariant (a bare VM) or not synchronized (a warp), switched continuously so time never jumps - boot log reports the outcome; new tsc-sync test exercises the TSC + warp path Verified: zig build; zig build test; 60/60 QEMU cases (incl. new containment and tsc-sync).
193 lines
12 KiB
Zig
193 lines
12 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
|
|
_,
|
|
};
|
|
|
|
/// 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)
|
|
_,
|
|
};
|
|
|
|
/// 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);
|