Userspace Developer's Handbook
+
+ ZenithOS is a hobbyist 64-bit operating system written in C++20.
+ Userspace programs run in Ring 3, are loaded as static ELF64
+ binaries, and communicate with the kernel through the x86-64
+ SYSCALL/SYSRET mechanism.
+
+ This document covers everything you need to write, build, and run + userspace applications — from a minimal "Hello World" to using + every available syscall — as well as how to extend the kernel + with new syscalls. +
+ + +Getting Started
+ +Project Structure
+programs/
+├── GNUmakefile # Build system (C++ programs)
+├── link.ld # Linker script (base address 0x400000)
+├── include/
+│ ├── Api/
+│ │ └── Syscall.hpp # Syscall numbers & data structures
+│ ├── zenith/
+│ │ ├── syscall.h # Inline asm wrappers & typed API
+│ │ └── heap.h # Userspace heap allocator (malloc/mfree/realloc)
+│ └── libc/ # Minimal C standard library headers
+│ ├── stdio.h # printf, FILE I/O
+│ ├── stdlib.h # malloc, free, atoi, exit
+│ ├── string.h # memcpy, strlen, strcmp, etc.
+│ └── ... # ctype.h, errno.h, assert.h, etc.
+├── src/
+│ ├── hello/
+│ │ └── main.cpp # Hello world example
+│ ├── shell/
+│ │ └── main.cpp # Interactive shell
+│ ├── man/
+│ │ └── main.cpp # Manual page viewer
+│ └── doom/
+│ ├── Makefile # DOOM build system
+│ ├── doomgeneric_zenith.c # ZenithOS platform layer
+│ └── libc.c # C library implementation
+├── bin/ # Compiled .elf binaries
+└── obj/ # Intermediate object files
+
+ Each subdirectory under src/ is treated as a separate
+ program. The build system discovers them automatically.
+
Toolchain
+
+ Programs are compiled with a freestanding x86_64-elf
+ cross-compiler. The build system looks for one in
+ toolchain/local/bin/; if not found, it falls back to the
+ host g++.
+
Key compiler flags:
+-
+
-std=gnu++20— C++20 with GNU extensions
+ -ffreestanding -nostdinc -nostdlib— no hosted standard library
+ -fno-rtti -fno-exceptions— no RTTI or C++ exceptions
+ -fno-PIC -mcmodel=small— static absolute addressing
+ -mno-sse -mno-mmx -mno-red-zone— kernel-safe ABI (C++ programs)
+
float/double)
+ should compile with -msse -msse2 instead. The kernel enables SSE in
+ CR0/CR4 at boot, so SSE instructions are safe in userspace.
+Build System
+To build all programs:
+cd programs/
+make # or: make -j$(nproc)
+
+ This produces one ELF binary per program in bin/
+ (e.g. bin/hello.elf, bin/shell.elf).
+ The binaries are then typically packed into the ramdisk
+ (ramdisk.tar) and loaded by the kernel at boot.
+
To add a new program, create src/<name>/main.cpp and run make.
Linker Script
+programs/link.ld
+
+ All userspace programs are linked at virtual address
+ 0x400000. The linker script defines four standard
+ sections:
+
| Section | Contents | Alignment |
|---|---|---|
.text | Executable code | — (base) |
.rodata | Read-only data, string literals | 4 KiB |
.data | Initialized read/write data | 4 KiB |
.bss | Zero-initialized data | — |
+ Debug frames (.eh_frame), notes, and comments are
+ discarded to keep binaries small.
+
Program Anatomy
+ +Entry Point: _start
+
+ Because there is no C runtime, every program must define
+ extern "C" void _start() as its entry point. There is no
+ main(), no argc/argv, and no
+ atexit handlers.
+
_start() returns normally,
+ the kernel's exit stub (mapped at 0x3FF000) automatically
+ calls SYS_EXIT(0). You can also call
+ zenith::exit(code) explicitly at any point.
+Hello World
+programs/src/hello/main.cpp
+// Minimal ZenithOS userspace program
+#include <zenith/syscall.h>
+
+extern "C" void _start() {
+ zenith::print("Hello from userspace!\n");
+}
+
+ Include <zenith/syscall.h> for the full typed API.
+ That header pulls in <Api/Syscall.hpp> for
+ constants and data structures.
+
Syscall Architecture
+ +Calling Convention
+
+ ZenithOS uses the hardware SYSCALL instruction on
+ x86-64. The kernel sets up the required MSRs
+ (IA32_STAR, IA32_LSTAR,
+ IA32_FMASK) during boot.
+
| Register | Purpose |
|---|---|
RAX | Syscall number (in) / return value (out) |
RDI | Argument 1 |
RSI | Argument 2 |
RDX | Argument 3 |
R10 | Argument 4 (not RCX — SYSCALL clobbers it) |
R8 | Argument 5 |
R9 | Argument 6 |
+ The SYSCALL instruction saves RIP in
+ RCX and RFLAGS in R11; both
+ registers are clobbered. The kernel masks IF on entry
+ (via IA32_FMASK) so interrupts are disabled during the
+ transition.
+
+ On the kernel side, SyscallEntry (assembly) saves
+ callee-saved registers and all arguments into a
+ SyscallFrame, then calls
+ SyscallDispatch(SyscallFrame*) which dispatches by
+ syscall number.
+
Raw Syscall Wrappers
+programs/include/zenith/syscall.h
++ Seven inline functions cover 0–6 argument syscalls: +
+int64_t syscall0(uint64_t nr);
+int64_t syscall1(uint64_t nr, uint64_t a1);
+int64_t syscall2(uint64_t nr, uint64_t a1, uint64_t a2);
+int64_t syscall3(uint64_t nr, uint64_t a1, uint64_t a2, uint64_t a3);
+int64_t syscall4(uint64_t nr, uint64_t a1, uint64_t a2, uint64_t a3, uint64_t a4);
+int64_t syscall5(uint64_t nr, uint64_t a1, uint64_t a2, uint64_t a3, uint64_t a4, uint64_t a5);
+int64_t syscall6(uint64_t nr, uint64_t a1, ..., uint64_t a6);
++ Each wrapper uses inline assembly with explicit register moves and a + full clobber list to ensure correctness. You can use these directly if + you need a syscall not yet covered by the typed API. +
+ +Typed API Wrappers
+
+ The zenith:: namespace provides type-safe wrappers around
+ the raw syscalls. These are the recommended interface for application
+ code. The full reference follows below.
+
Syscall Reference
++ ZenithOS v0.1.0 exposes 26 syscalls (numbers + 0–25), organized into 10 categories. +
+ + +Process Process Management
+ +SYS_EXIT (0) — Terminate the current process
+[[noreturn]] void zenith::exit(int code = 0);
++ Terminates the calling process. The exit code is currently unused by + the kernel but reserved for future wait/status support. + Control never returns to the caller. +
+ +SYS_YIELD (1) — Yield the CPU
+void zenith::yield();
++ Voluntarily yields the remainder of the current time slice, allowing + the scheduler to run another ready process immediately. +
+ +SYS_SLEEP_MS (2) — Sleep for a duration
+void zenith::sleep_ms(uint64_t ms);
+
+ Suspends the calling process for at least ms
+ milliseconds. Resolution depends on the APIC timer tick rate.
+
SYS_GETPID (3) — Get process ID
+int zenith::getpid();
+
+ Returns the PID of the calling process. Returns -1 if
+ called from the idle context (should not happen in userspace).
+
SYS_SPAWN (20) — Spawn a new process
+int zenith::spawn(const char* path, const char* args = nullptr);
+
+ Loads the ELF binary at path (a VFS path like
+ "0:/hello.elf") and spawns it as a new process.
+ Returns the new process's PID on success, or -1 on
+ failure (no free slots, invalid ELF, file not found).
+ The optional args string (up to 255 characters) is
+ copied into the new process and can be retrieved with
+ SYS_GETARGS.
+
SYS_WAITPID (23) — Wait for a process to exit
+void zenith::waitpid(int pid);
+
+ Blocks the calling process until the process identified by
+ pid has exited. Internally yields the CPU in a loop
+ until the target process is no longer alive. This is used by the
+ shell to wait for foreground processes (e.g. run
+ command) so that keyboard input and the terminal are not shared
+ simultaneously.
+
SYS_GETARGS (25) — Get process arguments
+int zenith::getargs(char* buf, uint64_t maxLen);
+
+ Copies the argument string passed to the current process (via
+ spawn()) into buf, writing at most
+ maxLen - 1 characters plus a null terminator.
+ Returns the number of characters copied, or -1 on error.
+ If no arguments were provided at spawn time, the buffer will be empty.
+
Console Console I/O
+ +SYS_PRINT (4) — Print a string
+void zenith::print(const char* text);
+
+ Writes a null-terminated string to the kernel terminal. Supports
+ newlines (\n) and standard printable ASCII.
+
SYS_PUTCHAR (5) — Print a single character
+void zenith::putchar(char c);
++ Writes a single character to the kernel terminal. Useful for building + output character by character (e.g. printing integers). +
+ + +File I/O File I/O
+ +SYS_OPEN (6) — Open a file
+int zenith::open(const char* path);
+
+ Opens a file on the VFS. Paths use the format
+ "<device>:/<name>" (e.g. "0:/hello.elf"
+ for the ramdisk). Returns a non-negative handle on success, or a
+ negative value on error.
+
SYS_READ (7) — Read from a file
+int zenith::read(int handle, uint8_t* buf, uint64_t offset, uint64_t size);
+
+ Reads up to size bytes from the file at the given byte
+ offset into buf. Returns the number of bytes
+ actually read, or a negative value on error. Does not maintain an
+ implicit file position — the offset is explicit on every call.
+
SYS_GETSIZE (8) — Get file size
+uint64_t zenith::getsize(int handle);
+Returns the total size (in bytes) of the file associated with handle.
SYS_CLOSE (9) — Close a file
+void zenith::close(int handle);
+Closes the file handle and releases associated kernel resources.
+ +SYS_READDIR (10) — List directory entries
+int zenith::readdir(const char* path, const char** names, int max);
+
+ Reads up to max directory entries from path.
+ Entry name pointers are written into the names array.
+ The kernel allocates a user-accessible page for the string data
+ automatically. Returns the number of entries read, or
+ ≤ 0 on error/empty directory. Maximum 64 entries per call.
+
Memory Memory Management
+ +
+ Memory allocation has two layers: low-level syscalls
+ that map pages from the kernel, and a userspace heap
+ that provides malloc/mfree on top.
+
Userspace Heap (recommended)
+programs/include/zenith/heap.h
+
+ Include <zenith/heap.h> for a proper free-list
+ allocator that runs entirely in userspace. It calls
+ SYS_ALLOC internally to obtain pages and manages
+ sub-page allocations with a linked free list — adapted from
+ the kernel's own HeapAllocator.
+
void* zenith::malloc(uint64_t size);
+
+ Allocates size bytes from the userspace free list.
+ Returns a 16-byte-aligned pointer, or nullptr on failure.
+ When the free list is exhausted, it transparently requests more pages
+ from the kernel via SYS_ALLOC (minimum 16 KiB growth).
+
void zenith::mfree(void* ptr);
+
+ Returns the block to the userspace free list.
+ No syscall is made — the memory stays mapped
+ and is immediately available for future malloc calls.
+ Passing nullptr is a safe no-op.
+
void* zenith::realloc(void* ptr, uint64_t size);
+
+ Resizes the allocation at ptr to size bytes.
+ Allocates a new block, copies the smaller of old/new sizes, and frees
+ the old block. If ptr is nullptr, behaves
+ like malloc.
+
Low-Level Page Syscalls
+ +SYS_ALLOC (11) — Map pages
+void* zenith::alloc(uint64_t size);
+
+ Maps size bytes of zeroed physical pages into the
+ process's address space (starting at 0x40000000).
+ The size is rounded up to the nearest page boundary (4 KiB).
+ Returns a pointer to the mapped region, or nullptr on
+ failure. This is the backing primitive for
+ zenith::malloc — most programs should use the heap
+ API instead of calling this directly.
+
SYS_FREE (12) — Unmap pages (no-op)
+void zenith::free(void* ptr);
+
+ Reserved for future page-level unmapping. Currently a no-op —
+ pages are reclaimed when the process exits. Use
+ zenith::mfree for heap allocations.
+
Time Timekeeping
+ +SYS_GETTICKS (13) — Get tick count
+uint64_t zenith::get_ticks();
++ Returns the number of APIC timer ticks since boot. The tick rate + depends on the hardware and APIC timer calibration. +
+ +SYS_GETMILLISECONDS (14) — Get milliseconds since boot
+uint64_t zenith::get_milliseconds();
++ Returns wall-clock milliseconds elapsed since boot. Useful for + calculating uptime or measuring durations. +
+ + +System System Information
+ +SYS_GETINFO (15) — Get OS information
+void zenith::get_info(Zenith::SysInfo* info);
+
+ Fills in a SysInfo structure
+ with the OS name, version string, API version number, and maximum
+ process count.
+
Keyboard Keyboard Input
+ +SYS_ISKEYAVAILABLE (16) — Check for pending key
+bool zenith::is_key_available();
+
+ Returns true if a key event is available in the PS/2
+ keyboard buffer. Non-blocking.
+
SYS_GETKEY (17) — Get a key event
+void zenith::getkey(Zenith::KeyEvent* out);
+
+ Fills in a KeyEvent
+ structure with the next keyboard event (press or release), including
+ scancode, ASCII translation, and modifier state (Shift, Ctrl, Alt).
+
SYS_GETCHAR (18) — Read a character (blocking)
+char zenith::getchar();
++ Blocks until a printable character key-press is available, then + returns the ASCII character. This is the simplest way to read + interactive text input. +
+ + +Network Networking
+ +SYS_PING (19) — Send an ICMP echo request
+int32_t zenith::ping(uint32_t ip, uint32_t timeoutMs = 3000);
+
+ Sends an ICMP echo request to ip and waits up to
+ timeoutMs milliseconds for a reply. The IP address is in
+ little-endian byte order (e.g. 10.0.2.2 →
+ 0x0202000A).
+ Returns the round-trip time in milliseconds on success, or
+ -1 on timeout.
+
Framebuffer Framebuffer Access
+ +SYS_FBINFO (21) — Get framebuffer information
+void zenith::fb_info(Zenith::FbInfo* info);
+
+ Fills in a FbInfo structure
+ with the framebuffer dimensions, pitch (bytes per scanline), and
+ bits per pixel. Call this before SYS_FBMAP to learn the
+ framebuffer geometry.
+
SYS_FBMAP (22) — Map framebuffer into process memory
+void* zenith::fb_map();
+
+ Maps the physical framebuffer into the calling process's address
+ space at 0x50000000 and returns the user virtual address.
+ The mapped region covers height × pitch bytes. Each
+ pixel is a 32-bit value in 0xAARRGGBB format (blue in
+ the low byte). Writing to this memory directly updates the screen.
+
Terminal Terminal
+ +SYS_TERMSIZE (24) — Get terminal dimensions
+void zenith::termsize(int* cols, int* rows);
+
+ Returns the current terminal dimensions (character grid) via the
+ two output pointers. Columns are packed in the low 32 bits and
+ rows in the high 32 bits of the raw return value; the typed
+ wrapper unpacks them for you. Either pointer may be
+ nullptr if you only need one dimension.
+
Data Structures
+ +Zenith::SysInfo
+programs/include/Api/Syscall.hpp
+struct SysInfo {
+ char osName[32]; // e.g. "ZenithOS"
+ char osVersion[32]; // e.g. "0.1.0"
+ uint32_t apiVersion; // Current: 2
+ uint32_t maxProcesses; // Current: 16
+};
+
+Zenith::FbInfo
+struct FbInfo {
+ uint64_t width; // Framebuffer width in pixels
+ uint64_t height; // Framebuffer height in pixels
+ uint64_t pitch; // Bytes per scanline
+ uint64_t bpp; // Bits per pixel (always 32)
+ uint64_t userAddr; // Reserved (0 until mapped via SYS_FBMAP)
+};
+
+Zenith::KeyEvent
+struct KeyEvent {
+ uint8_t scancode; // Raw PS/2 scancode
+ char ascii; // Translated ASCII character (0 if non-printable)
+ bool pressed; // true = key down, false = key up
+ bool shift; // Shift modifier active
+ bool ctrl; // Ctrl modifier active
+ bool alt; // Alt modifier active
+};
+
+Zenith::SyscallFrame (kernel only)
+struct SyscallFrame {
+ uint64_t r15, r14, r13, r12, rbp, rbx; // callee-saved
+ uint64_t arg6, arg5, arg4, arg3, arg2, arg1;
+ uint64_t syscall_nr;
+ uint64_t user_rflags, user_rip, user_rsp;
+};
+
+ This is the stack frame pushed by SyscallEntry.asm and
+ passed to SyscallDispatch. Userspace code never sees this
+ directly.
+
Shell Application Walkthrough
+programs/src/shell/main.cpp
++ The built-in shell is the best example of a real ZenithOS application. + It demonstrates most of the available syscalls. +
+ +Initialization
+extern "C" void _start() {
+ zenith::print("\n ZenithOS Shell v0.1\n");
+ zenith::print(" Type 'help' for available commands.\n\n");
+
+ char line[256];
+ int pos = 0;
+ prompt();
+
+ while (true) {
+ char c = zenith::getchar(); // blocking read
+ // ... handle input, echo, backspace ...
+ }
+}
+
+ The shell uses zenith::getchar() in a loop for blocking
+ character-by-character input, manually handling echo and backspace.
+
Shell Commands & Syscalls Used
+| Command | Description | Syscalls Used |
|---|---|---|
help |
+ Print available commands | +SYS_PRINT |
+
info |
+ Show OS name, version, API version | +SYS_GETINFO, SYS_PRINT, SYS_PUTCHAR |
+
man <topic> |
+ Fullscreen manual page viewer | +SYS_OPEN, SYS_GETSIZE, SYS_READ, SYS_CLOSE, SYS_ALLOC, SYS_TERMSIZE, SYS_GETKEY, SYS_PRINT, SYS_PUTCHAR |
+
ls |
+ List ramdisk files | +SYS_READDIR, SYS_PRINT |
+
cat <file> |
+ Display file contents in 512-byte chunks | +SYS_OPEN, SYS_GETSIZE, SYS_READ, SYS_CLOSE, SYS_PRINT |
+
run <file> |
+ Spawn a new process and wait for it to exit | +SYS_SPAWN, SYS_WAITPID, SYS_PRINT |
+
ping <ip> |
+ Send 4 ICMP echo requests | +SYS_PING, SYS_SLEEP_MS, SYS_PRINT, SYS_PUTCHAR |
+
uptime |
+ Show uptime in minutes, seconds, ms | +SYS_GETMILLISECONDS, SYS_PRINT, SYS_PUTCHAR |
+
clear |
+ Scroll past visible content | +SYS_PUTCHAR |
+
exit |
+ Terminate the shell | +SYS_PRINT, SYS_EXIT |
+
Pattern: Reading a File
+The shell's cat implementation shows the standard file-reading pattern:
// 1. Build VFS path
+char path[128];
+// ... copy "0:/" + filename into path ...
+
+// 2. Open
+int handle = zenith::open(path);
+if (handle < 0) { /* error */ return; }
+
+// 3. Get size
+uint64_t size = zenith::getsize(handle);
+
+// 4. Read in chunks
+uint8_t buf[512];
+uint64_t offset = 0;
+while (offset < size) {
+ uint64_t chunk = size - offset;
+ if (chunk > sizeof(buf) - 1) chunk = sizeof(buf) - 1;
+ int bytesRead = zenith::read(handle, buf, offset, chunk);
+ if (bytesRead <= 0) break;
+ buf[bytesRead] = '\0';
+ zenith::print((const char*)buf);
+ offset += bytesRead;
+}
+
+// 5. Close
+zenith::close(handle);
+
+Pattern: IP Networking
+// Parse "10.0.2.2" into uint32_t in little-endian order
+uint32_t ip;
+parse_ip("10.0.2.2", &ip); // ip = 0x0202000A
+
+// Send 4 pings with 1-second intervals
+for (int i = 0; i < 4; i++) {
+ int32_t rtt = zenith::ping(ip, 3000);
+ if (rtt < 0) { /* timeout */ }
+ else { /* reply in rtt ms */ }
+ if (i < 3) zenith::sleep_ms(1000);
+}
+
+
+Adding New Syscalls
++ Adding a syscall requires changes in 3 files + (kernel-side) and 2 files (userspace-side). Follow + these steps: +
+ +Step 1: Assign a Syscall Number
++ Add a new constant in both copies of the syscall + number definitions. The numbers must match exactly. +
+kernel/src/Api/Syscall.hpp
+programs/include/Api/Syscall.hpp
+static constexpr uint64_t SYS_MYFUNC = 26; // next available number
+Step 2: Implement the Kernel Handler
+kernel/src/Api/Syscall.cpp
+Add a static function implementing your syscall's logic:
+static int64_t Sys_MyFunc(uint64_t arg1, const char* arg2) {
+ // Your kernel-side implementation here.
+ // You have full access to kernel subsystems.
+ return 0;
+}
+
+Step 3: Add the Dispatch Case
+kernel/src/Api/Syscall.cpp — SyscallDispatch()
case SYS_MYFUNC:
+ return (int64_t)Sys_MyFunc(frame->arg1, (const char*)frame->arg2);
+
+ Arguments are accessed through frame->arg1 through
+ frame->arg6, corresponding to RDI, RSI, RDX, R10, R8,
+ R9 respectively.
+
Step 4: Add a Typed Userspace Wrapper
+programs/include/zenith/syscall.h
+inline int64_t my_func(uint64_t arg1, const char* arg2) {
+ return syscall2(Zenith::SYS_MYFUNC, arg1, (uint64_t)arg2);
+}
+
+ Choose the appropriate syscallN variant based on the
+ number of arguments. Cast pointer types to uint64_t.
+
Step 5: Update the Log Message (optional)
+kernel/src/Api/Syscall.cpp — InitializeSyscalls()
+ Update the boot log to reflect the new syscall count: +
+// Change "26 syscalls" to "27 syscalls"
+Kt::KernelLogStream(Kt::OK, "Syscall") << "SYSCALL/SYSRET initialized (LSTAR="
+ << kcp::hex << (uint64_t)SyscallEntry << kcp::dec << ", 27 syscalls)";
+
+Complete Checklist
+| # | File | Change |
|---|---|---|
| 1 | kernel/src/Api/Syscall.hpp | Add SYS_MYFUNC constant (+ any new structs) |
| 2 | kernel/src/Api/Syscall.cpp | Add Sys_MyFunc() implementation + dispatch case |
| 3 | programs/include/Api/Syscall.hpp | Add matching SYS_MYFUNC constant (+ any new structs) |
| 4 | programs/include/zenith/syscall.h | Add typed wrapper in zenith:: namespace |
| 5 | (optional) | Update syscall count in boot log |
Process Memory Model
++ Each process gets its own PML4 page table. The kernel half + (upper 256 entries) is shared; the lower half is per-process. +
+| Region | Virtual Address | Size | Purpose |
|---|---|---|---|
| Exit stub | 0x3FF000 | 4 KiB | Auto-exit trampoline (calls SYS_EXIT(0) if _start returns) |
| Program code | 0x400000+ | Varies | ELF .text, .rodata, .data, .bss |
| User heap | 0x40000000+ | Grows up | Page pool (SYS_ALLOC); managed by userspace free-list heap |
| Framebuffer | 0x50000000+ | height × pitch | Mapped by SYS_FBMAP; direct pixel access (32-bit ARGB) |
| User stack | 0x7FFFFEF000–0x7FFFFFF000 | 16 KiB (4 pages) | Grows down from 0x7FFFFFF000 |
+ The ELF loader maps PT_LOAD segments with user-accessible page flags. + BSS is zero-initialized automatically (pages are allocated zeroed). +
+ + +Current Limitations
+-
+
- No standard library (C++) — there is no libc++ for C++ programs.
+ String operations, integer formatting, etc. must be written manually (see the shell for
+ examples). A minimal C standard library exists for C programs (used by the DOOM port) in
+
programs/include/libc/with headers likestdio.h, +stdlib.h, andstring.h.
+ - No dynamic linking — all programs are statically linked. +
- No argc/argv —
_startreceives no +argc/argv. Usezenith::getargs()+ to retrieve a single argument string (max 255 chars) passed via +zenith::spawn().
+ - No page-level
free()— +SYS_FREEis a no-op; pages mapped viaSYS_ALLOC+ are only reclaimed when the process exits. However, the userspace heap + (zenith::malloc/zenith::mfree) does reuse + memory via a free list.
+ - 16 process slots — the scheduler supports a + maximum of 16 concurrent processes. +
- No writable file system — the VFS is read-only + (backed by the boot ramdisk). +
- Shared console — all processes write to the + same terminal; there is no per-process stdout. +
- Shared keyboard — keyboard input is global; + there is no input focus or multiplexing. +
- 10 ms scheduling granularity — preemptive + round-robin with a fixed 10 ms time slice. +
+
+ ZenithOS Documentation — Copyright © 2025-2026 Daniel Hammer +
+ +