Skip to content

Commit 942c9be

Browse files
committed
scheduler: add optional preemptive scheduling
The scheduler is tickless (a TSC-deadline one-shot timer), so a CPU-bound task that never blocks or yields keeps its core indefinitely. Add an optional `preemptive` feature that round-robins between ready tasks via a time slice. A new `Source::Preemption` timer slot is armed for `now + 10ms` whenever the scheduler switches to a task while others are ready, and whenever `custom_wakeup` makes a task ready (so a freshly woken task preempts a non-yielding runner soon). When the slice expires, the timer handler's existing `reschedule()` switches to the next ready task. With no contention nothing is armed, so the system stays tickless.
1 parent 30cb3f9 commit 942c9be

4 files changed

Lines changed: 59 additions & 1 deletion

File tree

Cargo.toml

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -209,6 +209,9 @@ virtio-fs = ["virtio", "dep:fuse-abi", "fuse-abi/num_enum"]
209209
## Enables the virtio-vsock driver.
210210
virtio-vsock = ["virtio"]
211211

212+
## Enable preemptive scheduling
213+
preemptive = []
214+
212215
#! ### Other Drivers
213216

214217
## Enables the _Video Graphics Array_ (VGA) driver.

src/scheduler/mod.rs

Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,11 @@ pub mod task;
3434
pub mod timer_interrupts;
3535

3636
static NO_TASKS: AtomicU32 = AtomicU32::new(0);
37+
/// Length of a preemptive scheduling time slice, in microseconds (10 ms). With
38+
/// rhyve's external-interrupt exiting this also bounds the guest VM-exit rate
39+
/// during contention (one exit per slice).
40+
#[cfg(feature = "preemptive")]
41+
const PREEMPTION_SLICE_US: u64 = 10_000;
3742
/// Map between Core ID and per-core scheduler
3843
#[cfg(feature = "smp")]
3944
static SCHEDULER_INPUTS: SpinMutex<Vec<&InterruptTicketMutex<SchedulerInput>>> =
@@ -395,6 +400,14 @@ impl PerCoreScheduler {
395400
without_interrupts(|| {
396401
let task = self.blocked_tasks.custom_wakeup(task);
397402
self.ready_queue.push(task);
403+
// The woken task is now ready. Arm the preemption timer so the running
404+
// task — which may be CPU-bound and never yield — is preempted soon and
405+
// the newcomer gets to run.
406+
#[cfg(feature = "preemptive")]
407+
{
408+
let deadline = crate::arch::processor::get_timer_ticks() + PREEMPTION_SLICE_US;
409+
timer_interrupts::create_timer_abs(timer_interrupts::Source::Preemption, deadline);
410+
}
398411
});
399412
}
400413

@@ -404,6 +417,16 @@ impl PerCoreScheduler {
404417
without_interrupts(|| {
405418
let task = self.blocked_tasks.custom_wakeup(task);
406419
self.ready_queue.push(task);
420+
// The woken task is now ready on this core. Arm the preemption timer
421+
// so the running (possibly non-yielding) task is preempted soon.
422+
#[cfg(feature = "preemptive")]
423+
{
424+
let deadline = crate::arch::processor::get_timer_ticks() + PREEMPTION_SLICE_US;
425+
timer_interrupts::create_timer_abs(
426+
timer_interrupts::Source::Preemption,
427+
deadline,
428+
);
429+
}
407430
});
408431
} else {
409432
get_scheduler_input(task.get_core_id())
@@ -797,6 +820,16 @@ impl PerCoreScheduler {
797820
return None;
798821
}
799822

823+
// Preemptive round-robin: arm a one-shot time-slice timer for the task we
824+
// are about to run, but only while other tasks are ready — with no
825+
// contention the system stays tickless. When the slice expires, the timer
826+
// interrupt's `reschedule()` switches to the next ready task.
827+
#[cfg(feature = "preemptive")]
828+
if !self.ready_queue.is_empty() {
829+
let deadline = crate::arch::processor::get_timer_ticks() + PREEMPTION_SLICE_US;
830+
timer_interrupts::create_timer_abs(timer_interrupts::Source::Preemption, deadline);
831+
}
832+
800833
// Tell the scheduler about the new task.
801834
debug!(
802835
"Switching task from {} to {} (stack {:#X} => {:p})",

src/scheduler/task/mod.rs

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -521,6 +521,12 @@ impl BlockedTaskQueue {
521521
borrowed.status = TaskStatus::Ready;
522522
}
523523

524+
#[cfg(feature = "preemptive")]
525+
#[inline(always)]
526+
pub fn is_empty(&self) -> bool {
527+
self.list.is_empty()
528+
}
529+
524530
/// Blocks the given task for `wakeup_time` ticks, or indefinitely if None is given.
525531
pub fn add(&mut self, task: Rc<RefCell<Task>>, wakeup_time: Option<u64>) {
526532
{

src/scheduler/timer_interrupts.rs

Lines changed: 17 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,11 @@ use crate::set_oneshot_timer;
1111
pub enum Source {
1212
Network,
1313
Scheduler,
14+
/// Preemptive time-slice timer (round-robin between equally prioritized,
15+
/// ready tasks). Only armed with the `preemptive` feature and when there is
16+
/// contention.
17+
#[cfg(feature = "preemptive")]
18+
Preemption,
1419
}
1520

1621
/// A slot in the timer list. Each source is represented once. This is so that
@@ -25,9 +30,15 @@ pub struct Slot {
2530
wakeup_time: u64,
2631
}
2732

33+
#[cfg(feature = "preemptive")]
34+
const NUMBER_OF_SLOTS: usize = 3;
35+
36+
#[cfg(not(feature = "preemptive"))]
37+
const NUMBER_OF_SLOTS: usize = 2;
38+
2839
// List of timers with one entry for every possible source.
2940
#[derive(Debug)]
30-
pub struct TimerList([Slot; 2]);
41+
pub struct TimerList([Slot; NUMBER_OF_SLOTS]);
3142

3243
impl TimerList {
3344
pub fn new() -> Self {
@@ -40,6 +51,11 @@ impl TimerList {
4051
source: Source::Scheduler,
4152
wakeup_time: u64::MAX,
4253
},
54+
#[cfg(feature = "preemptive")]
55+
Slot {
56+
source: Source::Preemption,
57+
wakeup_time: u64::MAX,
58+
},
4359
])
4460
}
4561

0 commit comments

Comments
 (0)