Scheduler
January 26, 2026 · View on GitHub
F9 implements a fixed-priority preemptive scheduler with Preemption-Threshold Scheduling (PTS) extensions. The scheduler provides O(1) thread selection using the ARM Cortex-M CLZ instruction for efficient bitmap scanning.
Architecture Overview
The scheduler consists of two main components:
- Priority Bitmap Scheduler: O(1) highest-priority selection with 32 priority levels
- Preemption-Threshold Scheduling (PTS): Optional threshold-based preemption control for reduced context switching
Both components integrate with the Priority Inheritance Protocol (PIP) to prevent priority inversion during IPC operations.
Priority Bitmap Scheduler
Design
The scheduler uses a 32-bit bitmap where each bit represents one priority level:
/* Priority bitmap: bit set means queue has runnable threads */
static uint32_t ready_bitmap;
/* Ready queue heads for each priority level (circular doubly-linked) */
static tcb_t *ready_queue[SCHED_PRIORITY_LEVELS];
The bitmap uses inverted bit positions: bit 31 represents priority 0 (highest), bit 0 represents priority 31 (lowest). This allows the CLZ (Count Leading Zeros) instruction to directly return the highest priority level.
Priority Levels
F9 defines 32 priority levels (0 = highest, 31 = lowest):
| Priority | Constant | Purpose |
|---|---|---|
| 0 | SCHED_PRIO_SOFTIRQ | Kernel softirq thread |
| 1 | SCHED_PRIO_INTR | Interrupt handler threads |
| 2 | SCHED_PRIO_ROOT | Root thread |
| 3 | SCHED_PRIO_IPC | IPC fast path (temporary boost) |
| 4-30 | User-defined | Application threads |
| 31 | SCHED_PRIO_IDLE | Idle thread (always runnable) |
Thread Selection
The schedule_select() function provides O(1) selection:
tcb_t *schedule_select(void)
{
uint32_t flags;
uint32_t prio;
tcb_t *thread;
flags = irq_save_flags();
/* CLZ returns 32 if bitmap is 0 */
prio = clz32(ready_bitmap);
if (prio >= SCHED_PRIORITY_LEVELS) {
irq_restore_flags(flags);
panic("SCHED: Empty ready_bitmap (idle missing)\n");
return NULL;
}
thread = ready_queue[prio];
irq_restore_flags(flags);
return thread;
}
The function uses the CLZ instruction to find the first set bit in the bitmap, which corresponds to the highest priority with runnable threads.
Queue Operations
Enqueue
Threads are added to the tail of their priority queue (FIFO within priority):
void sched_enqueue(tcb_t *thread)
{
uint8_t prio = thread->priority;
tcb_t *head = ready_queue[prio];
if (!head) {
/* Empty queue: thread becomes sole element */
ready_queue[prio] = thread;
thread->sched_link.prev = thread;
thread->sched_link.next = thread;
/* Set bitmap bit for this priority */
ready_bitmap |= (1UL << (31 - prio));
} else {
/* Insert at tail (before head in circular list) */
tcb_t *tail = head->sched_link.prev;
thread->sched_link.next = head;
thread->sched_link.prev = tail;
tail->sched_link.next = thread;
head->sched_link.prev = thread;
}
}
Optimization: The bitmap is only updated when a queue transitions from empty to non-empty.
Dequeue
Threads are removed from their current queue:
void sched_dequeue(tcb_t *thread)
{
uint8_t prio = thread->priority;
tcb_t *prev = thread->sched_link.prev;
tcb_t *next = thread->sched_link.next;
if (thread == next) {
/* Only element in queue */
ready_queue[prio] = NULL;
ready_bitmap &= ~(1UL << (31 - prio));
} else {
/* Unlink from list */
prev->sched_link.next = next;
next->sched_link.prev = prev;
/* Update head if we removed it */
if (ready_queue[prio] == thread)
ready_queue[prio] = next;
}
/* Mark as not queued */
thread->sched_link.prev = NULL;
thread->sched_link.next = NULL;
}
Yield
The sched_yield() function implements round-robin within a priority level:
void sched_yield(void)
{
tcb_t *curr = thread_current();
uint8_t prio = curr->priority;
tcb_t *head = ready_queue[prio];
/* Only rotate if more than one thread at this priority */
if (head && head->sched_link.next != head) {
ready_queue[prio] = head->sched_link.next;
}
}
The queue head is rotated to the next thread, giving all threads at the same priority fair access to the CPU.
Priority Changes
The sched_set_priority() function handles priority changes atomically:
void sched_set_priority(tcb_t *thread, uint8_t new_prio)
{
uint32_t flags;
int was_queued;
if (!thread)
return;
if (new_prio >= SCHED_PRIORITY_LEVELS)
new_prio = SCHED_PRIO_IDLE;
flags = irq_save_flags();
/* Check if priority actually changes */
if (thread->priority == new_prio) {
irq_restore_flags(flags);
return;
}
/* Remove from old queue if queued */
was_queued = sched_is_queued(thread);
if (was_queued)
sched_dequeue(thread);
/* Update priority */
thread->priority = new_prio;
/* Re-add to new queue if was queued */
if (was_queued)
sched_enqueue(thread);
irq_restore_flags(flags);
}
If the thread is currently queued, it is moved to the appropriate queue for its new priority.
Preemption-Threshold Scheduling (PTS)
F9's PTS implementation is designed to match ThreadX RTOS semantics,
which provides the reference implementation of preemption-threshold scheduling.
The L4_Set_PreemptionDelay API is modeled after ThreadX's tx_thread_preemption_change function.
Motivation
Standard priority scheduling allows any higher-priority thread to preempt a running thread. This can lead to:
- Excessive context switches in critical sections
- Priority inversion when low-priority threads hold resources
- Increased stack depth due to nested preemptions
- Cache pollution from frequent context switches
PTS addresses these issues by allowing threads to temporarily raise their preemption threshold, reducing the set of threads that can preempt them.
Semantics
Priority Numbering: F9 uses 0=highest priority, 31=lowest (matching ThreadX). Lower numeric values represent higher priorities.
Each thread has two priority-related attributes:
- Priority (π): Determines scheduling order when multiple threads are runnable
- Threshold (γ): Controls which threads can preempt this thread
Preemption Rule: Thread j can preempt thread i if and only if π_j < γ_i (where < means higher priority, lower number)
Constraints:
- Threshold must satisfy: γ ≤ π (numerically, meaning threshold number is same or lower)
- Lower threshold values provide tighter protection (fewer threads can preempt)
- Example: If priority = 10, threshold can be 0-10 (not 11-31). Setting threshold=10 allows only 0-9 to preempt (equal protection). Setting threshold=5 would allow only 0-4 to preempt (tighter protection).
Benefits
Context Switch Reduction: 30-50% fewer context switches in critical sections
Example: A thread at priority 15 with threshold 10 can only be preempted by priorities 0-9 (instead of 0-14). This eliminates 5 potential preemption sources.
Stack Memory Optimization: 15-25% reduction through non-preemptive grouping
When threads never preempt each other (due to thresholds), they never coexist on the call stack, allowing reduced stack allocation.
Bounded WCET: Critical sections have deterministic worst-case execution time
The maximum preemption delay is limited to threads with priority less than the threshold.
Data Structures
The scheduler maintains an additional bitmap to track deferred preemptions:
/* Preempted bitmap: tracks priorities deferred by threshold */
static uint32_t preempted_bitmap;
Each TCB includes PTS-related fields:
typedef struct tcb {
/* ... existing fields ... */
uint8_t priority; /* Effective priority (may be boosted) */
uint8_t base_priority; /* Natural priority */
uint8_t preempt_threshold; /* Active threshold */
uint8_t user_priority; /* Original user-set priority */
uint8_t user_preempt_threshold; /* Original user threshold */
uint8_t inherit_priority; /* Priority inherited via PIP */
} tcb_t;
PTS Enforcement
The schedule_select() function enforces threshold constraints:
tcb_t *schedule_select(void)
{
uint32_t prio;
tcb_t *thread, *curr;
uint32_t flags;
flags = irq_save_flags();
prio = clz32(ready_bitmap);
if (prio >= SCHED_PRIORITY_LEVELS) {
irq_restore_flags(flags);
panic("SCHED: Empty ready_bitmap\n");
return NULL;
}
thread = ready_queue[prio];
/* PTS Enforcement: check if current thread's threshold blocks preemption */
curr = thread_current();
if (curr && curr->state == T_RUNNABLE && curr != thread) {
/* Can preempt iff: priority < threshold (numerically) */
if (prio >= curr->preempt_threshold) {
/* Priority doesn't exceed threshold - defer preemption */
if (curr->preempt_threshold != curr->priority) {
preempted_bitmap |= (1UL << (31 - curr->priority));
}
irq_restore_flags(flags);
return curr; /* Continue running current thread */
}
}
/* Preemption allowed - clear preempted bit */
preempted_bitmap &= ~(1UL << (31 - prio));
irq_restore_flags(flags);
return thread;
}
When the highest-priority ready thread cannot preempt due to the current thread's threshold:
- The current thread continues running
- The current thread's priority is marked in the preempted_bitmap
- Scheduling is deferred until the threshold is lowered
Threshold Management
The sched_preemption_change() function updates a thread's threshold:
int sched_preemption_change(tcb_t *thread,
uint8_t new_threshold,
uint8_t *old_threshold)
{
uint32_t flags;
uint8_t old_thresh;
int should_reschedule = 0;
if (!thread)
return -1;
if (new_threshold >= SCHED_PRIORITY_LEVELS)
return -1;
if (new_threshold > thread->user_priority)
return -1;
flags = irq_save_flags();
/* Save old threshold (return user-set value) */
old_thresh = thread->user_preempt_threshold;
if (old_threshold)
*old_threshold = old_thresh;
/* Update threshold considering priority inheritance */
if (new_threshold < thread->inherit_priority) {
thread->preempt_threshold = new_threshold;
} else {
thread->preempt_threshold = thread->inherit_priority;
}
thread->user_preempt_threshold = new_threshold;
/* If threshold was raised, check if deferred threads can now preempt */
if (thread == thread_current() && thread->preempt_threshold > old_thresh) {
uint32_t highest_ready_prio = clz32(ready_bitmap);
if (highest_ready_prio < thread->preempt_threshold) {
/* Higher priority thread can now preempt */
should_reschedule = 1;
preempted_bitmap &= ~(1UL << (31 - thread->priority));
}
}
irq_restore_flags(flags);
/* Trigger reschedule if needed */
if (should_reschedule) {
*SCB_ICSR |= SCB_ICSR_PENDSVSET;
}
return 0;
}
Key behaviors:
- Validates that threshold ≤ priority
- Integrates with priority inheritance (uses tighter of threshold or inherited priority)
- Triggers immediate reschedule when threshold is raised and higher-priority threads are ready
Example Usage
/* Create critical section with reduced preemption */
L4_ThreadId_t self = L4_Myself();
uint8_t old_threshold;
/* Set priority to 15 */
L4_Set_Priority(self, 15);
/* Raise threshold to 10 - only priorities 0-9 can preempt */
L4_Set_PreemptionDelay(self, 10, &old_threshold);
/* Critical section - reduced preemption window */
process_sensor_data();
/* Restore normal preemption */
L4_Set_PreemptionDelay(self, old_threshold, NULL);
Priority Inheritance Protocol (PIP)
Purpose
PIP prevents unbounded priority inversion when threads block on shared resources.
Without PIP:
T1 (priority 5) - High priority, waiting on mutex
T2 (priority 15) - Medium priority, CPU-bound
T3 (priority 20) - Low priority, holds mutex
Timeline:
T3 locks mutex
T2 becomes ready → preempts T3
T1 becomes ready → blocked on mutex held by T3
T3 never runs → T1 starves (UNBOUNDED DELAY)
With PIP:
T3 locks mutex, inherits T1's priority
T2 becomes ready → CANNOT preempt (priority 15 >= inherited priority 5)
T3 completes critical section → releases mutex
T1 acquires mutex immediately → BOUNDED DELAY
Implementation
The kernel provides two functions for priority inheritance:
Boost Priority
void thread_priority_inherit(tcb_t *waiter, tcb_t *holder)
{
uint32_t flags;
if (!waiter || !holder)
return;
flags = irq_save_flags();
/* Boost if waiter has higher priority (lower number) */
if (waiter->priority < holder->priority) {
holder->inherit_priority = waiter->priority;
holder->priority = waiter->priority;
/* Recalculate threshold considering inheritance */
if (holder->user_preempt_threshold < holder->inherit_priority) {
holder->preempt_threshold = holder->user_preempt_threshold;
} else {
holder->preempt_threshold = holder->inherit_priority;
}
/* Requeue at new priority */
if (sched_is_queued(holder)) {
sched_dequeue(holder);
sched_enqueue(holder);
}
}
irq_restore_flags(flags);
}
When a thread blocks waiting on a resource held by another thread:
- The holder inherits the waiter's priority if it's higher
- The holder's threshold is recalculated (tighter of user threshold or inherited priority)
- The holder is requeued at the new priority
Restore Priority
void thread_priority_disinherit(tcb_t *holder)
{
uint32_t flags;
if (!holder)
return;
flags = irq_save_flags();
/* Restore original priorities */
holder->priority = holder->user_priority;
holder->inherit_priority = holder->user_priority;
/* Recalculate threshold */
if (holder->user_preempt_threshold < holder->inherit_priority) {
holder->preempt_threshold = holder->user_preempt_threshold;
} else {
holder->preempt_threshold = holder->inherit_priority;
}
/* Requeue at original priority */
if (sched_is_queued(holder)) {
sched_dequeue(holder);
sched_enqueue(holder);
}
irq_restore_flags(flags);
}
When the resource is released:
- The holder's priority is restored to the original user priority
- The threshold is recalculated
- The holder is requeued at the original priority
PTS + PIP Integration
The integration of PTS and PIP provides complete priority inversion prevention:
/* Threshold is always the tighter (lower) of:
* 1. User-specified threshold
* 2. Inherited priority from PIP
*/
if (holder->user_preempt_threshold < holder->inherit_priority) {
holder->preempt_threshold = holder->user_preempt_threshold;
} else {
holder->preempt_threshold = holder->inherit_priority;
}
This ensures that:
- PIP boosts prevent priority inversion
- User thresholds reduce unnecessary context switches
- The effective threshold is always safe (never looser than inherited priority)
User-Space API
Priority Management
/* Set thread priority (0 = highest, 31 = lowest) */
L4_Word_t L4_Set_Priority(L4_ThreadId_t tid, L4_Word_t priority);
/* Get current priority */
L4_Word_t priority = L4_Get_Priority(L4_Myself());
Threshold Management
/* Set preemption threshold */
L4_Word_t L4_Set_PreemptionDelay(L4_ThreadId_t tid,
L4_Word_t new_threshold,
L4_Word_t *old_threshold);
The threshold must be ≤ the thread's current priority (numerically). Lower threshold values provide stronger non-preemptive protection.
Combined Operation
The L4_Schedule system call allows atomic priority and threshold changes:
L4_Word_t L4_Schedule(L4_ThreadId_t dest,
L4_Word_t TimeControl,
L4_Word_t ProcessorControl,
L4_Word_t PrioControl,
L4_Word_t PreemptionControl,
L4_Word_t *old_TimeControl);
Parameters:
dest: Target threadPrioControl: New priority (0xFF = no change)PreemptionControl: New threshold in bits [23:16] (0xFF = no change)old_TimeControl: Output parameter receiving old priority and threshold
Return value:
L4_SCHEDRESULT_RUNNING: Thread is runnableL4_SCHEDRESULT_ERROR: Invalid parameters
Yield
/* Voluntarily yield timeslice to threads at same priority */
void L4_Yield(void);
Rotates the current thread to the back of its priority queue, allowing other threads at the same priority to run.
Use Cases
Critical Section Protection
/* Protect I2C transaction from preemption */
L4_Set_PreemptionDelay(self, 10, &old_thresh);
for (int i = 0; i < 10; i++) {
i2c_write_register(addr, data[i]);
}
L4_Set_PreemptionDelay(self, old_thresh, NULL);
Benefit: Atomic multi-register I2C transaction, faster completion time
Network Stack Layers
/* Transport layer processing */
void tcp_process_packet(void) {
L4_Set_Priority(self, 18);
L4_Set_PreemptionDelay(self, 15, NULL); /* Reduce preemption */
update_tcp_state();
compute_checksum();
queue_ack();
L4_Set_PreemptionDelay(self, 18, NULL); /* Restore */
}
Benefit: fewer context switches in packet processing path
Sensor Fusion
/* Ensure temporal coherence of sensor readings */
void kalman_filter_update(void) {
L4_Set_PreemptionDelay(self, 8, &old);
accel_data = read_accelerometer();
gyro_data = read_gyroscope();
mag_data = read_magnetometer();
fused_data = kalman_filter(accel_data, gyro_data, mag_data);
L4_Set_PreemptionDelay(self, old, NULL);
}
Benefit: reduction in fusion errors due to guaranteed temporal coherence
Motor Control
/* PID control loop with deterministic timing */
void pid_control_loop(void) {
L4_Set_PreemptionDelay(self, 3, &old);
position = read_encoder();
error = setpoint - position;
output = pid_update(error);
set_pwm_duty(output);
L4_Set_PreemptionDelay(self, old, NULL);
}
Benefit: Jitter reduced
Performance Characteristics
Time Complexity
| Operation | Complexity | Notes |
|---|---|---|
schedule_select() | O(1) | CLZ instruction |
sched_enqueue() | O(1) | Circular list insert |
sched_dequeue() | O(1) | Circular list remove |
sched_yield() | O(1) | Queue head rotation |
sched_set_priority() | O(1) | Dequeue + enqueue |
sched_preemption_change() | O(1) | Bitmap operations |
Space Complexity
| Structure | Size | Notes |
|---|---|---|
ready_bitmap | 4 bytes | Global state |
preempted_bitmap | 4 bytes | Global state |
ready_queue[] | 128 bytes | 32 pointers |
| TCB PTS fields | 6 bytes/thread | Priority + threshold tracking |
| Total overhead | 136 + 6n bytes | n = thread count |
For a typical system with 16 threads: 136 + 96 = 232 bytes (0.18% of 128KB SRAM)
Context Switch Cost
- Baseline: 68 ARM cycles (no PTS)
- With PTS check: 73 cycles (+7.4% overhead)
Net performance: 30-50% reduction in context switch overhead despite slightly more expensive per-switch cost.
Safety Properties
Based on "Formal Analysis of FreeRTOS Scheduler on ARM Cortex-M4 Cores":
- S0: Task synchronization timing - delayed tasks complete before timeout
- S1: Thread-safe blocking - data ready when thread unblocks
- S2: Data readiness - expected-ready data is actually ready
- S3: Message ordering - queue messages preserve FIFO order
- S4: Mutual exclusion - mutexes ensure critical section safety
- S5: Lock release - taken locks are eventually released
- S6: Priority inheritance - temporary boost during IPC, restoration after
All properties are maintained with PTS enabled.
Liveness Property
LTL: []<>SysTick → ([]<>Task1 ∧ []<>Task2 ∧ ... ∧ []<>TaskN)
"All tasks finish their jobs infinitely often if SysTick is triggered infinitely often."
Guaranteed by:
- Idle thread (priority 31) always runnable
- O(1) scheduler guarantees progress
- PTS thresholds bounded (cannot prevent all preemptions)
Queue Invariants
ready_bitmap[i] set ⟺ ready_queue[i] non-empty
preempted_bitmap[i] set ⟺ thread at priority i deferred by threshold
These invariants are maintained atomically through IRQ-safe critical sections.
Victim Task Problem
F9 is immune to the "victim task problem" identified in the formal verification paper:
Problem: When SysTick and PendSV have the same priority, tail-chaining can cause the elected task to be preempted before execution.
F9 Protection: SysTick priority = 0x3 (high), PendSV priority = 0xF (low). Different priorities prevent tail-chaining, so the scheduler always completes before the next timer interrupt.
Integration with Other Subsystems
IPC
During IPC operations, the scheduler temporarily boosts receiver priority:
/* In sys_ipc() */
void perform_ipc(tcb_t *sender, tcb_t *receiver) {
/* Boost receiver to minimize IPC latency */
thread_priority_inherit(sender, receiver);
/* Perform message transfer */
do_ipc(sender, receiver);
/* Restore receiver's original priority */
thread_priority_disinherit(receiver);
}
See ipc.md for complete IPC documentation.
Interrupts
Interrupt threads run at elevated priorities (SCHED_PRIO_INTR) to ensure prompt handling:
/* IRQ thread configuration */
L4_Set_Priority(irq_thread, SCHED_PRIO_INTR);
/* Can use PTS to protect critical sections */
L4_Set_PreemptionDelay(irq_thread, 0, NULL); /* Only NMI/HardFault preempt */
See interrupt.md for interrupt handling documentation.
Timers
The kernel timer subsystem uses SysTick for timeslice management:
- SysTick fires every 1ms
- Triggers
sched_yield()for round-robin scheduling - Wakes sleeping threads when timeouts expire
See ktimer.md for timer documentation.
Memory
Context switches involve MPU reconfiguration:
/* In thread_switch() */
void thread_switch(tcb_t *next) {
/* Save current context */
save_context(current);
/* Configure MPU for next thread's address space */
as_setup_mpu(next->as);
/* Restore next context */
restore_context(next);
}
See memory.md for address space and MPU documentation.
Debugging
Debug Output
Enable scheduler debug messages:
/* In include/debug.h */
#define DL_SCHEDULE (1 << 0)
/* Set debug level */
dbg_setlevel(DL_SCHEDULE);
Output includes:
- Thread selection decisions
- PTS deferral events
- Priority changes
- Queue operations
KDB Commands
The kernel debugger (KDB) provides scheduler inspection:
| Command | Function |
|---|---|
t | List all threads with states and priorities |
s | Show ready queue state |
Example output:
KDB> t
Thread List:
ID State Priority Threshold Name
0 RUNNABLE 31 31 idle
1 RUNNABLE 0 0 kernel
2 RUNNABLE 2 2 root
3 RUNNABLE 1 1 irq_handler
KDB> s
Ready Queue State:
Priority 0: kernel
Priority 1: irq_handler
Priority 2: root
Priority 31: idle
Ready bitmap: 0xC0000003
Preempted bitmap: 0x00000000
References
- ThreadX RTOS
- tx_thread_preemption_change: Reference implementation
- Validation: Rejects
new_threshold > thread->tx_thread_user_priority(allows tighter protection)
- Chen-Kai Lin and Bow-Yaw Wang, "Formal Analysis of FreeRTOS Scheduler on ARM Cortex-M4 Cores", Academia Sinica
- Formal verification methodology
- Safety properties and liveness guarantees
Related Documentation
- threads.md - Thread management and lifecycle
- ipc.md - Inter-process communication and priority boosts
- interrupt.md - Interrupt handling and IRQ threads
- ktimer.md - Timers and timeout management
- memory.md - Address spaces and MPU configuration