JOVANA
Explore Library Glossary Getting Started Three Levels Fields How it works Mission
Join the mission
All guides

RTOS, Real-Time Scheduling, and Determinism

When one super-loop can no longer juggle everything on time, you reach for a tiny OS that runs several tasks and a scheduler that guarantees the urgent ones meet their deadlines. This guide is about what "real-time" actually means, how priority-based scheduling delivers it, and the sharp edges — priority inversion, interrupt latency, worst-case timing — that decide whether a system is truly deterministic.

When the super-loop runs out of room

The first guide left you with the super-loop: read, decide, drive the outputs, repeat forever. It is genuinely enough for an enormous number of devices, and you should reach for it first. But picture a loop that does three jobs — sample a sensor every 1 ms, blink a status LED every 500 ms, and decode a slow stream of bytes from a serial port. If the byte-decoding step occasionally takes 4 ms, your 1 ms sensor sample is now late by 3 ms whenever that happens. The loop has only one thread of control, so a slow step anywhere delays everything after it. You can hand-tweak the ordering for a while, but as jobs multiply, the super-loop becomes a brittle puzzle of "who is allowed to take how long."

The way out is to stop writing one long sequence and instead write several independent tasks, each looking like its own little super-loop, and hand the job of choosing which one runs to a small piece of software: a real-time operating system (RTOS). An RTOS is not Linux shrunk down — it has no virtual memory, no processes, often no file system, and it can be a few kilobytes of code. What it gives you is exactly one precious thing the bare super-loop lacks: a scheduler that can pause one task and run another, so a slow job no longer blocks an urgent one. Each task is a C function with its own stack, written as "do my thing, then wait for the next time I am needed."

What "real-time" actually promises

The word real-time is the most misunderstood term in this whole subject, so let me be exact, because the slogan "real-time means fast" is simply false. Real-time means predictable: the system guarantees a response within a known time bound, every time, even in the worst case. A slow system that always answers within 50 ms is real-time; a blazing system that usually answers in 1 microsecond but occasionally stalls for 100 ms is not. The currency of real-time is not average speed but the deadline — a point in time by which a result must be delivered — and the promise is about meeting it reliably, not about being quick.

There are two flavors, and confusing them designs the wrong system. In hard real-time, missing a deadline is a failure, sometimes a catastrophic one: the airbag must fire within milliseconds of the crash, the motor commutation pulse must land on time or the motor stalls, the flight controller must update before the aircraft tips. A single missed deadline is a bug, full stop. In soft real-time, deadlines should be met and lateness costs you something, but an occasional miss merely degrades quality rather than breaking the system: a video frame arriving 5 ms late causes a tiny stutter, not a crash. The distinction is captured by hard vs soft real-time, and it drives everything downstream — your scheduler choice, how much margin you leave, how rigorously you must analyze the worst case.

How priority scheduling meets deadlines

How does a scheduler actually guarantee the urgent task runs on time? Almost every RTOS uses fixed-priority preemptive scheduling, and the rule is simple to state: at every instant, run the highest-priority task that is ready to run, and the moment a higher-priority task becomes ready, immediately pause the current one and switch to it. That pause-and-switch is preemption — the same preemptive scheduling idea from the OS rungs, now in the service of deadlines. Because the most urgent ready task always wins the CPU, a low-priority job decoding bytes can never make your high-priority sensor task miss its 1 ms beat: the sensor preempts it, does its work, and the decoder resumes from exactly where it was paused.

