Table of Contents

Timers and alarms

In this article, we will discuss delayed and repeating work on Cosmos Gen3: blocking for a while, running a callback later, and choosing between the two managers that offer to run one.

The two managers are the thing to read first. TimerManager runs a callback from the timer interrupt; AlarmManager runs one on a kernel thread. They take the same arguments and read almost identically at the call site, and picking the wrong one hangs the kernel.

TimerManager AlarmManager
Callback runs in Interrupt context Thread context
The callback may block, allocate or take a lock No Yes
Scheduling calls may be made from Anywhere, interrupt handlers included Thread context only
Needs the scheduler No Yes
Resolution The tick source's period The scheduler tick
Schedule hands back A SoftwareTimer handle, or null An alarm id, or 0
Feature switch CosmosEnableTimer CosmosEnableScheduler

If you find bugs or something abnormal, please submit an issue on our repository.


Enable the timer in your kernel

Timer support is behind a feature switch. Make sure your kernel's .csproj does not turn it off (it defaults to true), and leave the scheduler on as well if you want alarms:

<PropertyGroup>
  <CosmosEnableTimer>true</CosmosEnableTimer>
  <CosmosEnableScheduler>true</CosmosEnableScheduler>
</PropertyGroup>

These are the usings the snippets below rely on:

using System;
using Cosmos.Kernel.System.Diagnostics;
using Cosmos.Kernel.System.Timers;

The tick source is found and registered at boot, so TimerManager is ready as soon as your kernel runs. TimerManager.IsEnabled answers the compile-time question and TimerManager.IsInitialized the runtime one.


Waiting

TimerManager.Wait blocks the calling thread for a number of milliseconds. It is the simplest thing on this page and the one most kernels reach for, usually to pace a poll loop:

for (int i = 0; i < 10; i++)
{
    Console.WriteLine("tick " + i);
    TimerManager.Wait(500);
}

With CosmosEnableTimer=false this throws InvalidOperationException naming the switch, rather than returning at once and leaving a loop spinning.


Running a callback later

TimerManager.Schedule runs a callback once after a delay, and ScheduleRecurring runs one every period. Both hand back a SoftwareTimer handle:

bool cursorVisible = false;

/* Flipping a field is the shape an interrupt-context callback should have:
   the render loop reads it and does the drawing. */
SoftwareTimer? blink = TimerManager.ScheduleRecurring(
    () => cursorVisible = !cursorVisible,
    TimeSpan.FromMilliseconds(250));

/* ... later ... */
TimerManager.Cancel(blink);

The handle is read-only: TimeoutNs, Recurring and IsActive are what a caller can ask about a pending timer. Hand it back to Cancel to stop it. Cancel returns true only when the timer was still pending, so calling it twice, or on a one-shot that has already fired, is not an error.

The callback runs in interrupt context. It must not block, must not allocate, and must not take a lock, because the thread it interrupted may be holding one. It must also not let an exception escape: nothing above the interrupt dispatch catches one, and the kernel halts.

Schedule and ScheduleRecurring return null when the timer is compiled out or no tick source is registered, which is why the handle is nullable. Cancel accepts null and answers false.


Running a callback that needs to do real work

Anything the paragraph above forbids belongs on an alarm. AlarmManager keeps its own kernel thread and runs callbacks there, so an alarm callback may block, allocate, use scheduler primitives and take locks:

ulong id = AlarmManager.ScheduleRecurring(
    () => Console.WriteLine("free pages: " + MemoryDiagnostics.FreePages),
    TimeSpan.FromSeconds(5));

/* ... later ... */
AlarmManager.Cancel(id);

The shape is deliberately the same as TimerManager's so that switching between them is one word. What differs is what you get back: an alarm belongs to the alarm system rather than to a tick source's registry, so it is identified by a ulong id. Zero means the alarm was not scheduled, which happens when the scheduler is not running or the period is not positive.

The scheduling calls themselves are thread-context only. Every AlarmManager member takes the alarm list's mutex and parks if it is held. Calling one from an interrupt handler parks inside the handler and hangs, and a TimerManager callback is an interrupt handler. The TimerManager members mask interrupts instead of parking and carry no such restriction, so a timer callback that needs to do real work should signal a thread rather than schedule an alarm.

A recurring alarm's period restarts when the callback fires, not when it returns, so a callback that runs longer than its period leaves the next firing already due.


Changing the tick rate

TimerManager.Frequency reads and writes the tick source's rate in hertz. Each tick source divides a fixed counter, so each accepts a bounded range: 19 Hz to 1193180 Hz on the x64 PIT, and 1 Hz up to CNTFRQ_EL0 on the ARM64 generic timer. A value outside its range throws ArgumentOutOfRangeException rather than being quietly ignored:

Console.WriteLine("timer runs at " + TimerManager.Frequency + " Hz");
TimerManager.Frequency = 1000;

Raising it makes scheduled callbacks fire closer to their deadline and makes the interrupt itself more expensive. There is rarely a reason to change it.


Summary

Task Call
Block for a while TimerManager.Wait(ms)
Run a small callback later TimerManager.Schedule(callback, delay)
Run a small callback repeatedly TimerManager.ScheduleRecurring(callback, period)
Stop one TimerManager.Cancel(timer)
Run a callback that blocks or allocates AlarmManager.Schedule(callback, delay)
Run such a callback repeatedly AlarmManager.ScheduleRecurring(callback, period)
Stop one AlarmManager.Cancel(id)
Read or set the tick rate TimerManager.Frequency