The kernel · L2 and L1

Time

A task can ask what time it is, wait for a while, or wait for a message only until a deadline (sysk 022). Time is authority too: a task that can read a clock can measure what others do. So the clock is a capability the manifest hands out, like a channel end or a device.

A tick a millisecond, without drift

The machine's counter, mtime, runs at 10 MHz. The kernel asks for an interrupt when it reaches mtimecmp, and every interrupt is a tick, counted and turned into a preemption. Until sysk 022 the next deadline was one quantum after now, the moment the interrupt was handled, so every tick lasted a little longer than a millisecond: the interrupt's latency each time, and the count fell behind real time. Now the next deadline is one quantum after the last deadline. A late tick is followed by an early one, so the count is exact: the number of milliseconds since the machine started. It took no new trusted code; timer_arm lost a line. The tick paces the scheduler and the deadlines; the time a task reads is finer (below).

Each mark is a deadline; each dot is when the interrupt was handled, late by up to the latency. Above, each deadline is set from its dot, so the marks drift right and fewer fit in 12 ms. Below, each is set from the last mark.

The clock is a capability

A task clause with (clock) gets a Clock, a struct with no fields that only the kernel can make, as it makes a Device. Unlike a device, any number of tasks may hold one, and it gives nothing but the time. The shell holds one:

(task shell (heap 2048) (jobs out) (catalogue) (clock) (rx commands) (tx answers))
(now k clock) → the nanoseconds since the machine started
(sleep-until k clock t): returns once the time is t or later; others run meanwhile
(alarm k clock t): the task's clock, as a source of alt, becomes ready at t, once

A task without a clock cannot call any of them: it has no Clock to pass, and making one is rejected at compile time (N003), as forging a handle is.

One unit: the nanosecond

Every time a program sees is in nanoseconds (sysk 024), as in Linux's clock_gettime, Go's time.Duration and Fuchsia's zx_time_t: one unit from the shortest interval to the longest, so no program converts between two. now reads mtime itself, which counts every 100 ns, rather than the tick count. Two readings in a row differ, by a few microseconds. Deadlines are still checked once a tick, so a sleep ends within a millisecond after its deadline. Only the shell, where a person reads and types, speaks in milliseconds. The time stops at 1018 ns, the bound now's contract states, so that a program can add an interval to it without overflowing.

A logarithmic scale: each step right is ten times longer. Above the axis, what now resolves and how far it reaches; below it, the tick, and how far sysk 022's milliseconds reached.

Reading mtime took one trusted function, clint_read: a 64-bit CLINT register at an address the kernel proves is the CLINT's. The PLIC's two accessors take addresses too now, as the drivers' do, and lost a line each, so the trusted assembly ended a line shorter.

Sleeping is a wait like any other

A task waiting for a message already loops: every other task runs once, and if none of them did anything a waiting task could notice, the machine sleeps with wfi until the next interrupt. Every tick is something a waiting task could notice. So sleep-until is that same loop, waiting for the clock to pass the deadline rather than for a message: no timer queue, no new way to wake a task. When everyone sleeps, the machine sleeps too, a millisecond at a time.

A deadline is a source

A task's sources for alt are numbered as the manifest lists them: the channels it receives on, room on the streams it sends on, its device, and now its clock, last. The clock is ready when the task's alarm is due; alt returning it clears the alarm. So waiting for input or a deadline, whichever comes first, is the alt a task already writes, with one more guard: occam's TIMER ? AFTER t. The test system tests/time races them. Select a step:

    Checked like the rest

    The deadlock check reads sleep-until as a wait that always ends, and a wait whose sources include the clock as never stuck, as it treats the device: time passes whatever other tasks do. The boot tests check the clock against the host's: a sleeper wakes five times, each 50 ms after the last by the clock, and the five take at least 250 ms of the host's time; at the shell, two (uptime)s around (sleep 300) differ by at least 300 ms.

    sysk> uptime
    up 17 ms
    sysk> sleep 300
    sysk> uptime
    up 320 ms

    Not yet: a job's programs get no clock (their parameters are channels), and a deadline between two ticks is met at the second. Arming the timer for the nearest deadline would make it exact.