The Real-Time Execution Model
This chapter is about one question: when does a nano-ros callback meet its deadline, and what does the runtime do to make it? The design records are listed under Design background at the end of this page. Scheduling Models covers the RTOS schedulers underneath.
One level does not preempt
An executor runs a callback to completion. Within one OS thread nothing can interrupt user code mid-callback, so a callback that becomes ready while another runs waits for it to return. Its worst-case blocking is the longest callback that can be running when it wakes:
response time >= own execution time + longest other callback on the thread
A 5 ms telemetry callback on the same thread as a 1 kHz control loop makes that loop miss its 1 ms deadline every time the two meet, whatever priority bucket or scheduling class either has. The buckets and classes below change which READY callback runs next. They cannot stop one that is already running.
Scheduling classes
A callback runs in a scheduling context (SchedContext), and its class picks
how the executor orders ready work:
| Class | Orders ready work by | Use it for |
|---|---|---|
Fifo (default) | registration order within a priority bucket | most callbacks |
Edf | absolute deadline (release + relative deadline) | periodic work with stated deadlines on one level |
Sporadic | Fifo, plus a budget per replenishment period | a callback whose CPU share must be capped |
BestEffort | last, after every other bucket | logging, telemetry |
A tier or level’s class = "real_time" is the deployment-side spelling. On
Linux it also selects SCHED_DEADLINE for a level that states a budget (see
below). Time-triggered windows are a gate on a context
(tt_window_offset_us / tt_window_duration_us), not a class.
Levels: preemption from the kernel
The fix for the bound above is a second thread. nano-ros places callbacks on executor levels: one OS thread per level, all over one executor per core. A more urgent level’s thread has a higher kernel priority, so the kernel preempts a less urgent level mid-callback when the urgent one wakes.
- The number of levels is derived. Each node’s rank comes from its
contract (
paths.<p>.trigger.timer.rate_hz, rate-monotonic), and each distinct rank is one level, up to the image’smax_levels(CONFIG_NROS_EXECUTOR_MAX_LEVELS, default 8). - Priorities are allocated, not authored. A derived level takes a
priority from the board’s application band, below the transport tasks.
An authored
[tasks.<name>]bound withgroup_tasksis a pin: its members share one level at its stated priority. - The boot level is the least urgent one; it runs on
main()and owns the session. The others are spawned. - On a multi-core host, levels run in parallel. Preemption only shows when
they compete for one CPU, which is why the Linux tests pin the process with
taskset -c 0.
Who enforces a budget
A level whose tier states a budget and a period has exactly one enforcer. The kernel takes it where the kernel can; otherwise the executor enforces the same numbers at callback boundaries. The runner prints which:
nros: level budget tier=`capped` 2000us/10000us enforced by the KERNEL
| Platform | Kernel mechanism | When the kernel enforces it |
|---|---|---|
| Linux | SCHED_DEADLINE (runtime, deadline, period) | a spawned level with class = "real_time", when the process holds CAP_SYS_NICE and the level is not pinned to fewer CPUs than its root domain |
| NuttX | SCHED_SPORADIC (CONFIG_SCHED_SPORADIC) | every budgeted level thread on the C runner; spawned levels on the Rust runner |
| Zephyr, FreeRTOS, ThreadX | none | never: the executor enforces it |
Without CAP_SYS_NICE a Linux image keeps running. The kernel refuses both
SCHED_FIFO and SCHED_DEADLINE, the image prints the refusal once with the
setcap command that fixes it, and every level runs at the default policy.
That image has levels but no preemption between them.
Scenarios and what measured them
The execution-model design names twelve scenarios. Each row names the test that measured it, or says that none has.
| Scenario | Status |
|---|---|
| S1, drone flight controller: 1 kHz critical loop beside 5 ms telemetry | Linux, on levels: rt_sched_levels_e2e::s1_critical_loop_meets_every_deadline_on_its_own_level (60 s, zero misses on a PREEMPT_RT kernel and the host’s own floor on a generic one, plus a one-level negative control). It runs only on a lane that claims rt-sched. |
| S2, sense, plan and act at 10 Hz on one thread | No dedicated test. |
| S3, watchdog liveness | Levels feed the watchdog only when every level is alive. Measured on the S32K344 safety island. |
| S4, Autoware-style mixed rates on Linux | No dedicated test. |
| S5, Cortex-M0+ sensor node | No dedicated test. |
S6, Trigger::AllOf fusion | trigger_conditions (nros-tests) and the executor’s unit tests. |
| S7, a 1 kHz subscription drained once a cycle | Idempotent ready-set insertion, unit-tested in nros-node. |
| S8, a long-running action goal | The action server is a cooperative state machine; covered by the action e2e tests, not by a timing test. |
| S9, a service handler that triggers a callback | No dedicated test. |
| S10, cross-level priority inversion | Priority-inheritance mutexes in the POSIX port (c_port_posix). No scenario test. |
| S11, several nodes on one executor | The default deployment; every multi-node example runs it. |
| S12, a cycle wall-time budget | Not built: there is no cycle budget option. |
Running the real-time tests in the tree: see Internals → Running the RT tests.
Design background
- RFC-0002: real-time execution model — the executor’s scheduling contexts and the twelve scenarios (§11)
- RFC-0108: executor preemption levels — levels and the watchdog rule (D8)
- RFC-0079: priority is allocated, not authored