|
miniOS
x86_64 hobby kernel with SMP, VFS, and POSIX process model
|
miniOS features a complete physical and virtual memory management system designed for the x86_64 architecture. This document provides an overview of its key components and design decisions.
The Physical Memory Manager, or frame allocator, is responsible for managing the system's physical memory frames.
The Virtual Memory Manager handles the virtual address space for the kernel and user processes.
The 64-bit canonical address space is split at the 48-bit boundary. Addresses 0x0000800000000000–0xFFFF7FFFFFFFFFFF are non-canonical (hardware fault on access).
| Symbol | Value | Purpose |
|---|---|---|
| KERNEL_VMA | 0xFFFF800000000000 | Higher-half kernel base (PML4 slot 256) |
| VGA_BUFFER_VA | 0xFFFF8000000B8000 | VGA text-mode framebuffer |
| HEAP_START | 0xFFFF820000000000 | Kernel heap base (PML4 slot 260) |
| HEAP_MAX | HEAP_START + 64 MiB | Kernel heap ceiling |
| DMA_BUFFER_VA | 0xFFFF830000000000 | ATA DMA bounce buffer (PML4 slot 262) |
| PAGE_SIZE | 0x1000 | 4 KiB page |
The canonical source for all virtual memory address constants is vmm.h, ensuring consistency across the kernel.
All memory allocators are protected by ticket spinlocks for SMP correctness:
The spinlock_irqsave variant disables local interrupts before acquiring, preventing a deadlock where the LAPIC timer ISR on the same CPU tries to re-acquire a lock already held by the interrupted kernel path.
mprotect(addr, len, prot) (SYS_mprotect = 10) updates the PTE flags for every mapped page in the range [addr, addr + len):
The mlibc sysdep Sysdeps<VmProtect> is implemented and routes mprotect(3) calls directly to SYS_mprotect.
miniOS implements three categories of demand-paged (lazy) memory:
sys_mmap records the region in mmap_regions[] with lazy = 1 and returns the base address without allocating any physical pages. On the first write or read to any page in the region the CPU generates a #PF. The page fault handler calls vmm_resolve_user_fault(), which allocates a zeroed physical frame and installs the PTE with permissions derived from region.prot. Subsequent accesses to the same page hit the TLB/page table normally.
sys_brk simply advances *brk_ptr to the requested address without allocating pages. A brk_base field (set at ELF load time, inherited at fork/clone) marks the lower bound of the heap. Any #PF in [brk_base, *brk_ptr) is handled by vmm_resolve_user_fault(), which allocates a zeroed read/write page. The shrink path (new_brk < old_brk) still walks the existing PTEs and frees any already-mapped frames, so memory is correctly reclaimed.
sys_mmap records the file's inode, file offset, file size and vfs_ops_t * pointer in the region slot with lazy = 1. No I/O is performed at mmap time. On #PF, vmm_resolve_user_fault() computes the file offset for the faulting page (region.file_offset + page_index * PAGE_SIZE), calls ops->read(inode, offset, kva, PAGE_SIZE) to populate the frame, zero-pads any partial trailing page, and installs the PTE. The vfs_ops_t * pointer is a stable static kernel pointer (e.g. &g_ext2_vfs_ops) so it remains valid even if the fd is closed after mmap.
vmm_resolve_user_fault() checks vmm_virt_to_phys(page_va) != 0 before allocating to handle the race where two CPUs fault the same page simultaneously. The second CPU finds the page already present and returns 0 immediately. vmm_map_page() issues both a local invlpg and ipi_tlb_shootdown() to ensure all CPUs see the new mapping.
Every process has its own PML4 (vmm_new_address_space()) — see docs/scheduler.md for the full per-process address space and fork protocol. Code/data/BSS/heap and mmap-region pages are shared between parent and child via fork_cow_share_page() (src/kernel/sched/sched.c) instead of eager copying:
Every shared frame carries a refcount (pmm_ref_frame()/pmm_unref_frame(), src/kernel/mm/pmm.c) — incremented once per fork_cow_share_page() call, decremented whenever a process stops using it (first CoW write, process exit, or execve()). The frame is only actually returned to the free pool when the count reaches zero, so a frame shared across several fork generations is freed exactly when the last owner lets go, regardless of write order between them.
Stack pages are not CoW — eagerly copied at fork time via the KERNEL_VMA direct-map alias (parent and child both already have their own address space by the time sched_fork() copies the content, so there's no shared-VA hazard to avoid; stack pages are just few enough that CoW isn't worth the complexity).
Exit / exec correctness. vmm_free_address_space() (process exit) and sys_execve()'s image teardown both just call pmm_unref_frame() on every mapped page in the process's own low half — no fork-lineage bookkeeping needed, since the refcount already tracks how many processes still reference a given frame.
madvise(addr, len, MADV_DONTNEED) (SYS_madvise = 28, advice value 4):
All other advice values return EINVAL. This lets mlibc's free() return physical pages to the OS rather than keeping freed heap pages mapped indefinitely.
Two regions are randomised per exec using low bits of rdtsc():
| Region | Randomised? | Entropy | Constraint |
|---|---|---|---|
| mmap base | Yes | 8 bits (256 pages = 1 MiB) | MMAP_BASE - rand * PAGE_SIZE |
| Stack RSP | Yes | 8 bits (256 slots × 16 B) | displacement below USER_STACK_TOP |
| Heap base (brk_base) | No | — | fixed at ELF image end |
| Text (code) | No | — | fixed at 0x400000 |
Randomisation happens in sched_create_user_task() (initial task creation) and again in sys_execve() for non-fork execs. Fork children inherit the parent's layout unchanged (correct POSIX semantics — child addresses must match parent until exec).
Heap and text randomisation require position-independent executables (PIE), which miniOS does not yet support (all binaries link at fixed address 0x400000).