RTOS Kernel Internals: Tasks, Scheduler & Context Switch

Look inside a real-time kernel. Learn what a task is (TCB + stack), task states, the tick and tickless idle, priority-based preemptive scheduling with ready lists, how a Cortex-M context switch works (SysTick, PendSV, PSP/MSP, EXC_RETURN), stack sizing per task, kernel-aware interrupt priorities, and the overheads you pay for an RTOS.

An RTOS makes one CPU look like many: each task believes it owns the processor. The illusion is created by the scheduler, which saves one task's registers and restores another's many times per second — in well under a microsecond.

Knowing the internals turns mysterious RTOS bugs into understandable ones: a task that never runs (priority starvation), random crashes (stack overflow into another task), ISRs that break the kernel (wrong priority for FreeRTOS API calls), or a high idle current (tick waking the CPU).

An analogy

A kernel is a juggler: each ball is a task. The juggler (scheduler) only ever holds one ball at a time (running), others are in the air (ready) or set aside on a table (blocked). The hand-off must be fast and precise, or balls drop.

Tasks and states

A task is a function with its own stack and a TCB. It is Running (on the CPU), Ready (could run, waiting for the CPU), Blocked (waiting for time, a queue, semaphore or event) or Suspended (explicitly paused). Blocking is what makes an RTOS efficient: a task waiting for data uses zero CPU.

The scheduler

Most RTOSes use fixed-priority preemptive scheduling: the highest-priority Ready task always runs; equal priorities share the CPU by time slicing each tick. The kernel keeps a ready list per priority and often a bitmap so finding the highest priority is a single CLZ instruction — O(1) regardless of task count.

Context switch on Cortex-M

Tasks run in Thread mode on the PSP; the kernel and ISRs use the MSP. When the scheduler decides to switch, it pends PendSV. When no other ISR is active, PendSV saves R4–R11 to the current task's stack, stores the SP in its TCB, selects the next task, restores its registers and returns — hardware pops the rest. SVC starts the first task.

Kernel-aware interrupts, tickless idle and overheads

FreeRTOS masks interrupts up to configMAX_SYSCALL_INTERRUPT_PRIORITY during critical sections; ISRs that call FromISR APIs must have that priority or lower urgency (numerically ≥), while higher-priority ISRs run with zero kernel latency but must not call the kernel. Tickless idle stops the periodic tick during long idle periods and programs a low-power timer to wake exactly when the next task is due. Costs of an RTOS: ~5–10 KB Flash, TCB + stack per task (RAM), context-switch time (~1 µs), and non-determinism if priorities are designed badly.

Formulas

Worked example: Tick overhead

Given: 1 kHz tick, 200 cycles per tick ISR, 168 MHz

Find: CPU load.

Answer: 0.12% — negligible for CPU, but it wakes the core 1000×/s (bad for low power)

Worked example: Stack sizing

Given: Context 64 B; Deepest calls 520 B incl. locals; printf-style logging 400 B

Find: Task stack with 30% margin.

Answer: ≈ 1.3 KB → allocate 1.5 KB (384 words)

Worked example: Delay granularity

Given: Tick 1 kHz, vTaskDelay(1)

Find: Actual delay.

Answer: Anywhere from ~0 to 1 ms — use vTaskDelayUntil or hardware timers for precise timing

PendSV context switch (Cortex-M4, no FPU — simplified FreeRTOS-style)

        .syntax unified
        .thumb
        .global PendSV_Handler
        .extern current_tcb, scheduler_select_next
PendSV_Handler:
        mrs     r0, psp              @ r0 = current task's stack pointer
        stmdb   r0!, {r4-r11}        @ push callee-saved regs (HW already pushed r0-r3,r12,lr,pc,xpsr)
        ldr     r1, =current_tcb
        ldr     r2, [r1]
        str     r0, [r2]             @ save SP into current TCB (first field)

        push    {r1, lr}
        bl      scheduler_select_next  @ C: current_tcb = highest-priority ready task
        pop     {r1, lr}

        ldr     r2, [r1]
        ldr     r0, [r2]             @ load new task's saved SP
        ldmia   r0!, {r4-r11}        @ restore its callee-saved regs
        msr     psp, r0
        bx      lr                   @ EXC_RETURN: hardware unstacks r0-r3,... on PSP

Hardware saves half the registers on exception entry; PendSV saves the other half, swaps stack pointers between TCBs and returns. PendSV has the lowest priority so switches happen only after all ISRs finish.

Detecting stack overflow and measuring headroom (FreeRTOS)

/* FreeRTOSConfig.h */
#define configCHECK_FOR_STACK_OVERFLOW  2      /* check guard pattern on switch */

void vApplicationStackOverflowHook(TaskHandle_t t, char *name)
{
    log_fatal("stack overflow in %s", name);   /* then reset safely */
    NVIC_SystemReset();
}

void monitor_task(void *arg)
{
    for (;;) {
        UBaseType_t words = uxTaskGetStackHighWaterMark(sensor_task_handle);
        log_info("sensor task free stack: %u bytes", (unsigned)(words * 4));
        vTaskDelay(pdMS_TO_TICKS(5000));
    }
}

Stacks are painted with a pattern at creation; the high-water mark is the least free space ever observed. Size stacks from measurements under worst-case load, then add margin.

Common mistakes

Applications

Key takeaways

Going further