1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
|
ZenOS - A purposefully small project.
This page is to remind you of the goals you had in mind when starting this project.
#This is *not* supposed to replace everything for everyone.
Keep in mind that what you do is only for you. You're the dev and the end user.
There is no pressure to do things in any order or at any speed.
No need to comply with any standarts or anything of that kind.
Just excercise. This isn't too unlike mental yoga.
What needs to be done?
Whatever you want to do, really.
On a high level:
Edit files, manipulate filesystems, compute (calculator, compiler, etc.), at a later stage internet (gossipChat, gopher/gemini)
What needs to be done on a lower level? (Feel free to add more stuff here, once clear)
Bootloader
-
-
-
Microkernel
- Services
Filesystem
-
-
-
Interface, whether graphical or text-based.
-
Write in Rust.
Nice — thanks for the clear priorities. Below is a concise, concrete design checklist and recommended trade-offs tuned to your goals: safety (practical, not formally guaranteed), high performance, extreme minimality/simplicity, extensibility, x86_64+UEFI only, resource efficiency, and maintainability.
Top-level choices (decision summary)
- Kernel style: small microkernel-ish monolithic-minimal hybrid — keep the kernel tiny but include a few carefully chosen fast-path services in-kernel for performance (see list below).
- Language: Rust for most kernel code, with tiny assembly for entry/context-switch. Use Rust to get strong memory-safety benefits while keeping code minimal. Allow small, audited unsafe blocks where needed.
- Boot target: UEFI (x86_64, OVMF/QEMU for dev). ExitBootServices early and run in your own long mode environment.
- Build/testing: QEMU + GDB for iterative development; keep host-side unit tests for freestanding modules where possible.
Kernel responsibilities (what stays in kernel)
1. IPC primitives and synchronous/asynchronous message passing with a very small, fast ABI (zero-copy/shared-buffer support).
2. Preemptive scheduler / context switch and per-CPU runqueues (strictly minimal API: create/kill/yield/block/unblock).
3. Minimal memory protection: basic page table manipulation, frame allocator, and mappings necessary to enforce isolation. Keep the allocator simple (buddy or slab-free with small footprint).
4. Interrupt/trap entry and demultiplexer to deliver events to in-kernel handlers or user services.
5. Low-level timer and high-resolution tick source for scheduling.
6. Capability / handle table (tiny capability mechanism) to grant resources to services.
7. Minimal device arbitration stubs only where absolutely needed for performance (e.g., IRQ dispatch to a driver service); otherwise forward to user services.
User-space services (what runs as services)
- Block device server and simple filesystem server (user-space FS reduces kernel size).
- Network stack as a user service (unless you need ultra-low-latency—then keep NIC RX/TX fast-paths in kernel).
- Device drivers in user space, except for tiny interrupt-forwarding stubs in kernel.
- Pager service / user-level virtual memory manager handling page-faults signalled by kernel.
- Session/process manager, shell, and other high-level daemons.
Performance vs safety trade-offs (concrete guidance)
- Fast IPC: implement a minimal syscall-like fast-path (direct call into kernel with shared ring buffer or mapped message pages) to reduce overhead. Make the fast-path small and verifiable; move complex logic to safe Rust user services.
- Shared memory: use explicit grant/mapping semantics (kernel mediates mappings) to avoid accidental aliasing/unsynchronized access. Prefer immutable shared pages or require explicit synchronization primitives in services.
- Critical fast-paths in kernel: keep them tiny (<200 LOC each) and audit them carefully; isolate unsafe code into small modules with heavy unit tests.
- Avoid heavy kernel inlining of high-level policies — move policy to services to reduce kernel complexity and make verification/auditing feasible.
Simplicity & minimality tactics
- Minimal syscall/API surface: design 10–20 kernel calls only (IPC send/recv, map/unmap, create/process, nanosleep/timer, sysinfo, grant capabilities).
- Small trusted computing base: aim for <50K LOC total initially (kernel <5–10K LOC).
- Single-threaded service model to simplify state machine reasoning; use async in services if needed.
- Prefer clear, small data structures (no complex trees unless required). Use simple allocators and avoid full-featured memory managers early.
Memory-safety practical rules
- Write most code in safe Rust; mark unsafe blocks explicitly and keep them tiny.
- Peer-review unsafe modules and provide tests and fuzz harnesses for them.
- Use strong assertions and debug builds when developing; enable fewer assertions in optimized builds if you need raw speed.
- Keep invariants explicit in types (e.g., newtypes for physical vs virtual addresses).
Maintainability & extensibility
- Well-defined IPC interface versioning: attach a tiny version number to each service protocol to allow evolution without kernel changes.
- Module-per-concern layout + small crates (if using Rust) so components can be rebuilt/tested independently.
- Document the minimal kernel API in one page and maintain simple diagrams of control flow for interrupts and IPC.
Resource efficiency
- Use static linking and strip debug info for release images.
- Keep default heap and stacks small; tune sizes per-service.
- Use on-demand allocation and avoid caches unless they measurably improve perf.
Debugging / observability (minimal, optional)
- Lightweight serial console + panic logging; avoid heavy telemetry.
- Optional in-memory ring buffer for recent kernel events (readable from a privileged service).
- Use QEMU's built-in tracing/GDB for low-level debugging instead of instrumenting the OS too much.
A minimal initial roadmap (stepwise)
1. UEFI boot stub in Rust/assembly that prints to serial/GOP and obtains memory map.
2. Small physical frame allocator + page-table setup. Create an initial address space and kernel stack.
3. Context switch + single-core scheduler and a thread abstraction; implement yield/block.
4. Simple IPC (sync call/response) and a trivial echo service in user-space.
5. Interrupt entry and timer; preemptive scheduling.
6. Implement user-space pager and a basic file/block server.
7. Measure hotspots; move vetted, tiny fast-paths into kernel only if necessary.
Concrete tech choices
- Rust stable (use only minimal nightly features if unavoidable).
- x86_64 crate for low-level bindings, or write tiny inline assembly for CPUID/CR3/irq.
- Use OVMF + QEMU for dev, cargo-xbuild or custom cross-toolchain for freestanding targets.
- Logging over serial; use VGA framebuffer only for debugging initially.
Final notes
- This plan gives strong practical safety via Rust while favoring speed and small trusted code. It keeps the kernel minimal but pragmatic: enough low-level primitives for isolation and fast IPC while pushing policy and complexity into user-space services you control.
- If you want, I can generate a 1-page kernel API (list of syscalls/IPC messages and their signatures) and a 200–300 line Rust starter template implementing the boot stub, frame allocator, and a trivial scheduler. Which would you prefer?
|