From c8d25d9aed146dca28dc8987afd229b52c20361a Mon Sep 17 00:00:00 2001 From: Yuekai Jia Date: Thu, 11 Jul 2024 04:00:04 +0800 Subject: [PATCH] Initial commit --- .github/workflows/ci.yml | 55 ++++++++++++ .gitignore | 4 + Cargo.toml | 14 +++ src/cfs.rs | 190 +++++++++++++++++++++++++++++++++++++++ src/fifo.rs | 102 +++++++++++++++++++++ src/lib.rs | 68 ++++++++++++++ src/round_robin.rs | 113 +++++++++++++++++++++++ src/tests.rs | 84 +++++++++++++++++ 8 files changed, 630 insertions(+) create mode 100644 .github/workflows/ci.yml create mode 100644 .gitignore create mode 100644 Cargo.toml create mode 100644 src/cfs.rs create mode 100644 src/fifo.rs create mode 100644 src/lib.rs create mode 100644 src/round_robin.rs create mode 100644 src/tests.rs 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::);