Calibrated timer / clock

This commit is contained in:
2026-07-03 14:17:00 +01:00
parent 9a834c91ff
commit e80043b611
9 changed files with 212 additions and 14 deletions
+6
View File
@@ -34,6 +34,12 @@ rather than restate it. Roughly in the order things happen at runtime:
10. **[halting.md](halting.md) — halting.** Why a kernel can't just "exit", and
how `while (true) hlt` parks the CPU safely once there's nothing left to do.
Start with the north star:
- **[vision.md](vision.md) — the vision.** danos is aiming to be a real-time
microkernel: minimal kernel, drivers/services isolated in user space, preemptive
scheduling with timing guarantees. The *why* that shapes everything below.
Cutting across all of these:
- **[arch.md](arch.md) — the architecture split.** How CPU-specific code is kept
+11 -5
View File
@@ -39,10 +39,15 @@ LVT-timer entry giving it a **vector** (32) and **periodic** mode, then an initi
count that becomes the reload value. From then on it fires vector 32 repeatedly, on
its own, forever.
> The count isn't calibrated to real time yet — the tick *rate* is arbitrary
> (bus-clock dependent). Turning it into a known frequency (say 100 Hz) needs a
> reference clock to measure against (the PIT, HPET, or the TSC). That's a later
> step; for now it just needs to tick.
The reload count isn't picked arbitrarily — it's **calibrated to real time**,
which the [real-time](vision.md) scheduling guarantees depend on. Since the LAPIC
timer's raw rate is bus-clock dependent and unknown up front, `calibrate` measures
it against the **PIT** (the legacy 8254, whose 1.193182 MHz is fixed): run the
LAPIC timer one-shot from its maximum count while the PIT counts out a known 10 ms
(polling channel 2, no interrupt needed), then see how far the LAPIC got. That
yields its counts-per-millisecond, from which `initTimer(hz)` computes the reload
count for any target frequency. danos runs it at **1000 Hz** (a 1 ms tick), and the
tick count times the known period gives a monotonic `uptimeMs()`.
## Two kinds of vector, one dispatch
@@ -101,7 +106,8 @@ spinning in unrelated code — is the whole mechanism working end to end.
- **The keyboard**: bring up the IO-APIC, route its IRQ to a vector, and read
scancodes from the PS/2 controller — the first *input* device.
- **A calibrated timer** at a known frequency, and a monotonic clock.
- **`sleep()` / timeouts** built on the calibrated clock (the monotonic
`uptimeMs()` is in place).
- **Uncacheable MMIO**: the LAPIC page is currently mapped writeback-cacheable like
the rest of the identity map. QEMU tolerates it, but real hardware wants MMIO
marked uncacheable (via the page's cache bits or an MTRR).
+1
View File
@@ -47,6 +47,7 @@ Current cases:
|------|----------------|-----------------------------|
| `smoke` | memory map has usable RAM; frame alloc/free; paging active | `DANOS-TEST-RESULT: PASS` |
| `timer` | device interrupts fire and return (tick count advances) | `DANOS-TEST-RESULT: PASS` |
| `clock` | calibrated LAPIC frequency is sane; monotonic uptime advances | `DANOS-TEST-RESULT: PASS` |
| `vmm` | on-demand `map` works: a mapped page is writable and reads back | `DANOS-TEST-RESULT: PASS` |
| `heap` | kernel heap: alloc/free, block reuse, growth, and a std container on it | `DANOS-TEST-RESULT: PASS` |
| `fault-ud` | invalid-opcode exception is caught | serial shows `invalid opcode (vector 6)` |
+84
View File
@@ -0,0 +1,84 @@
# Vision: a real-time microkernel
danos is aiming to be a **real-time operating system built on a microkernel** —
where drivers and services run isolated in user space for maximum stability, and
scheduling gives real guarantees about timing. This page is the north star: the
*why* that shapes every design decision below it. Read it before adding anything
structural.
## Microkernel
The kernel stays **minimal** — only what genuinely must run in privileged mode:
- scheduling,
- inter-process communication (IPC),
- memory management (address spaces, page tables),
- low-level interrupt dispatch.
Everything else — device drivers, filesystems, the network stack — runs as an
**isolated user-space server**, each in its own address space with only the
privileges it needs.
The payoff is **stability through isolation**. A driver bug can't corrupt the
kernel or another driver; a crashing service is contained and can be restarted,
while the rest of the system keeps running. That's the opposite of a monolithic
kernel, where a single driver fault can take everything down.
The cost is that **IPC becomes the backbone**: whatever used to be a function call
across a monolithic kernel is now a message between address spaces. In a
microkernel, IPC performance essentially *is* system performance (the lesson of
L4). So IPC must be fast, and it's a first-class concern, not an afterthought.
Hardware interrupts, too, become IPC: the kernel turns an IRQ into a message to the
driver task that owns that device.
## Real-time
danos schedules **preemptively, with guarantees about quanta** — the system must
be able to promise that a task runs when it's supposed to, within bounded time.
That imposes concrete requirements:
- **Fixed-priority preemptive scheduling.** The highest-priority ready task always
runs; a higher-priority task that becomes ready preempts a lower one immediately.
Not round-robin (which is fair but not predictable).
- **A calibrated, deterministic clock.** Guarantees measured in "quanta" are
meaningless on an arbitrary tick rate — real time requires a timer calibrated to
a known frequency.
- **Bounded interrupt latency.** Interrupt-disabled sections must be short and
bounded, so a ready high-priority task is never delayed by an unbounded kernel
operation.
- **Deterministic kernel operations.** Scheduling decisions should be O(1) (e.g. a
priority bitmap), not "walk a list of unknown length."
- **Priority inheritance** (once there are locks/IPC), so a high-priority task
blocked on a resource held by a low-priority one can't be delayed indefinitely by
a middle-priority task — bounding priority inversion.
A consequence worth stating early: the current [kernel heap](heap.md) is a
first-fit free list, which has **unbounded allocation time** and can fragment — it
is *not* real-time safe. It's fine for one-time kernel setup, but real-time paths
must pre-allocate or use a bounded (fixed-size pool) allocator. Don't allocate on a
hot real-time path.
## What this means for the roadmap
The vision reorders the obvious hobby-kernel path. Notably, **drivers are not
built into the kernel** — so an in-kernel keyboard driver would be throwaway work.
Input devices arrive later, as the *first user-space drivers*, once the machinery
to isolate them exists. The trajectory:
1. **Calibrated timer / clock** — a known-frequency, deterministic tick. The
foundation real-time quanta rest on. *(next)*
2. **Real-time scheduler** — fixed-priority preemptive, kernel threads first:
context switch, task struct, priority run-queue, timer-driven preemption.
3. **User mode + address-space isolation** — higher-half kernel, ring 3, per-process
page tables. The substrate for isolated servers.
4. **IPC** — fast message passing between address spaces. The microkernel's heart.
5. **User-space drivers** — interrupts delivered as IPC, plus MMIO/port-access
grants. The keyboard becomes the first one, validating the whole model.
## Where we are
The foundation is in place: UEFI boot, framebuffer + [serial](testing.md),
[physical frames](frame-allocator.md), [paging](paging.md) with W^X, [exceptions
and interrupts](interrupts.md), a [timer](device-interrupts.md), and a
[heap](heap.md) — plus a [test harness](testing.md). The kernel boots and has its
core services; the next milestones make it *schedule*, then *isolate*.