From 82f9b6a81e028a0e146d2c039e5de4e0be525d7c Mon Sep 17 00:00:00 2001 From: Yuuki Takano Date: Tue, 6 Oct 2026 18:59:47 +0900 Subject: [PATCH 1/2] fix(scheduler): do not preempt a running task of equal priority `Task::cmp` breaks priority ties by task ID (larger ID is greater) so that the preemption-pending heaps have a total order. The preemption decisions reused that ordering via `task > target_task`, so a task woken with the same priority as a running task preempted it whenever its ID was larger. This violates the FIFO order within a priority level, bypasses the RR time quantum, and costs a context switch with no scheduling benefit. It is most visible with small CPU sets, where "all CPUs in the set are busy" is the common case. Add `Task::preempts()`, which requires a strictly higher priority, and use it in every preemption decision: `invoke_preemption*()` of PrioritizedFIFO, PrioritizedRR, GEDF and ClusteredEDF, and the check in `do_preemption()` that re-queues a pending task when the running task on the target CPU has changed. `Task::cmp` is left as is for the heaps. Co-Authored-By: Claude Fable 5.1 --- .../src/scheduler/clustered_edf.rs | 2 +- awkernel_async_lib/src/scheduler/gedf.rs | 2 +- .../src/scheduler/prioritized_fifo.rs | 2 +- .../src/scheduler/prioritized_rr.rs | 2 +- awkernel_async_lib/src/task.rs | 18 ++++++++++++++++++ awkernel_async_lib/src/task/preempt.rs | 6 +++++- mdbook/src/internal/scheduler.md | 2 +- 7 files changed, 28 insertions(+), 6 deletions(-) diff --git a/awkernel_async_lib/src/scheduler/clustered_edf.rs b/awkernel_async_lib/src/scheduler/clustered_edf.rs index 3bbde23f8..15ba4deb5 100644 --- a/awkernel_async_lib/src/scheduler/clustered_edf.rs +++ b/awkernel_async_lib/src/scheduler/clustered_edf.rs @@ -197,7 +197,7 @@ impl ClusteredEDFScheduler { return false; }; - if task > target_task { + if task.preempts(&target_task) { push_preemption_pending(victim_cpu, task); let preempt_irq = awkernel_lib::interrupt::get_preempt_irq(); set_need_preemption(target_task.id, victim_cpu); diff --git a/awkernel_async_lib/src/scheduler/gedf.rs b/awkernel_async_lib/src/scheduler/gedf.rs index a2060d362..2666d4318 100644 --- a/awkernel_async_lib/src/scheduler/gedf.rs +++ b/awkernel_async_lib/src/scheduler/gedf.rs @@ -182,7 +182,7 @@ impl GEDFScheduler { .unwrap(); let (target_task, target_cpu) = preemption_target; - if task > target_task { + if task.preempts(&target_task) { push_preemption_pending(target_cpu, task); let preempt_irq = awkernel_lib::interrupt::get_preempt_irq(); set_need_preemption(target_task.id, target_cpu); diff --git a/awkernel_async_lib/src/scheduler/prioritized_fifo.rs b/awkernel_async_lib/src/scheduler/prioritized_fifo.rs index ead5db77b..6c2091c23 100644 --- a/awkernel_async_lib/src/scheduler/prioritized_fifo.rs +++ b/awkernel_async_lib/src/scheduler/prioritized_fifo.rs @@ -135,7 +135,7 @@ impl PrioritizedFIFOScheduler { .unwrap(); let (target_task, target_cpu) = preemption_target; - if task > target_task { + if task.preempts(&target_task) { push_preemption_pending(target_cpu, task); let preempt_irq = awkernel_lib::interrupt::get_preempt_irq(); set_need_preemption(target_task.id, target_cpu); diff --git a/awkernel_async_lib/src/scheduler/prioritized_rr.rs b/awkernel_async_lib/src/scheduler/prioritized_rr.rs index af56b218f..2b6c2e105 100644 --- a/awkernel_async_lib/src/scheduler/prioritized_rr.rs +++ b/awkernel_async_lib/src/scheduler/prioritized_rr.rs @@ -161,7 +161,7 @@ impl PrioritizedRRScheduler { .unwrap(); let (target_task, target_cpu) = preemption_target; - if task > target_task { + if task.preempts(&target_task) { push_preemption_pending(target_cpu, task); let preempt_irq = awkernel_lib::interrupt::get_preempt_irq(); set_need_preemption(target_task.id, target_cpu); diff --git a/awkernel_async_lib/src/task.rs b/awkernel_async_lib/src/task.rs index b4a36f25a..ad3a10b02 100644 --- a/awkernel_async_lib/src/task.rs +++ b/awkernel_async_lib/src/task.rs @@ -80,6 +80,24 @@ impl Task { pub fn scheduler_name(&self) -> SchedulerType { self.scheduler.scheduler_name() } + + /// True if `self` must preempt `other`, i.e. `self` has a strictly higher + /// priority (scheduler priority, then task priority; see `PriorityInfo`). + /// + /// Equal priorities never preempt. Within a priority level the schedulers + /// are FIFO (PrioritizedFIFO, ClusteredEDF/GEDF on equal deadlines) or + /// rotate by their own time quantum (PrioritizedRR), so a task woken while + /// an equal-priority task is running waits in the run queue instead of + /// displacing it. + /// + /// This is deliberately not `self > other`: [`Ord`] for `Task` breaks + /// priority ties by task ID to give the preemption-pending heaps a total + /// order, and that tie-break must not decide whether a running task is + /// preempted. + #[inline(always)] + pub(crate) fn preempts(&self, other: &Self) -> bool { + self.priority > other.priority + } } impl PartialEq for Task { diff --git a/awkernel_async_lib/src/task/preempt.rs b/awkernel_async_lib/src/task/preempt.rs index 9e460b751..29b548a2f 100644 --- a/awkernel_async_lib/src/task/preempt.rs +++ b/awkernel_async_lib/src/task/preempt.rs @@ -153,7 +153,11 @@ unsafe fn do_preemption() { { let current_task = get_task(current_task_id).unwrap(); - if current_task > next { + // The running task may have changed since the IPI was requested. Only + // a strictly higher-priority pending task preempts it; on equal + // priority the pending task is re-queued instead (FIFO within a + // priority level). + if !next.preempts(¤t_task) { remove_preemption_pending(cpu_id, next.id); next.scheduler.wake_task(next); return; diff --git a/mdbook/src/internal/scheduler.md b/mdbook/src/internal/scheduler.md index 520ee2cc7..6f5628158 100644 --- a/mdbook/src/internal/scheduler.md +++ b/mdbook/src/internal/scheduler.md @@ -98,7 +98,7 @@ The PrioritizedFIFO scheduler is implemented in [prioritized_fifo.rs](https://gi The scheduler uses a `PriorityQueue` as its run queue. When a task is enqueued through `wake_task()`, the priority is extracted from `SchedulerType::PrioritizedFIFO(priority)` and used to insert the task into the priority queue. The get_next() method retrieves the task at the head of the highest-priority non-empty queue. -The scheduler implements preemption via `invoke_preemption()`, which evaluates all currently running tasks and determines if the newly awakened task should preempt any of them. If preemption is triggered, the scheduler sends an IPI to the target CPU and updates the preemption pending queue. +The scheduler implements preemption via `invoke_preemption()`, which evaluates all currently running tasks and determines if the newly awakened task should preempt any of them. If preemption is triggered, the scheduler sends an IPI to the target CPU and updates the preemption pending queue. Preemption requires a strictly higher priority (`Task::preempts()`): a task woken while an equal-priority task is running is enqueued behind it, so the FIFO order within a priority level also holds against running tasks. The same rule applies to the other schedulers and to the preemption handler itself, which re-queues a pending task if the task running on the target CPU no longer has a strictly lower priority. ### PrioritizedRR Scheduler From 25a0d374edce316db7f8939456b72c2024411c9a Mon Sep 17 00:00:00 2001 From: Yuuki Takano Date: Thu, 8 Oct 2026 18:07:00 +0900 Subject: [PATCH 2/2] fix(preempt): let the RR time quantum rotate equal-priority tasks `PrioritizedRRScheduler::invoke_preemption_tick()` pushes the next runnable task to the preemption-pending heap without a priority check, and in RR that task normally has the same priority as the running one. With the strict comparison introduced in 82f9b6a8, `do_preemption()` re-queued it every time, so the RR time quantum never switched tasks and the tick re-sent the IPI on every primary loop iteration. Let an equal-priority pending task preempt the running task when that task is PrioritizedRR. Every wake path requires a strictly higher priority, so this is the only case where equal priority preempts. Document the exception in `Task::preempts()` and the mdbook. Co-Authored-By: Claude Fable 5.1 --- awkernel_async_lib/src/task.rs | 7 +++++++ awkernel_async_lib/src/task/preempt.rs | 22 ++++++++++++++++++++-- mdbook/src/internal/scheduler.md | 4 ++-- 3 files changed, 29 insertions(+), 4 deletions(-) diff --git a/awkernel_async_lib/src/task.rs b/awkernel_async_lib/src/task.rs index ad3a10b02..170638d37 100644 --- a/awkernel_async_lib/src/task.rs +++ b/awkernel_async_lib/src/task.rs @@ -90,6 +90,13 @@ impl Task { /// an equal-priority task is running waits in the run queue instead of /// displacing it. /// + /// The one exception is the PrioritizedRR time quantum: + /// `PrioritizedRRScheduler::invoke_preemption_tick` pushes the next + /// runnable task to the preemption-pending heap without a priority check, + /// and `preempt::do_preemption` lets such an equal-priority pending task + /// preempt a running PrioritizedRR task so that the quantum rotates tasks + /// of the same priority. + /// /// This is deliberately not `self > other`: [`Ord`] for `Task` breaks /// priority ties by task ID to give the preemption-pending heaps a total /// order, and that tie-break must not decide whether a running task is diff --git a/awkernel_async_lib/src/task/preempt.rs b/awkernel_async_lib/src/task/preempt.rs index 29b548a2f..df30685fc 100644 --- a/awkernel_async_lib/src/task/preempt.rs +++ b/awkernel_async_lib/src/task/preempt.rs @@ -1,5 +1,7 @@ use crate::{ - scheduler::{move_preemption_pending, peek_preemption_pending, remove_preemption_pending}, + scheduler::{ + move_preemption_pending, peek_preemption_pending, remove_preemption_pending, SchedulerType, + }, task::{get_current_task, get_task, set_current_task, Task}, }; use alloc::{collections::VecDeque, sync::Arc}; @@ -157,7 +159,23 @@ unsafe fn do_preemption() { // a strictly higher-priority pending task preempts it; on equal // priority the pending task is re-queued instead (FIFO within a // priority level). - if !next.preempts(¤t_task) { + // + // The one exception is the PrioritizedRR time quantum. Every wake + // path requires a strictly higher priority (`Task::preempts`), so a + // pending task of equal priority can only come from + // `PrioritizedRRScheduler::invoke_preemption_tick`, which pushes the + // next runnable task without a priority check when the quantum of + // the running task has expired. Let it through when the running task + // is PrioritizedRR so that the quantum rotates equal-priority tasks. + // `PriorityInfo` includes the scheduler priority, so equal priority + // implies that `next` is a PrioritizedRR task too. + let quantum_rotation = !current_task.preempts(&next) + && matches!( + current_task.scheduler_name(), + SchedulerType::PrioritizedRR(_) + ); + + if !next.preempts(¤t_task) && !quantum_rotation { remove_preemption_pending(cpu_id, next.id); next.scheduler.wake_task(next); return; diff --git a/mdbook/src/internal/scheduler.md b/mdbook/src/internal/scheduler.md index 6f5628158..3107f683a 100644 --- a/mdbook/src/internal/scheduler.md +++ b/mdbook/src/internal/scheduler.md @@ -98,7 +98,7 @@ The PrioritizedFIFO scheduler is implemented in [prioritized_fifo.rs](https://gi The scheduler uses a `PriorityQueue` as its run queue. When a task is enqueued through `wake_task()`, the priority is extracted from `SchedulerType::PrioritizedFIFO(priority)` and used to insert the task into the priority queue. The get_next() method retrieves the task at the head of the highest-priority non-empty queue. -The scheduler implements preemption via `invoke_preemption()`, which evaluates all currently running tasks and determines if the newly awakened task should preempt any of them. If preemption is triggered, the scheduler sends an IPI to the target CPU and updates the preemption pending queue. Preemption requires a strictly higher priority (`Task::preempts()`): a task woken while an equal-priority task is running is enqueued behind it, so the FIFO order within a priority level also holds against running tasks. The same rule applies to the other schedulers and to the preemption handler itself, which re-queues a pending task if the task running on the target CPU no longer has a strictly lower priority. +The scheduler implements preemption via `invoke_preemption()`, which evaluates all currently running tasks and determines if the newly awakened task should preempt any of them. If preemption is triggered, the scheduler sends an IPI to the target CPU and updates the preemption pending queue. Preemption requires a strictly higher priority (`Task::preempts()`): a task woken while an equal-priority task is running is enqueued behind it, so the FIFO order within a priority level also holds against running tasks. The same rule applies to the other schedulers and to the preemption handler itself, which re-queues a pending task if the task running on the target CPU no longer has a strictly lower priority, except for the PrioritizedRR time-quantum rotation described below. ### PrioritizedRR Scheduler @@ -106,7 +106,7 @@ The PrioritizedRR (Prioritized Round Robin) scheduler is implemented in [priorit The scheduler maintains a `PriorityQueue` similar to PrioritizedFIFO, but adds time quantum management with a default interval of 4ms (4,000 microseconds). The scheduler provides two preemption mechanisms: `invoke_preemption_wake()` for priority-based preemption when tasks are awakened, and `invoke_preemption_tick()` for time quantum-based preemption. -The `invoke_preemption_tick()` method is called periodically on primary CPU to check if the currently running task has exceeded its time quantum. It compares the elapsed execution time against the configured interval and triggers preemption by sending an IPI if the quantum is exceeded. +The `invoke_preemption_tick()` method is called periodically on primary CPU to check if the currently running task has exceeded its time quantum. It compares the elapsed execution time against the configured interval and triggers preemption by sending an IPI if the quantum is exceeded. The task pushed by the tick usually has the same priority as the running one; `do_preemption()` lets such an equal-priority pending task preempt a running PrioritizedRR task, which is the only case where equal priority preempts. ### Panicked Scheduler