A scheduler is the component of an RTOS (or bare-metal framework) responsible for deciding which task or thread runs on the CPU at any given moment, and for switching between tasks according to a defined policy. It enforces the timing and priority rules that give an embedded system its concurrency and, in real-time systems, its determinism guarantees.
In practice
In most RTOSes (FreeRTOS, Zephyr, ThreadX, embOS, etc.), the scheduler is preemptive and priority-based: the highest-priority ready task always runs, and a running lower-priority task is immediately preempted when a higher-priority task becomes ready. Tick-based schedulers fire a periodic interrupt (commonly 1 ms on Cortex-M targets, though this is configurable) to reassess which task should run. Some RTOSes also support round-robin or time-sliced scheduling among equal-priority tasks.
The scheduler interacts directly with the CPU context-switch mechanism. On ARM Cortex-M cores, FreeRTOS and many similar RTOSes commonly trigger a context switch via the PendSV exception, which saves and restores the software portion of the task stack frame alongside the hardware-saved frame, though other exceptions or switch paths are used in some ports and configurations. On other architectures (AVR, PIC32, RISC-V), the mechanism differs but the scheduler's logical role is the same.
Not every embedded project needs an RTOS scheduler. Cooperative schedulers, super-loops with state machines, and timer-driven dispatch tables can handle many workloads with lower overhead and simpler stack management. The blog posts "You Don't Need an RTOS (Part 2)" and "You Don't Need an RTOS (Part 4)" cover cases where these lighter alternatives are appropriate. "From Baremetal to RTOS: A review of scheduling techniques" surveys the spectrum from simple super-loops through cooperative and preemptive schedulers.
A common pitfall is priority inversion: a high-priority task blocks waiting for a resource held by a low-priority task, while a medium-priority task runs freely and starves both. Many RTOSes address this with priority inheritance mutexes, but the feature must be explicitly enabled or selected. Misconfigured tick rates, stack sizes, and interrupt priorities are the other most frequent sources of hard-to-reproduce scheduler bugs.
Frequently asked
What is the difference between a preemptive and a cooperative scheduler?
A
preemptive scheduler can
interrupt a running task at any time (typically on each tick interrupt or when a higher-priority task becomes ready) and switch to another task without the running task's cooperation. A cooperative scheduler only switches tasks when the currently running task explicitly yields control, calls a blocking API, or completes. Cooperative schedulers are simpler and avoid many
race conditions, but a task that never yields can starve all others.
How does the scheduler relate to interrupt service routines (ISRs)?
ISRs run outside the scheduler's task context and generally preempt any task. Most RTOSes provide separate
ISR-safe API variants (e.g.,
FreeRTOS's xSemaphoreGiveFromISR) that can post work or unblock a task, with the actual
context switch deferred until the ISR exits. It is a common bug to call the non-ISR API from within an ISR, which can corrupt the RTOS's internal state.
What is priority inversion, and how is it handled?
Priority inversion occurs when a high-priority task is blocked waiting for a shared resource (
mutex, buffer) held by a low-priority task, while medium-priority tasks continue to run and prevent the low-priority task from completing. The classic fix is
priority inheritance: the RTOS temporarily elevates the mutex holder's priority to match the highest waiter. Priority ceiling protocols are an alternative. Not all RTOSes implement either by default, so check your RTOS documentation.
Can a scheduler be used on a bare-metal system without a full RTOS?
Yes. Simple cooperative schedulers, round-robin dispatchers, and timer-driven task tables can be implemented in a few hundred bytes with no RTOS overhead. These are common on resource-constrained parts like 8-bit PIC or AVR targets where an RTOS would consume a significant fraction of available
RAM. The tradeoff is that these schemes typically lack priority
preemption and blocking primitives.
What causes unexpected scheduling latency, and how can it be measured?
Latency can come from disabled
interrupts (common in critical sections), long-running ISRs, tick granularity, or scheduler overhead during
context switches. On Linux-based embedded platforms, the blog post 'Surprising Linux Real Time Scheduler Behavior' illustrates how even SCHED_
FIFO tasks can experience unexpected delays due to kernel internals. On bare-metal RTOSes, a
GPIO toggled at the start of an ISR and measured with an oscilloscope or
logic analyzer is a straightforward way to characterize worst-case latency.
Differentiators vs similar concepts
The scheduler is sometimes conflated with the RTOS kernel as a whole, but the kernel also includes IPC primitives (queues,
semaphores,
mutexes), memory management, and timer services. The scheduler is specifically the policy and mechanism for selecting which task receives CPU time; the actual act of transferring control is handled by the dispatcher/context-switch code, though many sources use the terms interchangeably.