diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml
new file mode 100644
index 0000000..531ddd1
--- /dev/null
+++ b/.github/workflows/ci.yml
@@ -0,0 +1,55 @@
+name: CI
+
+on: [push, pull_request]
+
+jobs:
+ ci:
+ runs-on: ubuntu-latest
+ strategy:
+ fail-fast: false
+ matrix:
+ rust-toolchain: [nightly]
+ targets: [x86_64-unknown-linux-gnu, x86_64-unknown-none, riscv64gc-unknown-none-elf, aarch64-unknown-none-softfloat]
+ steps:
+ - uses: actions/checkout@v4
+ - uses: dtolnay/rust-toolchain@nightly
+ with:
+ toolchain: ${{ matrix.rust-toolchain }}
+ components: rust-src, clippy, rustfmt
+ targets: ${{ matrix.targets }}
+ - name: Check rust version
+ run: rustc --version --verbose
+ - name: Check code format
+ run: cargo fmt --all -- --check
+ - name: Clippy
+ run: cargo clippy --target ${{ matrix.targets }} --all-features -- -A clippy::new_without_default
+ - name: Build
+ run: cargo build --target ${{ matrix.targets }} --all-features
+ - name: Unit test
+ if: ${{ matrix.targets == 'x86_64-unknown-linux-gnu' }}
+ run: cargo test --target ${{ matrix.targets }} -- --nocapture
+
+ doc:
+ runs-on: ubuntu-latest
+ strategy:
+ fail-fast: false
+ permissions:
+ contents: write
+ env:
+ default-branch: ${{ format('refs/heads/{0}', github.event.repository.default_branch) }}
+ RUSTDOCFLAGS: -D rustdoc::broken_intra_doc_links -D missing-docs
+ steps:
+ - uses: actions/checkout@v4
+ - uses: dtolnay/rust-toolchain@nightly
+ - name: Build docs
+ continue-on-error: ${{ github.ref != env.default-branch && github.event_name != 'pull_request' }}
+ run: |
+ cargo doc --no-deps --all-features
+ printf '' $(cargo tree | head -1 | cut -d' ' -f1) > target/doc/index.html
+ - name: Deploy to Github Pages
+ if: ${{ github.ref == env.default-branch }}
+ uses: JamesIves/github-pages-deploy-action@v4
+ with:
+ single-commit: true
+ branch: gh-pages
+ folder: target/doc
diff --git a/.gitignore b/.gitignore
new file mode 100644
index 0000000..ff78c42
--- /dev/null
+++ b/.gitignore
@@ -0,0 +1,4 @@
+/target
+/.vscode
+.DS_Store
+Cargo.lock
diff --git a/Cargo.toml b/Cargo.toml
new file mode 100644
index 0000000..9133e80
--- /dev/null
+++ b/Cargo.toml
@@ -0,0 +1,14 @@
+[package]
+name = "scheduler"
+version = "0.1.0"
+edition = "2021"
+authors = ["Yuekai Jia "]
+description = "Various scheduler algorithms in a unified interface"
+license = "GPL-3.0-or-later OR Apache-2.0 OR MulanPSL-2.0"
+homepage = "https://github.com/arceos-org/arceos"
+repository = "https://github.com/arceos-org/scheduler"
+documentation = "https://arceos-org.github.io/scheduler"
+
+[dependencies]
+linked_list = { git = "https://github.com/arceos-org/linked_list.git", tag = "v0.1.0" }
+
diff --git a/src/cfs.rs b/src/cfs.rs
new file mode 100644
index 0000000..30043aa
--- /dev/null
+++ b/src/cfs.rs
@@ -0,0 +1,190 @@
+use alloc::{collections::BTreeMap, sync::Arc};
+use core::ops::Deref;
+use core::sync::atomic::{AtomicIsize, Ordering};
+
+use crate::BaseScheduler;
+
+/// task for CFS
+pub struct CFSTask {
+ inner: T,
+ init_vruntime: AtomicIsize,
+ delta: AtomicIsize,
+ nice: AtomicIsize,
+ id: AtomicIsize,
+}
+
+// https://elixir.bootlin.com/linux/latest/source/include/linux/sched/prio.h
+
+const NICE_RANGE_POS: usize = 19; // MAX_NICE in Linux
+const NICE_RANGE_NEG: usize = 20; // -MIN_NICE in Linux, the range of nice is [MIN_NICE, MAX_NICE]
+
+// https://elixir.bootlin.com/linux/latest/source/kernel/sched/core.c
+
+const NICE2WEIGHT_POS: [isize; NICE_RANGE_POS + 1] = [
+ 1024, 820, 655, 526, 423, 335, 272, 215, 172, 137, 110, 87, 70, 56, 45, 36, 29, 23, 18, 15,
+];
+const NICE2WEIGHT_NEG: [isize; NICE_RANGE_NEG + 1] = [
+ 1024, 1277, 1586, 1991, 2501, 3121, 3906, 4904, 6100, 7620, 9548, 11916, 14949, 18705, 23254,
+ 29154, 36291, 46273, 56483, 71755, 88761,
+];
+
+impl CFSTask {
+ /// new with default values
+ pub const fn new(inner: T) -> Self {
+ Self {
+ inner,
+ init_vruntime: AtomicIsize::new(0_isize),
+ delta: AtomicIsize::new(0_isize),
+ nice: AtomicIsize::new(0_isize),
+ id: AtomicIsize::new(0_isize),
+ }
+ }
+
+ fn get_weight(&self) -> isize {
+ let nice = self.nice.load(Ordering::Acquire);
+ if nice >= 0 {
+ NICE2WEIGHT_POS[nice as usize]
+ } else {
+ NICE2WEIGHT_NEG[(-nice) as usize]
+ }
+ }
+
+ fn get_id(&self) -> isize {
+ self.id.load(Ordering::Acquire)
+ }
+
+ fn get_vruntime(&self) -> isize {
+ if self.nice.load(Ordering::Acquire) == 0 {
+ self.init_vruntime.load(Ordering::Acquire) + self.delta.load(Ordering::Acquire)
+ } else {
+ self.init_vruntime.load(Ordering::Acquire)
+ + self.delta.load(Ordering::Acquire) * 1024 / self.get_weight()
+ }
+ }
+
+ fn set_vruntime(&self, v: isize) {
+ self.init_vruntime.store(v, Ordering::Release);
+ }
+
+ // Simple Implementation: no change in vruntime.
+ // Only modifying priority of current process is supported currently.
+ fn set_priority(&self, nice: isize) {
+ let current_init_vruntime = self.get_vruntime();
+ self.init_vruntime
+ .store(current_init_vruntime, Ordering::Release);
+ self.delta.store(0, Ordering::Release);
+ self.nice.store(nice, Ordering::Release);
+ }
+
+ fn set_id(&self, id: isize) {
+ self.id.store(id, Ordering::Release);
+ }
+
+ fn task_tick(&self) {
+ self.delta.fetch_add(1, Ordering::Release);
+ }
+
+ /// Returns a reference to the inner task struct.
+ pub const fn inner(&self) -> &T {
+ &self.inner
+ }
+}
+
+impl Deref for CFSTask {
+ type Target = T;
+ fn deref(&self) -> &Self::Target {
+ &self.inner
+ }
+}
+
+/// A simple [Completely Fair Scheduler][1] (CFS).
+///
+/// [1]: https://en.wikipedia.org/wiki/Completely_Fair_Scheduler
+pub struct CFScheduler {
+ ready_queue: BTreeMap<(isize, isize), Arc>>, // (vruntime, taskid)
+ min_vruntime: Option,
+ id_pool: AtomicIsize,
+}
+
+impl CFScheduler {
+ /// Creates a new empty [`CFScheduler`].
+ pub const fn new() -> Self {
+ Self {
+ ready_queue: BTreeMap::new(),
+ min_vruntime: None,
+ id_pool: AtomicIsize::new(0_isize),
+ }
+ }
+ /// get the name of scheduler
+ pub fn scheduler_name() -> &'static str {
+ "Completely Fair"
+ }
+}
+
+impl BaseScheduler for CFScheduler {
+ type SchedItem = Arc>;
+
+ fn init(&mut self) {}
+
+ fn add_task(&mut self, task: Self::SchedItem) {
+ if self.min_vruntime.is_none() {
+ self.min_vruntime = Some(AtomicIsize::new(0_isize));
+ }
+ let vruntime = self.min_vruntime.as_mut().unwrap().load(Ordering::Acquire);
+ let taskid = self.id_pool.fetch_add(1, Ordering::Release);
+ task.set_vruntime(vruntime);
+ task.set_id(taskid);
+ self.ready_queue.insert((vruntime, taskid), task);
+ if let Some(((min_vruntime, _), _)) = self.ready_queue.first_key_value() {
+ self.min_vruntime = Some(AtomicIsize::new(*min_vruntime));
+ } else {
+ self.min_vruntime = None;
+ }
+ }
+
+ fn remove_task(&mut self, task: &Self::SchedItem) -> Option {
+ if let Some((_, tmp)) = self
+ .ready_queue
+ .remove_entry(&(task.clone().get_vruntime(), task.clone().get_id()))
+ {
+ if let Some(((min_vruntime, _), _)) = self.ready_queue.first_key_value() {
+ self.min_vruntime = Some(AtomicIsize::new(*min_vruntime));
+ } else {
+ self.min_vruntime = None;
+ }
+ Some(tmp)
+ } else {
+ None
+ }
+ }
+
+ fn pick_next_task(&mut self) -> Option {
+ if let Some((_, v)) = self.ready_queue.pop_first() {
+ Some(v)
+ } else {
+ None
+ }
+ }
+
+ fn put_prev_task(&mut self, prev: Self::SchedItem, _preempt: bool) {
+ let taskid = self.id_pool.fetch_add(1, Ordering::Release);
+ prev.set_id(taskid);
+ self.ready_queue
+ .insert((prev.clone().get_vruntime(), taskid), prev);
+ }
+
+ fn task_tick(&mut self, current: &Self::SchedItem) -> bool {
+ current.task_tick();
+ self.min_vruntime.is_none()
+ || current.get_vruntime() > self.min_vruntime.as_mut().unwrap().load(Ordering::Acquire)
+ }
+
+ fn set_priority(&mut self, task: &Self::SchedItem, prio: isize) -> bool {
+ if (-20..=19).contains(&prio) {
+ task.set_priority(prio);
+ true
+ } else {
+ false
+ }
+ }
+}
diff --git a/src/fifo.rs b/src/fifo.rs
new file mode 100644
index 0000000..f2469e5
--- /dev/null
+++ b/src/fifo.rs
@@ -0,0 +1,102 @@
+use alloc::sync::Arc;
+use core::ops::Deref;
+
+use linked_list::{Adapter, Links, List};
+
+use crate::BaseScheduler;
+
+/// A task wrapper for the [`FifoScheduler`].
+///
+/// It add extra states to use in [`linked_list::List`].
+pub struct FifoTask {
+ inner: T,
+ links: Links,
+}
+
+unsafe impl Adapter for FifoTask {
+ type EntryType = Self;
+
+ #[inline]
+ fn to_links(t: &Self) -> &Links {
+ &t.links
+ }
+}
+
+impl FifoTask {
+ /// Creates a new [`FifoTask`] from the inner task struct.
+ pub const fn new(inner: T) -> Self {
+ Self {
+ inner,
+ links: Links::new(),
+ }
+ }
+
+ /// Returns a reference to the inner task struct.
+ pub const fn inner(&self) -> &T {
+ &self.inner
+ }
+}
+
+impl Deref for FifoTask {
+ type Target = T;
+ #[inline]
+ fn deref(&self) -> &Self::Target {
+ &self.inner
+ }
+}
+
+/// A simple FIFO (First-In-First-Out) cooperative scheduler.
+///
+/// When a task is added to the scheduler, it's placed at the end of the ready
+/// queue. When picking the next task to run, the head of the ready queue is
+/// taken.
+///
+/// As it's a cooperative scheduler, it does nothing when the timer tick occurs.
+///
+/// It internally uses a linked list as the ready queue.
+pub struct FifoScheduler {
+ ready_queue: List>>,
+}
+
+impl FifoScheduler {
+ /// Creates a new empty [`FifoScheduler`].
+ pub const fn new() -> Self {
+ Self {
+ ready_queue: List::new(),
+ }
+ }
+ /// get the name of scheduler
+ pub fn scheduler_name() -> &'static str {
+ "FIFO"
+ }
+}
+
+impl BaseScheduler for FifoScheduler {
+ type SchedItem = Arc>;
+
+ fn init(&mut self) {}
+
+ fn add_task(&mut self, task: Self::SchedItem) {
+ self.ready_queue.push_back(task);
+ }
+
+ fn remove_task(&mut self, task: &Self::SchedItem) -> Option {
+ unsafe { self.ready_queue.remove(task) }
+ }
+
+ fn pick_next_task(&mut self) -> Option {
+ self.ready_queue.pop_front()
+ }
+
+ fn put_prev_task(&mut self, prev: Self::SchedItem, _preempt: bool) {
+ self.ready_queue.push_back(prev);
+ }
+
+ fn task_tick(&mut self, _current: &Self::SchedItem) -> bool {
+ false // no reschedule
+ }
+
+ fn set_priority(&mut self, _task: &Self::SchedItem, _prio: isize) -> bool {
+ false
+ }
+}
diff --git a/src/lib.rs b/src/lib.rs
new file mode 100644
index 0000000..de15661
--- /dev/null
+++ b/src/lib.rs
@@ -0,0 +1,68 @@
+//! Various scheduler algorithms in a unified interface.
+//!
+//! Currently supported algorithms:
+//!
+//! - [`FifoScheduler`]: FIFO (First-In-First-Out) scheduler (cooperative).
+//! - [`RRScheduler`]: Round-robin scheduler (preemptive).
+//! - [`CFScheduler`]: Completely Fair Scheduler (preemptive).
+
+#![cfg_attr(not(test), no_std)]
+
+mod cfs;
+mod fifo;
+mod round_robin;
+
+#[cfg(test)]
+mod tests;
+
+extern crate alloc;
+
+pub use cfs::{CFSTask, CFScheduler};
+pub use fifo::{FifoScheduler, FifoTask};
+pub use round_robin::{RRScheduler, RRTask};
+
+/// The base scheduler trait that all schedulers should implement.
+///
+/// All tasks in the scheduler are considered runnable. If a task is go to
+/// sleep, it should be removed from the scheduler.
+pub trait BaseScheduler {
+ /// Type of scheduled entities. Often a task struct.
+ type SchedItem;
+
+ /// Initializes the scheduler.
+ fn init(&mut self);
+
+ /// Adds a task to the scheduler.
+ fn add_task(&mut self, task: Self::SchedItem);
+
+ /// Removes a task by its reference from the scheduler. Returns the owned
+ /// removed task with ownership if it exists.
+ ///
+ /// # Safety
+ ///
+ /// The caller should ensure that the task is in the scheduler, otherwise
+ /// the behavior is undefined.
+ fn remove_task(&mut self, task: &Self::SchedItem) -> Option;
+
+ /// Picks the next task to run, it will be removed from the scheduler.
+ /// Returns [`None`] if there is not runnable task.
+ fn pick_next_task(&mut self) -> Option;
+
+ /// Puts the previous task back to the scheduler. The previous task is
+ /// usually placed at the end of the ready queue, making it less likely
+ /// to be re-scheduled.
+ ///
+ /// `preempt` indicates whether the previous task is preempted by the next
+ /// task. In this case, the previous task may be placed at the front of the
+ /// ready queue.
+ fn put_prev_task(&mut self, prev: Self::SchedItem, preempt: bool);
+
+ /// Advances the scheduler state at each timer tick. Returns `true` if
+ /// re-scheduling is required.
+ ///
+ /// `current` is the current running task.
+ fn task_tick(&mut self, current: &Self::SchedItem) -> bool;
+
+ /// set priority for a task
+ fn set_priority(&mut self, task: &Self::SchedItem, prio: isize) -> bool;
+}
diff --git a/src/round_robin.rs b/src/round_robin.rs
new file mode 100644
index 0000000..bf7e2c4
--- /dev/null
+++ b/src/round_robin.rs
@@ -0,0 +1,113 @@
+use alloc::{collections::VecDeque, sync::Arc};
+use core::ops::Deref;
+use core::sync::atomic::{AtomicIsize, Ordering};
+
+use crate::BaseScheduler;
+
+/// A task wrapper for the [`RRScheduler`].
+///
+/// It add a time slice counter to use in round-robin scheduling.
+pub struct RRTask {
+ inner: T,
+ time_slice: AtomicIsize,
+}
+
+impl RRTask {
+ /// Creates a new [`RRTask`] from the inner task struct.
+ pub const fn new(inner: T) -> Self {
+ Self {
+ inner,
+ time_slice: AtomicIsize::new(S as isize),
+ }
+ }
+
+ fn time_slice(&self) -> isize {
+ self.time_slice.load(Ordering::Acquire)
+ }
+
+ fn reset_time_slice(&self) {
+ self.time_slice.store(S as isize, Ordering::Release);
+ }
+
+ /// Returns a reference to the inner task struct.
+ pub const fn inner(&self) -> &T {
+ &self.inner
+ }
+}
+
+impl Deref for RRTask {
+ type Target = T;
+ #[inline]
+ fn deref(&self) -> &Self::Target {
+ &self.inner
+ }
+}
+
+/// A simple [Round-Robin] (RR) preemptive scheduler.
+///
+/// It's very similar to the [`FifoScheduler`], but every task has a time slice
+/// counter that is decremented each time a timer tick occurs. When the current
+/// task's time slice counter reaches zero, the task is preempted and needs to
+/// be rescheduled.
+///
+/// Unlike [`FifoScheduler`], it uses [`VecDeque`] as the ready queue. So it may
+/// take O(n) time to remove a task from the ready queue.
+///
+/// [Round-Robin]: https://en.wikipedia.org/wiki/Round-robin_scheduling
+/// [`FifoScheduler`]: crate::FifoScheduler
+pub struct RRScheduler {
+ ready_queue: VecDeque>>,
+}
+
+impl RRScheduler {
+ /// Creates a new empty [`RRScheduler`].
+ pub const fn new() -> Self {
+ Self {
+ ready_queue: VecDeque::new(),
+ }
+ }
+ /// get the name of scheduler
+ pub fn scheduler_name() -> &'static str {
+ "Round-robin"
+ }
+}
+
+impl BaseScheduler for RRScheduler {
+ type SchedItem = Arc>;
+
+ fn init(&mut self) {}
+
+ fn add_task(&mut self, task: Self::SchedItem) {
+ self.ready_queue.push_back(task);
+ }
+
+ fn remove_task(&mut self, task: &Self::SchedItem) -> Option {
+ // TODO: more efficient
+ self.ready_queue
+ .iter()
+ .position(|t| Arc::ptr_eq(t, task))
+ .and_then(|idx| self.ready_queue.remove(idx))
+ }
+
+ fn pick_next_task(&mut self) -> Option {
+ self.ready_queue.pop_front()
+ }
+
+ fn put_prev_task(&mut self, prev: Self::SchedItem, preempt: bool) {
+ if prev.time_slice() > 0 && preempt {
+ self.ready_queue.push_front(prev)
+ } else {
+ prev.reset_time_slice();
+ self.ready_queue.push_back(prev)
+ }
+ }
+
+ fn task_tick(&mut self, current: &Self::SchedItem) -> bool {
+ let old_slice = current.time_slice.fetch_sub(1, Ordering::Release);
+ old_slice <= 1
+ }
+
+ fn set_priority(&mut self, _task: &Self::SchedItem, _prio: isize) -> bool {
+ false
+ }
+}
diff --git a/src/tests.rs b/src/tests.rs
new file mode 100644
index 0000000..472820c
--- /dev/null
+++ b/src/tests.rs
@@ -0,0 +1,84 @@
+macro_rules! def_test_sched {
+ ($name: ident, $scheduler: ty, $task: ty) => {
+ mod $name {
+ use crate::*;
+ use alloc::sync::Arc;
+
+ #[test]
+ fn test_sched() {
+ const NUM_TASKS: usize = 11;
+
+ let mut scheduler = <$scheduler>::new();
+ for i in 0..NUM_TASKS {
+ scheduler.add_task(Arc::new(<$task>::new(i)));
+ }
+
+ for i in 0..NUM_TASKS * 10 - 1 {
+ let next = scheduler.pick_next_task().unwrap();
+ assert_eq!(*next.inner(), i % NUM_TASKS);
+ // pass a tick to ensure the order of tasks
+ scheduler.task_tick(&next);
+ scheduler.put_prev_task(next, false);
+ }
+
+ let mut n = 0;
+ while scheduler.pick_next_task().is_some() {
+ n += 1;
+ }
+ assert_eq!(n, NUM_TASKS);
+ }
+
+ #[test]
+ fn bench_yield() {
+ const NUM_TASKS: usize = 1_000_000;
+ const COUNT: usize = NUM_TASKS * 3;
+
+ let mut scheduler = <$scheduler>::new();
+ for i in 0..NUM_TASKS {
+ scheduler.add_task(Arc::new(<$task>::new(i)));
+ }
+
+ let t0 = std::time::Instant::now();
+ for _ in 0..COUNT {
+ let next = scheduler.pick_next_task().unwrap();
+ scheduler.put_prev_task(next, false);
+ }
+ let t1 = std::time::Instant::now();
+ println!(
+ " {}: task yield speed: {:?}/task",
+ stringify!($scheduler),
+ (t1 - t0) / (COUNT as u32)
+ );
+ }
+
+ #[test]
+ fn bench_remove() {
+ const NUM_TASKS: usize = 10_000;
+
+ let mut scheduler = <$scheduler>::new();
+ let mut tasks = Vec::new();
+ for i in 0..NUM_TASKS {
+ let t = Arc::new(<$task>::new(i));
+ tasks.push(t.clone());
+ scheduler.add_task(t);
+ }
+
+ let t0 = std::time::Instant::now();
+ for i in (0..NUM_TASKS).rev() {
+ let t = scheduler.remove_task(&tasks[i]).unwrap();
+ assert_eq!(*t.inner(), i);
+ }
+ let t1 = std::time::Instant::now();
+ println!(
+ " {}: task remove speed: {:?}/task",
+ stringify!($scheduler),
+ (t1 - t0) / (NUM_TASKS as u32)
+ );
+ }
+ }
+ };
+}
+
+def_test_sched!(fifo, FifoScheduler::, FifoTask::);
+def_test_sched!(rr, RRScheduler::, RRTask::);
+def_test_sched!(cfs, CFScheduler::, CFSTask::);