Memory
Motor OS uses 4 KiB pages, 2 MiB pages for large heaps, eager mappings by default, and guarded lazy stacks. Instead of an OOM killer it has admission control: a memory-growing operation is refused before it starts if it would push the machine below a safety floor, and a shared "memory pressure" flag lets sys-io shed load before it runs out.
Pages and mappings
- Ordinary userspace memory is mapped in 4 KiB pages. A heap allocation larger than 1 MiB is served with 2 MiB pages where whole ones are available and with 4 KiB pages otherwise; the size returned is rounded to what was mapped. The I/O manager also has a fixed segment of 2 MiB pages for device memory.
- Mappings are eager by default. A process can ask for a lazy heap segment, which is backed on first touch. Stacks are always lazy and guarded, so a stack overflow faults instead of corrupting a neighbor.
- Shared memory between two processes is created by URL and is the basis of the I/O channels; a spawning process also maps pages into a child's address space directly while loading it.
- Writable and executable is never granted together for one mapping.
- A per-process memory limit exists in the kernel but is not exposed yet and is unlimited by default.
Physical memory
The kernel manages RAM as 2 MiB blocks. A block is whole, which means free or serving
one 2 MiB page, or split into 4 KiB pages. A split block keeps a free list threaded
through its free pages, and every link carries a check word, so a corrupted or forged link
is detected rather than followed. Memory below 128 MiB only supplies small pages; above it
a whole block serves either a huge page or, as a last resort, small pages. Small pages
are taken from the bottom of memory up and huge pages from the top down, and a block
whose pages have all been freed becomes whole again, so the two sizes stay apart. Each
CPU keeps a cursor into the block it last allocated from. The design, the boot-time
shaping of the pool, and the kernel's virtual layout are in docs/kernel-mm.md.
The allocator
Every process's heap, and the kernel's own heap, is frusa, a no-std
allocator written for Motor OS. Requests up to 4 KiB are rounded to a power-of-two size
class and served from 64-slot blocks, each tracked by one bitmap; larger requests map pages
directly. A thread keeps one private block per class for its temporaries, freed objects
wait on short per-CPU lists, and a pointer is located by a binary search over an
address-sorted index, so no operation walks the heap and no lock is held across a request
to the kernel. Memory goes back to the kernel on demand: the runtime returns idle pages
every five seconds once more than 1 MiB is idle, or as soon as a page is idle while the
memory-pressure flag is up. The kernel uses the same crate with a per-CPU cache in front
of it. The design is in docs/frusa.md.
Why admission control
Allocating N pages of data also costs page descriptors, page tables, and occasionally a new slab for kernel bookkeeping, about 2–2.5% on top of the data in measurements. Before admission control the kernel's low-memory check counted only the requested data pages and was separate from the allocation, so concurrent requests could all pass it, and one unprivileged process could empty the physical page pool and panic the kernel. Admission control is a stability mechanism, not a fairness policy: it does not pick a victim, does not promise that every lazy page will get backing, and does not attribute shared kernel objects to processes.
Floors
Two compile-time low-water marks over free small pages divide memory into zones:
available above USER_FLOOR (256 pages, 1 MiB):
any process may start a memory-growing operation
SYS_IO_FLOOR (128 pages, 512 KiB) < available <= USER_FLOOR:
only system processes may (sys-io, sys-tty, sys-init, strobe)
available <= SYS_IO_FLOOR:
nobody may; what is left is for kernel work already in flight
The reserve between the floors belongs to processes with the system or I/O-manager capability; the lower floor keeps its historical sys-io name. The floors are constants, not configuration: an operator must not be able to configure the machine back into physical exhaustion. If measurements ever show they are too small, the floors are raised rather than a new allocation class added.
How an operation is admitted
Every userspace operation that grows memory (eager, lazy, shared, and contiguous mappings, page faults on lazy memory, stacks, address-space, process, and thread creation, kernel objects, IPC) is charged a conservative worst case, and a reservation for that charge is published in a global counter before the operation runs:
charge = data_pages + ceil((data_pages + descriptors) / 32) + 64
admit iff (available - reservations) - charge >= floor
The reservation is held until the operation completes, which double-counts pages the operation has already allocated; that can refuse a request slightly early but can never admit too much. Fixed charges: a process 256 pages, a thread 64, a kernel object or IPC 16. A remote operation (sys-io loading a program into a new process) is charged against the target process's class, so a privileged loader does not widen an ordinary process's allowance.
What a refusal means:
- A syscall returns
E_OUT_OF_MEMORYbefore anything happens; there are no side effects, and the caller can back off. - A refused page fault on lazy memory cannot return an error to the faulting instruction, so the faulting thread is killed, which kills its process. This is the accepted cost of having no OOM killer: the victim is whichever process faults while availability is below its floor.
Admission costs nothing measurable: in a release A/B test, eager 16-page allocate-and-free and warm lazy faults were within noise of the pre-admission build, and boot time was unchanged.
Pressure mode
The floors alone cannot keep sys-io alive, because sys-io's heap grows through the
same allocator and a failed allocation aborts the process. Pressure mode stops sys-io's
demand before the kernel has to refuse it. The kernel maintains a
memory_pressure flag in a read-only page mapped into every process, with
hysteresis so a single allocation or free cannot flip service on and off:
raise when free-for-admission <= 512 pages (2 MiB)
clear when free-for-admission >= 768 pages (3 MiB)
Any process can read the flag with one memory load. While it is up:
- the runtime refuses to spawn new processes; the parent gets a recoverable
E_OUT_OF_MEMORYbefore any work is done; - sys-io refuses new TCP listeners, outbound connects, UDP binds, and ICMP echo;
- sys-io refuses every filesystem request except releasing a lock (reads and stats too, because they walk the block cache and allocate on a miss), and frees the pages the refused request had donated;
- sys-io drops new client connections at accept, for both the filesystem and the network service: a new client is exactly the load being shed;
- listener pools stop refilling; a recovery task re-arms them after the flag clears.
Nothing that must run during an episode may allocate; the refusal paths are allocation-free by construction. Established TCP connections keep working, because serving them allocates nothing. The flag clears from the page-free path as well, so it drops when the process that caused the pressure exits even if nobody else allocates.
Observability
Kernel metrics: mem.admission_refused_user,
mem.admission_refused_sys_io, mem.admission_reserved_pages,
mem.small_pages_low_water, mem.phys_small_pages_low_water, and
the two floor gauges. sys-io metrics: net.pressure_active,
net.pressure_entries, net.pressure_refused,
net.pressure_refused_clients, net.pressure_deferred_replenish,
fs.pressure_refused, and fs.pressure_refused_clients. A single
syscall (F_QUERY_ADMISSION_STATS) reports availability, reservations, floors,
and watermarks in one call. The block pool reports its own gauges, mem.blocks_*
and mem.pages_*. See Logs and diagnostics for
how to read metrics.
Deliberately not built
- An OOM killer, or any recovery class that may allocate the pool to zero.
- Exact per-process attribution of page tables and shared kernel objects.
- A configurable kernel reserve.
- Retrying refused allocations. An actual allocator failure is always a defect in a charge or in a bounded-work assumption, and production code must not paper over it with a retry or a longer timeout.
The full argument, including the measurements that validated the floors, is in
docs/oom-handling.md; the kernel's physical and virtual memory management is in
docs/kernel-mm.md and the allocator in docs/frusa.md.