1// Licensed under the Apache License, Version 2.0 or the MIT License.
2// SPDX-License-Identifier: Apache-2.0 OR MIT
3// Copyright Tock Contributors 2022.
45//! Multilevel feedback queue scheduler for Tock
6//!
7//! Based on the MLFQ rules described in "Operating Systems: Three Easy Pieces"
8//! by Remzi H. Arpaci-Dusseau and Andrea C. Arpaci-Dusseau.
9//!
10//! This scheduler can be summarized by the following rules:
11//!
12//! - Rule 1: If Priority(A) > Priority(B), and both are ready, A runs (B
13//! doesn't).
14//! - Rule 2: If Priority(A) = Priority(B), A & B run in round-robin fashion
15//! using the time slice (quantum length) of the given queue.
16//! - Rule 3: When a job enters the system, it is placed at the highest priority
17//! (the topmost queue).
18//! - Rule 4: Once a job uses up its time allotment at a given level (regardless
19//! of how many times it has given up the CPU), its priority is reduced (i.e.,
20//! it moves down one queue).
21//! - Rule 5: After some time period S, move all the jobs in the system to the
22//! topmost queue.
2324use core::cell::Cell;
25use core::num::NonZeroU32;
2627use crate::collections::list::{List, ListLink, ListNode};
28use crate::hil::time::{self, ConvertTicks, Ticks};
29use crate::platform::chip::Chip;
30use crate::process::Process;
31use crate::process::StoppedExecutingReason;
32use crate::scheduler::{Scheduler, SchedulingDecision};
3334#[derive(Default)]
35struct MfProcState {
36/// Total CPU time used by this process while in current queue
37us_used_this_queue: Cell<u32>,
38}
3940/// Nodes store per-process state
41pub struct MLFQProcessNode<'a> {
42 proc: &'static Option<&'static dyn Process>,
43 state: MfProcState,
44 next: ListLink<'a, MLFQProcessNode<'a>>,
45}
4647impl<'a> MLFQProcessNode<'a> {
48pub fn new(proc: &'static Option<&'static dyn Process>) -> MLFQProcessNode<'a> {
49 MLFQProcessNode {
50 proc,
51 state: MfProcState::default(),
52 next: ListLink::empty(),
53 }
54 }
55}
5657impl<'a> ListNode<'a, MLFQProcessNode<'a>> for MLFQProcessNode<'a> {
58fn next(&'a self) -> &'a ListLink<'a, MLFQProcessNode<'a>> {
59&self.next
60 }
61}
6263pub struct MLFQSched<'a, A: 'static + time::Alarm<'static>> {
64 alarm: &'static A,
65pub processes: [List<'a, MLFQProcessNode<'a>>; 3], // Using Self::NUM_QUEUES causes rustc to crash..
66next_reset: Cell<A::Ticks>,
67 last_reset_check: Cell<A::Ticks>,
68 last_timeslice: Cell<u32>,
69 last_queue_idx: Cell<usize>,
70}
7172impl<'a, A: 'static + time::Alarm<'static>> MLFQSched<'a, A> {
73/// How often to restore all processes to max priority
74pub const PRIORITY_REFRESH_PERIOD_MS: u32 = 5000;
75pub const NUM_QUEUES: usize = 3;
7677pub fn new(alarm: &'static A) -> Self {
78Self {
79 alarm,
80 processes: [List::new(), List::new(), List::new()],
81 next_reset: Cell::new(A::Ticks::from(0)),
82 last_reset_check: Cell::new(A::Ticks::from(0)),
83 last_timeslice: Cell::new(0),
84 last_queue_idx: Cell::new(0),
85 }
86 }
8788fn get_timeslice_us(&self, queue_idx: usize) -> u32 {
89match queue_idx {
900 => 10000,
911 => 20000,
922 => 50000,
93_ => panic!("invalid queue idx"),
94 }
95 }
9697fn redeem_all_procs(&self) {
98for queue in self.processes.iter().skip(1) {
99if let Some(proc) = queue.pop_head() {
100self.processes[0].push_tail(proc)
101 }
102 }
103 }
104105/// Returns the process at the head of the highest priority queue containing a process
106 /// that is ready to execute (as determined by `has_tasks()`)
107 /// This method moves that node to the head of its queue.
108fn get_next_ready_process_node(&self) -> (Option<&MLFQProcessNode<'a>>, usize) {
109for (idx, queue) in self.processes.iter().enumerate() {
110let next = queue
111 .iter()
112 .find(|node_ref| node_ref.proc.is_some_and(|proc| proc.ready()));
113if next.is_some() {
114// pop procs to back until we get to match
115loop {
116let cur = queue.pop_head();
117if let Some(node) = cur {
118if core::ptr::eq(node, next.unwrap()) {
119 queue.push_head(node);
120// match! Put back on front
121return (next, idx);
122 } else {
123 queue.push_tail(node);
124 }
125 }
126 }
127 }
128 }
129 (None, 0)
130 }
131}
132133impl<A: 'static + time::Alarm<'static>, C: Chip> Scheduler<C> for MLFQSched<'_, A> {
134fn next(&self) -> SchedulingDecision {
135let now = self.alarm.now();
136let next_reset = self.next_reset.get();
137let last_reset_check = self.last_reset_check.get();
138139// storing last reset check is necessary to avoid missing a reset when the underlying
140 // alarm wraps around
141if !now.within_range(last_reset_check, next_reset) {
142// Promote all processes to highest priority queue
143self.next_reset
144 .set(now.wrapping_add(self.alarm.ticks_from_ms(Self::PRIORITY_REFRESH_PERIOD_MS)));
145self.redeem_all_procs();
146 }
147self.last_reset_check.set(now);
148let (node_ref_opt, queue_idx) = self.get_next_ready_process_node();
149if node_ref_opt.is_none() {
150return SchedulingDecision::TrySleep;
151 }
152let node_ref = node_ref_opt.unwrap();
153let timeslice = self.get_timeslice_us(queue_idx) - node_ref.state.us_used_this_queue.get();
154let next = node_ref.proc.unwrap().processid();
155self.last_queue_idx.set(queue_idx);
156self.last_timeslice.set(timeslice);
157158 SchedulingDecision::RunProcess((next, NonZeroU32::new(timeslice)))
159 }
160161fn result(&self, result: StoppedExecutingReason, execution_time_us: Option<u32>) {
162let execution_time_us = execution_time_us.unwrap(); // should never fail as we never run cooperatively
163let queue_idx = self.last_queue_idx.get();
164// Last executed node will always be at head of its queue
165let node_ref = self.processes[queue_idx].head().unwrap();
166 node_ref
167 .state
168 .us_used_this_queue
169 .set(self.last_timeslice.get() - execution_time_us);
170171let punish = result == StoppedExecutingReason::TimesliceExpired;
172if punish {
173 node_ref.state.us_used_this_queue.set(0);
174let next_queue = if queue_idx == Self::NUM_QUEUES - 1 {
175 queue_idx
176 } else {
177 queue_idx + 1
178};
179self.processes[next_queue].push_tail(self.processes[queue_idx].pop_head().unwrap());
180 } else {
181self.processes[queue_idx].push_tail(self.processes[queue_idx].pop_head().unwrap());
182 }
183 }
184}