blob: 31ca4096c11820cb075390ef3b233f173d039151 [file] [log] [blame]
// Copyright 2025 The Pigweed Authors
//
// Licensed under the Apache License, Version 2.0 (the "License"); you may not
// use this file except in compliance with the License. You may obtain a copy of
// the License at
//
// https://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
// WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
// License for the specific language governing permissions and limitations under
// the License.
use core::cell::UnsafeCell;
use core::mem::offset_of;
use foreign_box::ForeignBox;
use list::*;
use pw_log::info;
use crate::arch::{Arch, ArchInterface, ArchThreadState, ThreadState};
use crate::sync::spinlock::{SpinLock, SpinLockGuard};
mod locks;
pub use locks::{SchedLock, SchedLockGuard, WaitQueueLock};
#[derive(Clone, Copy)]
pub struct Stack {
start: *const u8,
end: *const u8,
}
#[allow(dead_code)]
impl Stack {
pub const fn from_slice(slice: &[u8]) -> Self {
let start: *const u8 = slice.as_ptr();
// Safety: offset based on known size of slice.
let end = unsafe { start.add(slice.len() - 1) };
Self { start, end }
}
const fn new() -> Self {
Self {
start: core::ptr::null(),
end: core::ptr::null(),
}
}
pub fn start(self) -> *const u8 {
self.start
}
pub fn end(self) -> *const u8 {
self.end
}
}
// TODO: want to name this ThreadState, but collides with ArchThreadstate
#[derive(Copy, Clone, PartialEq)]
enum State {
New,
Initial,
Ready,
Running,
Stopped,
Waiting,
}
// TODO: use From or Into trait (unclear how to do it with 'static str)
fn to_string(s: State) -> &'static str {
match s {
State::New => "New",
State::Initial => "Initial",
State::Ready => "Ready",
State::Running => "Running",
State::Stopped => "Stopped",
State::Waiting => "Waiting",
}
}
pub struct Thread {
// List of the threads in the system
pub global_link: Link,
// Active state link (run queue, wait queue, etc)
pub active_link: Link,
state: State,
stack: Stack,
// Architecturally specific thread state, saved on context switch
pub arch_thread_state: UnsafeCell<ArchThreadState>,
}
pub struct ThreadListAdapter {}
impl list::Adapter for ThreadListAdapter {
const LINK_OFFSET: usize = offset_of!(Thread, active_link);
}
pub struct GlobalThreadListAdapter {}
impl list::Adapter for GlobalThreadListAdapter {
const LINK_OFFSET: usize = offset_of!(Thread, global_link);
}
impl Thread {
// Create an empty, uninitialzed thread
pub fn new() -> Self {
Thread {
global_link: Link::new(),
active_link: Link::new(),
state: State::New,
arch_thread_state: UnsafeCell::new(ThreadState::new()),
stack: Stack::new(),
}
}
// Initialize the mutable parts of the thread, must be called once per
// thread prior to starting it
#[allow(dead_code)]
pub fn initialize(&mut self, stack: Stack, entry_point: fn(usize), arg: usize) -> &mut Thread {
assert!(self.state == State::New);
self.stack = stack;
// Call the arch to arrange for the thread to start directly
unsafe {
(*self.arch_thread_state.get()).initialize_frame(stack, entry_point, arg);
}
self.state = State::Initial;
// Add our list to the global thread list
SCHEDULER_STATE.lock().add_thread_to_list(self);
self
}
#[allow(dead_code)]
pub fn start(mut thread: ForeignBox<Self>) {
info!("starting thread {:#x}", thread.id() as usize);
assert!(thread.state == State::Initial);
thread.state = State::Ready;
let mut sched_state = SCHEDULER_STATE.lock();
// If there is a current thread, put it back on the top of the run queue.
let id = if let Some(mut current_thread) = sched_state.current_thread.take() {
let id = current_thread.id();
current_thread.state = State::Ready;
sched_state.insert_in_run_queue_head(current_thread);
id
} else {
Self::null_id()
};
sched_state.insert_in_run_queue_tail(thread);
// Add this thread to the scheduler and trigger a reschedule event
reschedule(sched_state, id);
}
// Dump to the console useful information about this thread
#[allow(dead_code)]
pub fn dump(&self) {
info!(
"thread {:#x} state {}",
self.id() as usize,
to_string(self.state) as &str
);
}
// A simple id for debugging purposes, currently the pointer to the thread structure itself
pub fn id(&self) -> usize {
core::ptr::from_ref(self) as usize
}
// An id that can not be assigned to any thread in the system.
pub const fn null_id() -> usize {
// `core::ptr::null::<Self>() as usize` can not be evaluated at const time
// and a null pointer is defined to be at address 0 (see
// https://doc.rust-lang.org/beta/core/ptr/fn.null.html).
0usize
}
}
pub fn bootstrap_scheduler(mut thread: ForeignBox<Thread>) -> ! {
let mut sched_state = SCHEDULER_STATE.lock();
// TODO: assert that this is called exactly once at bootup to switch
// to this particular thread.
assert!(thread.state == State::Initial);
thread.state = State::Ready;
sched_state.run_queue.push_back(thread);
info!("context switching to first thread");
// Special case where we're switching from a non-thread to something real
let mut temp_arch_thread_state = ArchThreadState::new();
sched_state.current_arch_thread_state = &raw mut temp_arch_thread_state;
reschedule(sched_state, Thread::null_id());
panic!("should not reach here");
}
// Global scheduler state (single processor for now)
#[allow(dead_code)]
pub struct SchedulerState {
current_thread: Option<ForeignBox<Thread>>,
current_arch_thread_state: *mut ArchThreadState,
thread_list: UnsafeList<Thread, GlobalThreadListAdapter>,
// For now just have a single round robin list, expand to multiple queues.
run_queue: ForeignList<Thread, ThreadListAdapter>,
}
pub static SCHEDULER_STATE: SpinLock<SchedulerState> = SpinLock::new(SchedulerState::new());
unsafe impl Sync for SchedulerState {}
unsafe impl Send for SchedulerState {}
impl SchedulerState {
#[allow(dead_code)]
const fn new() -> Self {
Self {
current_thread: None,
current_arch_thread_state: core::ptr::null_mut(),
thread_list: UnsafeList::new(),
run_queue: ForeignList::new(),
}
}
#[allow(dead_code)]
pub(super) unsafe fn get_current_arch_thread_state(&mut self) -> *mut ArchThreadState {
self.current_arch_thread_state
}
fn move_current_thread_to_back(&mut self) -> usize {
let Some(mut current_thread) = self.current_thread.take() else {
panic!("no current thread");
};
let current_thread_id = current_thread.id();
current_thread.state = State::Ready;
self.insert_in_run_queue_tail(current_thread);
current_thread_id
}
fn move_current_thread_to_front(&mut self) -> usize {
let Some(mut current_thread) = self.current_thread.take() else {
panic!("no current thread");
};
let current_thread_id = current_thread.id();
current_thread.state = State::Ready;
self.insert_in_run_queue_head(current_thread);
current_thread_id
}
fn set_current_thread(&mut self, thread: ForeignBox<Thread>) {
self.current_arch_thread_state = thread.arch_thread_state.get();
self.current_thread = Some(thread);
}
pub fn current_thread_id(&self) -> usize {
match &self.current_thread {
Some(thread) => thread.id(),
None => Thread::null_id(),
}
}
#[allow(dead_code)]
#[inline(never)]
pub fn add_thread_to_list(&mut self, thread: &mut Thread) {
unsafe {
self.thread_list.push_front_unchecked(thread);
}
}
#[allow(dead_code)]
pub fn dump_all_threads(&self) {
info!("list of all threads:");
unsafe {
let _ = self.thread_list.for_each(|thread| -> Result<(), ()> {
// info!("ptr {:#x}", thread.id());
thread.dump();
Ok(())
});
}
}
#[allow(dead_code)]
fn insert_in_run_queue_head(&mut self, thread: ForeignBox<Thread>) {
assert!(thread.state == State::Ready);
// info!("pushing thread {:#x} on run queue head", thread.id());
self.run_queue.push_front(thread);
}
#[allow(dead_code)]
fn insert_in_run_queue_tail(&mut self, thread: ForeignBox<Thread>) {
assert!(thread.state == State::Ready);
// info!("pushing thread {:#x} on run queue tail", thread.id());
self.run_queue.push_back(thread);
}
}
#[allow(dead_code)]
fn reschedule(
mut sched_state: SpinLockGuard<SchedulerState>,
current_thread_id: usize,
) -> SpinLockGuard<SchedulerState> {
// Caller to reschedule is responsible for removing current thread and
// put it in the correct run/wait queue.
assert!(sched_state.current_thread.is_none());
// info!("reschedule");
// Pop a new thread off the head of the run queue.
// At the moment cannot handle an empty queue, so will panic in that case.
// TODO: Implement either an idle thread or a special idle routine for that case.
let Some(mut new_thread) = sched_state.run_queue.pop_head() else {
panic!("run_queue empty");
};
assert!(new_thread.state == State::Ready);
new_thread.state = State::Running;
if current_thread_id == new_thread.id() {
sched_state.current_thread = Some(new_thread);
// info!("decided to continue running thread {:#x}", new_thread.id());
return sched_state;
}
// info!("switching to thread {:#x}", new_thread.id());
unsafe {
let old_thread_state = sched_state.current_arch_thread_state;
let new_thread_state = new_thread.arch_thread_state.get();
sched_state.set_current_thread(new_thread);
<Arch as ArchInterface>::ThreadState::context_switch(
sched_state,
old_thread_state,
new_thread_state,
)
}
}
#[allow(dead_code)]
pub fn yield_timeslice() {
// info!("yielding thread {:#x}", current_thread.id());
let mut sched_state = SCHEDULER_STATE.lock();
// Yielding always moves the current task to the back of the run queue
let current_thread_id = sched_state.move_current_thread_to_back();
reschedule(sched_state, current_thread_id);
}
#[allow(dead_code)]
pub fn preempt() {
// info!("preempt thread {:#x}", current_thread.id());
let mut sched_state = SCHEDULER_STATE.lock();
// For now, always move the current thread to the back of the run queue.
// When the scheduler gets more complex, it should evaluate if it has used
// up it's time allocation.
let current_thread_id = sched_state.move_current_thread_to_back();
reschedule(sched_state, current_thread_id);
}
// Tick that is called from a timer handler. The scheduler will evaluate if the current thread
// should be preempted or not
#[allow(dead_code)]
pub fn tick(_time_ms: u32) {
// info!("tick {} ms", _time_ms);
// TODO: dynamically deal with time slice for this thread and put it
// at the head or tail depending.
TICK_WAIT_QUEUE.lock().wake_one();
preempt();
}
// Exit the current thread.
// For now, simply remove ourselves from the run queue. No cleanup of thread resources
// is performed.
#[allow(dead_code)]
pub fn exit_thread() -> ! {
let mut sched_state = SCHEDULER_STATE.lock();
let Some(mut current_thread) = sched_state.current_thread.take() else {
panic!("no current thread");
};
let current_thread_id = current_thread.id();
info!("thread {:#x} exiting", current_thread.id() as usize);
current_thread.state = State::Stopped;
reschedule(sched_state, current_thread_id);
// Should not get here
#[allow(clippy::empty_loop)]
loop {}
}
pub struct WaitQueue {
queue: ForeignList<Thread, ThreadListAdapter>,
}
unsafe impl Sync for WaitQueue {}
unsafe impl Send for WaitQueue {}
impl WaitQueue {
#[allow(dead_code)]
pub const fn new() -> Self {
Self {
queue: ForeignList::new(),
}
}
}
impl SchedLockGuard<'_, WaitQueue> {
pub fn wake_one(mut self) {
if let Some(mut thread) = self.queue.pop_head() {
// Move the current thread to the head of its work queue as to not
// steal it's time allocation.
let current_thread_id = self.sched_mut().move_current_thread_to_front();
thread.state = State::Ready;
self.sched_mut().run_queue.push_back(thread);
reschedule(self.into_sched(), current_thread_id);
}
}
pub fn wait(mut self) {
let Some(mut thread) = self.sched_mut().current_thread.take() else {
panic!("no active thread");
};
let current_thread_id = thread.id();
thread.state = State::Waiting;
self.queue.push_back(thread);
reschedule(self.into_sched(), current_thread_id);
}
}
pub static TICK_WAIT_QUEUE: SchedLock<WaitQueue> = SchedLock::new(WaitQueue::new());