Skip to main content

ostd/task/scheduler/
mod.rs

1// SPDX-License-Identifier: MPL-2.0
2//! Task scheduling.
3//!
4//! # Scheduler Injection
5//!
6//! The task scheduler of an OS is a complex beast,
7//! and the most suitable scheduling algorithm often depends on the target usage scenario.
8//! To avoid code bloat and offer flexibility,
9//! OSTD does not include a gigantic, one-size-fits-all task scheduler.
10//! Instead, it allows the client to implement a custom scheduler (in safe Rust, of course)
11//! and register it with OSTD.
12//! This feature is known as **scheduler injection**.
13//!
14//! The client kernel performs scheduler injection via the [`inject_scheduler`] API.
15//! This API should be called as early as possible during kernel initialization,
16//! before any [`Task`]-related APIs are used.
17//! This requirement is reasonable since `Task`s depend on the scheduler.
18//!
19//! # Scheduler Abstraction
20//!
21//! The `inject_scheduler` API accepts an object implementing the [`Scheduler`] trait,
22//! which abstracts over any SMP-aware task scheduler.
23//! Whenever an OSTD client spawns a new task (via [`crate::task::TaskOptions`])
24//! or wakes a sleeping task (e.g., via [`crate::sync::Waker`]),
25//! OSTD internally forwards the corresponding `Arc<Task>`
26//! to the scheduler by invoking the [`Scheduler::enqueue`] method.
27//! This allows the injected scheduler to manage all runnable tasks.
28//!
29//! Each enqueued task is dispatched to one of the per-CPU local runqueues,
30//! which manage all runnable tasks on a specific CPU.
31//! A local runqueue is abstracted by the [`LocalRunQueue`] trait.
32//! OSTD accesses the local runqueue of the current CPU
33//! via [`Scheduler::local_rq_with`] or [`Scheduler::mut_local_rq_with`],
34//! which return immutable and mutable references to `dyn LocalRunQueue`, respectively.
35//!
36//! The [`LocalRunQueue`] trait enables OSTD to inspect and manipulate local runqueues.
37//! For instance, OSTD invokes the [`LocalRunQueue::pick_next`] method
38//! to let the scheduler select the next task to run.
39//! OSTD then performs a context switch to that task,
40//! which becomes the _current_ running task, accessible via [`LocalRunQueue::current`].
41//! When the current task is about to sleep (e.g., via [`crate::sync::Waiter`]),
42//! OSTD removes it from the local runqueue using [`LocalRunQueue::dequeue_current`].
43//!
44//! The interfaces of `Scheduler` and `LocalRunQueue` are simple
45//! yet (perhaps surprisingly) powerful enough to support
46//! even complex and advanced task scheduler implementations.
47//! Scheduler implementations are free to employ any load-balancing strategy
48//! to dispatch enqueued tasks across local runqueues,
49//! and each local runqueue is free to choose any prioritization strategy
50//! for selecting the next task to run.
51//! Based on OSTD's scheduling abstractions,
52//! the Asterinas kernel has successfully supported multiple Linux scheduling classes,
53//! including both real-time and normal policies.
54//!
55//! # Safety Impact
56//!
57//! While OSTD delegates scheduling decisions to the injected task scheduler,
58//! it verifies these decisions to avoid undefined behavior.
59//! In particular, it enforces the following safety invariant:
60//!
61//! > A task must not be scheduled to run on more than one CPU at a time.
62//!
63//! Violating this invariant—e.g., running the same task on two CPUs concurrently—
64//! can have catastrophic consequences,
65//! as the task's stack and internal state may be corrupted by concurrent modifications.
66// mod fifo_scheduler;
67#[cfg(feature = "irc11")]
68mod thread_view;
69// pub mod info;
70use alloc::sync::Arc;
71use spin::Once;
72
73// use super::{preempt::cpu_local, processor, Task};
74use super::Task;
75// use crate::{
76//     cpu::{CpuId, CpuSet, PinCurrentCpu},
77//     prelude::*,
78//     task::disable_preempt,
79//     timer,
80// };
81use crate::specs::mm::cpu::CpuId;
82
83#[cfg(feature = "irc11")]
84pub use self::thread_view::SchedulerIrc11State;
85
86/// Injects a custom implementation of task scheduler into OSTD.
87///
88/// This function can only be called once and must be called during the initialization phase of kernel,
89/// before any [`Task`]-related APIs are invoked.
90pub fn inject_scheduler(scheduler: &'static dyn Scheduler<Task>) {
91    /* SCHEDULER.call_once(|| scheduler);
92
93    timer::register_callback(|| {
94        SCHEDULER.get().unwrap().mut_local_rq_with(&mut |local_rq| {
95            let should_pick_next = local_rq.update_current(UpdateFlags::Tick);
96            if should_pick_next {
97                cpu_local::set_need_preempt();
98            }
99        })
100    }); */
101}
102
103static SCHEDULER: Once<&'static dyn Scheduler<Task>> = Once::new();
104
105/// A SMP-aware task scheduler.
106pub trait Scheduler<T = Task>: Sync + Send {
107    /// Enqueues a runnable task.
108    ///
109    /// The scheduler implementer can perform load-balancing or some time accounting work here.
110    ///
111    /// The newly-enqueued task may have a higher priority than the currently running one on a CPU
112    /// and thus should preempt the latter.
113    /// In this case, this method returns the ID of that CPU.
114    fn enqueue(&self, runnable: Arc<T>, flags: EnqueueFlags) -> Option<CpuId>;
115
116    /// Gets an immutable access to the local runqueue of the current CPU.
117    fn local_rq_with(&self, f: &mut dyn FnMut(&dyn LocalRunQueue<T>));
118
119    /// Gets a mutable access to the local runqueue of the current CPU.
120    fn mut_local_rq_with(&self, f: &mut dyn FnMut(&mut dyn LocalRunQueue<T>));
121}
122
123/// A per-CPU, local runqueue.
124///
125/// This abstraction allows OSTD to inspect and manipulate local runqueues.
126///
127/// Conceptually, a local runqueue maintains:
128/// 1. A priority queue of runnable tasks.
129///    The definition of "priority" is left to the concrete implementation.
130/// 2. The current running task.
131///
132/// # Interactions with OSTD
133///
134/// ## Overview
135///
136/// It is crucial for implementers of `LocalRunQueue`
137/// to understand how OSTD interacts with local runqueues.
138///
139/// A local runqueue is consulted by OSTD in response to one of four scheduling events:
140/// - **Yielding**, triggered by [`Task::yield_now`], where the current task voluntarily gives up CPU time.
141/// - **Sleeping**, triggered by [`crate::sync::Waiter::wait`]
142///   or any synchronization primitive built upon it (e.g., [`crate::sync::WaitQueue`], [`crate::sync::Mutex`]),
143///   which blocks the current task until a wake-up event occurs.
144/// - **Ticking**, triggered periodically by the system timer
145///   (see [`crate::arch::timer::TIMER_FREQ`]),
146///   which provides an opportunity to do time accounting and consider preemption.
147/// - **Exiting**, triggered when the execution logic of a task has come to an end,
148///   which informs the scheduler that the task is exiting and will never be enqueued again.
149///
150/// The general workflow for OSTD to handle a scheduling event is as follows:
151/// 1. Acquire exclusive access to the local runqueue using [`Scheduler::mut_local_rq_with`].
152/// 2. Call [`LocalRunQueue::update_current`] to update the current task's state,
153///    returning a boolean value that indicates
154///    whether the current task should and can be replaced with another runnable task.
155/// 3. If the task is about to sleep or exit, call [`LocalRunQueue::dequeue_current`]
156///    to remove it from the runqueue.
157/// 4. If the return value of `update_current` in Step 2 is true,
158///    then select the next task to run with [`LocalRunQueue::pick_next`].
159///
160/// ## When to Pick the Next Task?
161///
162/// As shown above,
163/// OSTD guarantees that `pick_next` is only called
164/// when the current task should and can be replaced.
165/// This avoids unnecessary invocations and improves efficiency.
166///
167/// But under what conditions should the current task be replaced?
168/// Two criteria must be met:
169/// 1. There exists at least one other runnable task in the runqueue.
170/// 2. That task should preempt the current one, if present.
171///
172/// Some implications of these rules:
173/// - If the runqueue is empty, `update_current` must return `false`—there's nothing to run.
174/// - If the runqueue is non-empty but the current task is absent,
175///   `update_current` should return `true`—anything is better than nothing.
176/// - If the runqueue is non-empty and the flag is `UpdateFlags::WAIT`,
177///   `update_current` should also return `true`,
178///   because the current task is about to block.
179/// - In other cases, the return value depends on the scheduler's prioritization policy.
180///   For instance, a real-time task may only be preempted by a higher-priority task
181///   or if it explicitly yields.
182///   A normal task under Linux's CFS may be preempted by a task with smaller vruntime,
183///   but never by the idle task.
184///
185/// When OSTD is unsure about whether the current task should or can be replaced,
186/// it will invoke [`LocalRunQueue::try_pick_next`], the fallible version of `pick_next`.
187///
188/// ## Internal Working
189///
190/// To guide scheduler implementers,
191/// we provide a simplified view of how OSTD interacts with local runqueues _internally_
192/// in order to handle the four scheduling events.
193///
194/// ### Yielding
195///
196/// ```
197/// # use ostd::prelude::*;
198/// # use ostd::task::{*, scheduler::*};
199/// #
200/// # fn switch_to(next: Arc<Task>) {}
201/// #
202/// /// Yields the current task.
203/// fn yield(scheduler: &'static dyn Scheduler) {
204///     let next_task_opt: Option<Arc<Task>> = scheduler.mut_local_rq_with(|local_rq| {
205///         let should_pick_next = local_rq.update_current(UpdateFlags::Yield);
206///         should_pick_next.then(|| local_rq.pick_next().clone())
207///     });
208///     let Some(next_task) = next_task_opt {
209///         switch_to(next_task);
210///     }
211/// }
212/// ```
213///
214/// ### Sleeping
215///
216/// ```
217/// # use ostd::prelude::*;
218/// # use ostd::task::{*, scheduler::*};
219/// #
220/// # fn switch_to(next: Arc<Task>) {}
221/// #
222/// /// Puts the current task to sleep.
223/// ///
224/// /// The function takes a closure to check if the task is woken.
225/// /// This function is used internally to guard against race conditions,
226/// /// where the task is woken just before it goes to sleep.
227/// fn sleep<F: Fn() -> bool>(scheduler: &'static dyn Scheduler, is_woken: F) {
228///     let mut next_task_opt: Option<Arc<Task>> = None;
229///     let mut is_first_try = true;
230///     while scheduler.mut_local_rq_with(|local_rq| {
231///         if is_first_try {
232///             if is_woken() {
233///                 return false; // exit loop
234///             }
235///             is_first_try = false;
236///
237///             let should_pick_next = local_rq.update_current(UpdateFlags::Wait);
238///             let _current = local_rq.dequeue_current();
239///             if !should_pick_next {
240///                 return true; // continue loop
241///             }
242///             next_task_opt = Some(local_rq.pick_next().clone());
243///             false // exit loop
244///         } else {
245///             next_task_opt = local_rq.try_pick_next().cloned();
246///             next_task_opt.is_none()
247///         }
248///     }) {}
249///     let Some(next_task) = next_task_opt {
250///         switch_to(next_task);
251///     }
252/// }
253/// ```
254///
255/// ### Ticking
256///
257/// ```
258/// # use ostd::prelude::*;
259/// # use ostd::task::{*, scheduler::*};
260/// #
261/// # fn switch_to(next: Arc<Task>) {}
262/// # mod cpu_local {
263/// #     fn set_need_preempt();
264/// #     fn should_preempt() -> bool;
265/// # }
266/// #
267/// /// A callback to be invoked periodically by the timer interrupt.
268/// fn on_tick(scheduler: &'static dyn Scheduler) {
269///     scheduler.mut_local_rq_with(|local_rq| {
270///         let should_pick_next = local_rq.update_current(UpdateFlags::Tick);
271///         if should_pick_next {
272///             cpu_local::set_need_preempt();
273///         }
274///     });
275/// }
276///
277/// /// A preemption point, called at an earliest convenient timing
278/// /// when OSTD can safely preempt the current running task.
279/// fn might_preempt(scheduler: &'static dyn Scheduler) {
280///     if !cpu_local::should_preempt() {
281///         return;
282///     }
283///     let next_task_opt: Option<Arc<Task>> = scheduler
284///         .mut_local_rq_with(|local_rq| local_rq.try_pick_next().cloned())
285///     let Some(next_task) = next_task_opt {
286///         switch_to(next_task);
287///     }
288/// }
289/// ```
290///
291/// ### Exiting
292///
293/// ```
294/// # use ostd::prelude::*;
295/// # use ostd::task::{*, scheduler::*};
296/// #
297/// # fn switch_to(next: Arc<Task>) {}
298/// #
299/// /// Exits the current task.
300/// fn exit(scheduler: &'static dyn Scheduler) {
301///     let mut next_task_opt: Option<Arc<Task>> = None;
302///     let mut is_first_try = true;
303///     while scheduler.mut_local_rq_with(|local_rq| {
304///         if is_first_try {
305///             is_first_try = false;
306///             let should_pick_next = local_rq.update_current(UpdateFlags::Exit);
307///             let _current = local_rq.dequeue_current();
308///             if !should_pick_next {
309///                 return true; // continue loop
310///             }
311///             next_task_opt = Some(local_rq.pick_next().clone());
312///             false // exit loop
313///         } else {
314///             next_task_opt = local_rq.try_pick_next().cloned();
315///             next_task_opt.is_none()
316///         }
317///     }) {}
318///     let next_task = next_task_opt.unwrap();
319///     switch_to(next_task);
320/// }
321/// ```
322pub trait LocalRunQueue<T = Task> {
323    /// Gets the current runnable task.
324    fn current(&self) -> Option<&Arc<T>>;
325
326    /// Updates the current runnable task's scheduling statistics and
327    /// potentially its position in the runqueue.
328    ///
329    /// The return value of this method indicates whether an invocation of `pick_next` should be followed
330    /// to find another task to replace the current one.
331    #[must_use]
332    fn update_current(&mut self, flags: UpdateFlags) -> bool;
333
334    /// Picks the next runnable task.
335    ///
336    /// This method instructs the local runqueue to pick the next runnable task and replace the current one.
337    /// A reference to the new "current" task will be returned by this method.
338    /// If the "old" current task presents, then it is still runnable and thus remains in the runqueue.
339    ///
340    /// # Panics
341    ///
342    /// As explained in the type-level Rust doc,
343    /// this method will only be invoked by OSTD after a call to `update_current` returns true.
344    /// In case that this contract is broken by the caller,
345    /// the implementer is free to exhibit any undesirable or incorrect behaviors, include panicking.
346    fn pick_next(&mut self) -> &Arc<T> {
347        self.try_pick_next().unwrap()
348    }
349
350    /// Tries to pick the next runnable task.
351    ///
352    /// This method instructs the local runqueue to pick the next runnable task on a best-effort basis.
353    /// If such a task can be picked, then this task supersedes the current task and
354    /// the new the method returns a reference to the new "current" task.
355    /// If the "old" current task presents, then it is still runnable and thus remains in the runqueue.
356    fn try_pick_next(&mut self) -> Option<&Arc<T>>;
357
358    /// Removes the current runnable task from runqueue.
359    ///
360    /// This method returns the current runnable task.
361    /// If there is no current runnable task, this method returns `None`.
362    fn dequeue_current(&mut self) -> Option<Arc<T>>;
363}
364
365/// Possible triggers of an `enqueue` action.
366#[derive(PartialEq, Copy, Clone)]
367pub enum EnqueueFlags {
368    /// Spawn a new task.
369    Spawn,
370    /// Wake a sleeping task.
371    Wake,
372}
373
374/// Possible triggers of an `update_current` action.
375#[derive(PartialEq, Copy, Clone)]
376pub enum UpdateFlags {
377    /// Timer interrupt.
378    Tick,
379    /// Task waiting.
380    Wait,
381    /// Task yielding.
382    Yield,
383    /// Task exiting.
384    Exit,
385}
386
387/// Preempts the current task.
388#[track_caller]
389pub(crate) fn might_preempt() {
390    /*
391    if !cpu_local::should_preempt() {
392        return;
393    }
394    reschedule(|local_rq| {
395        let next_task_opt = local_rq.try_pick_next();
396        if let Some(next_task) = next_task_opt {
397            ReschedAction::SwitchTo(next_task.clone())
398        } else {
399            ReschedAction::DoNothing
400        }
401    })
402    */
403}
404
405/// Blocks the current task unless `has_unparked()` returns `true`.
406///
407/// Note that this method may return due to spurious wake events. It's the caller's responsibility
408/// to detect them (if necessary).
409#[track_caller]
410pub(crate) fn park_current<F>(has_unparked: F)
411where
412    F: Fn() -> bool,
413{
414    let mut current = None;
415    let mut is_first_try = true;
416
417    reschedule(|local_rq: &mut dyn LocalRunQueue| {
418        let next_task_opt = if is_first_try {
419            if has_unparked() {
420                return ReschedAction::DoNothing;
421            }
422            is_first_try = false;
423
424            // Note the race conditions: the current task may be woken after the above `has_unparked`
425            // check, but before the below `dequeue_current` action, we need to make sure that the
426            // wakeup event isn't lost.
427            //
428            // Currently, for the FIFO and CFS scheduler, `Scheduler::enqueue` will try to lock `local_rq`
429            // when the above race condition occurs, so it will wait until we finish calling the
430            // `dequeue_current` method and nothing bad will happen. This may need to be revisited
431            // after more complex schedulers are introduced.
432
433            let should_pick_next = local_rq.update_current(UpdateFlags::Wait);
434            current = local_rq.dequeue_current();
435            should_pick_next.then(|| local_rq.pick_next())
436        } else {
437            local_rq.try_pick_next()
438        };
439
440        if let Some(next_task) = next_task_opt {
441            if Arc::ptr_eq(current.as_ref().unwrap(), next_task) {
442                // The current task has been woken and picked as the next runnable task.
443                return ReschedAction::DoNothing;
444            }
445            return ReschedAction::SwitchTo(next_task.clone());
446        }
447
448        ReschedAction::Retry
449    });
450}
451
452/// Unblocks a target task.
453pub(crate) fn unpark_target(runnable: Arc<Task>) {
454    let preempt_cpu = SCHEDULER
455        .get()
456        .unwrap()
457        .enqueue(runnable, EnqueueFlags::Wake);
458    if let Some(preempt_cpu_id) = preempt_cpu {
459        // set_need_preempt(preempt_cpu_id);
460    }
461}
462
463/// Enqueues a newly built task.
464///
465/// Note that the new task is not guaranteed to run at once.
466/*
467#[track_caller]
468pub(super) fn run_new_task(runnable: Arc<Task>) {
469    // FIXME: remove this check for `SCHEDULER`.
470    // Currently OSTD cannot know whether its user has injected a scheduler.
471    if !SCHEDULER.is_completed() {
472        fifo_scheduler::init();
473    }
474
475    let preempt_cpu = SCHEDULER
476        .get()
477        .unwrap()
478        .enqueue(runnable, EnqueueFlags::Spawn);
479    if let Some(preempt_cpu_id) = preempt_cpu {
480        set_need_preempt(preempt_cpu_id);
481    }
482
483    might_preempt();
484}
485*/
486
487/*
488fn set_need_preempt(cpu_id: CpuId) {
489    let preempt_guard = disable_preempt();
490
491    if preempt_guard.current_cpu() == cpu_id {
492        cpu_local::set_need_preempt();
493    } else {
494        crate::smp::inter_processor_call(&CpuSet::from(cpu_id), || {
495            cpu_local::set_need_preempt();
496        });
497    }
498}
499*/
500
501/// Dequeues the current task from its runqueue.
502///
503/// This should only be called if the current is to exit.
504/*
505#[track_caller]
506pub(super) fn exit_current() -> ! {
507    let mut is_first_try = true;
508
509    reschedule(|local_rq: &mut dyn LocalRunQueue| {
510        let next_task_opt = if is_first_try {
511            is_first_try = false;
512            let should_pick_next = local_rq.update_current(UpdateFlags::Exit);
513            let _current = local_rq.dequeue_current();
514            should_pick_next.then(|| local_rq.pick_next())
515        } else {
516            local_rq.try_pick_next()
517        };
518
519        if let Some(next_task) = next_task_opt {
520            ReschedAction::SwitchTo(next_task.clone())
521        } else {
522            ReschedAction::Retry
523        }
524    });
525
526    unreachable!()
527}
528*/
529
530/// Yields execution.
531/*
532#[track_caller]
533pub(super) fn yield_now() {
534    reschedule(|local_rq| {
535        let should_pick_next = local_rq.update_current(UpdateFlags::Yield);
536        let next_task_opt = should_pick_next.then(|| local_rq.pick_next());
537        if let Some(next_task) = next_task_opt {
538            ReschedAction::SwitchTo(next_task.clone())
539        } else {
540            ReschedAction::DoNothing
541        }
542    })
543}
544*/
545
546/// Do rescheduling by acting on the scheduling decision (`ReschedAction`) made by a
547/// user-given closure.
548///
549/// The closure makes the scheduling decision by taking the local runqueue has its input.
550#[track_caller]
551fn reschedule<F>(mut f: F)
552where
553    F: FnMut(&mut dyn LocalRunQueue) -> ReschedAction,
554{
555    // Even if the decision below is `DoNothing`, we should clear this flag. Meanwhile, to avoid
556    // race conditions, we should do this before making the decision.
557    // cpu_local::clear_need_preempt();
558
559    let next_task = loop {
560        let mut action = ReschedAction::DoNothing;
561        SCHEDULER.get().unwrap().mut_local_rq_with(&mut |rq| {
562            action = f(rq);
563        });
564
565        match action {
566            ReschedAction::DoNothing => {
567                return;
568            }
569            ReschedAction::Retry => {
570                continue;
571            }
572            ReschedAction::SwitchTo(next_task) => {
573                break next_task;
574            }
575        };
576    };
577
578    // `switch_to_task` will spin if it finds that the next task is still running on some CPU core,
579    // which guarantees soundness regardless of the scheduler implementation.
580    //
581    // FIXME: The scheduler decision and context switching are not atomic, which can lead to some
582    // strange behavior even if the scheduler is implemented correctly. See "Problem 2" at
583    // <https://github.com/asterinas/asterinas/issues/1633> for details.
584    // processor::switch_to_task(next_task);
585}
586
587/// Possible actions of a rescheduling.
588enum ReschedAction {
589    /// Keep running current task and do nothing.
590    DoNothing,
591    /// Loop until finding a task to swap out the current.
592    Retry,
593    /// Switch to target task.
594    SwitchTo(Arc<Task>),
595}