Which task should get which priority? For periodic tasks there is a beautiful, provable answer called rate-monotonic scheduling: assign priority by rate — the task that runs most often (shortest period) gets the highest priority. Counterintuitively, urgency follows frequency, not importance. Rate-monotonic scheduling comes with a real theorem: if the total CPU utilization (the sum of each task's execution time divided by its period) stays under a bound of about 69% for many tasks, every deadline is provably met. That bound is honest about its cost — you may have to leave roughly a third of the CPU idle to guarantee timeliness, which is the price of a proof rather than a hope.

Rate-monotonic test (3 periodic tasks):

  task   exec C   period T   utilization C/T
  ----   ------   --------   ---------------
  A      0.2 ms    1 ms        0.20
  B      1.0 ms    5 ms        0.20
  C      3.0 ms   20 ms        0.15
                           total U = 0.55

  RM bound for n=3 :  3 * (2^(1/3) - 1)  ~=  0.78
  U = 0.55  <  0.78   ->  all deadlines provably met
  priorities by rate:  A (1ms) > B (5ms) > C (20ms)
A worked rate-monotonic check. Add up each task's utilization C/T; if the total stays under the bound for that number of tasks, the fixed-priority schedule provably meets every deadline. Priorities go to the fastest-repeating task, not the most "important" one.

Rate-monotonic is not the only scheme. Earliest-deadline-first takes a different, dynamic view: at each moment, run whichever ready task has the nearest deadline, recomputing as deadlines approach. Earliest-deadline-first (EDF) is theoretically more powerful — it can keep every deadline up to 100% utilization, with no idle margin wasted — but that power costs you: priorities change at runtime so the scheduler is more complex, behavior under overload is harder to reason about, and a single overrun can cascade. Many shipping systems still prefer rate-monotonic precisely because its fixed priorities make the worst case easy to see and easy to trust. The honest summary: EDF squeezes out more CPU; fixed-priority is simpler to analyze and to debug at 3 a.m.

Priority inversion: when the scheduler lies to itself

Here is the bug that the simple priority rule does not, by itself, prevent — and it is famous because it nearly killed the 1997 Mars Pathfinder mission. Imagine three tasks: high, medium, and low. The low task takes a mutex to touch a shared resource. While it holds the lock, the high task wakes, tries to take the same mutex, and must block — fair enough, it has to wait for the low task to release. But now the medium task wakes. It does not need the lock, so the scheduler, seeing it is higher priority than the low task, runs it. The medium task can run as long as it likes, and while it does, the low task never gets the CPU to finish and release the lock — so the high task, the most important one in the system, is stuck waiting on the medium one. The priorities have been silently inverted.

This is priority inversion, and it is one of the genuinely subtle hazards of real-time systems — you will not catch it with a casual test because it needs that exact three-way timing to appear. Priority inversion is a close cousin of the priority-inversion hazard you met in the synchronization rung, and the standard cure is priority inheritance: while a low-priority task holds a lock that a high-priority task is waiting for, the kernel temporarily raises the low task's priority to match the waiter's. Now the medium task can no longer preempt it; the low task finishes its critical section, releases the lock, drops back to its normal priority, and the high task proceeds. A good RTOS offers a priority-inheritance mutex; using it is the difference between a system that works in the lab and one that mysteriously hangs on Mars.

Where determinism is won or lost

A scheduler can only meet a deadline if it can predict how long each step takes — and the deepest enemy of prediction is the gap between the average case and the worst case. The number that matters for a hard deadline is the worst-case execution time (WCET): the longest a task can possibly take, over every input and every path. WCET is genuinely hard to pin down, and it is humbling, because the very hardware features that make code fast on average make its worst case fuzzy: a cache miss can make one run of a function ten times slower than another, branch misprediction adds jitter, and on bigger chips the MMU and page faults add more. Designers of hard real-time systems often disable caches or pin code into fast memory, deliberately trading average speed for a tight, knowable worst case — the same predictability-over-speed bargain that defines real-time itself.

There is one more piece of the timing budget the scheduler does not control: how fast the chip can react to the outside world. When a hardware event fires an interrupt, there is an unavoidable delay before its ISR actually starts running — finishing the current instruction, saving registers, looking up the vector, entering the handler. That delay is the interrupt latency, and a hard real-time deadline must account for the worst-case latency, not the typical one. The single biggest enemy of low interrupt latency is disabling interrupts: every time you protect a critical section by turning interrupts off, you extend the worst-case latency for everything by however long they stay off. Interrupt latency is why embedded engineers keep ISRs and interrupt-disabled regions ruthlessly short — every microsecond you spend with interrupts masked is a microsecond added to the worst case of every deadline in the system.

One last honest caveat about the heap. On a hosted system you call malloc() without a second thought, but a general-purpose allocator's time is not deterministic — a single malloc() may walk a free list of unknown length, and over time fragmentation can make it slower and slower, or fail entirely. In a hard real-time task you almost never call malloc() on the hot path; you allocate everything up front, or use a fixed-size memory pool whose every operation takes the same bounded time. The recurring lesson of this whole rung returns one final time: in real-time embedded work you trade the convenient and the average for the bounded and the knowable — because a guarantee you cannot prove is not a guarantee at all.