Handout
This commit is contained in:
26
kernel/thread/dispatcher.cc
Normal file
26
kernel/thread/dispatcher.cc
Normal file
@@ -0,0 +1,26 @@
|
||||
// vim: set noet ts=4 sw=4:
|
||||
|
||||
#include "dispatcher.h"
|
||||
|
||||
#include "../arch/core.h"
|
||||
#include "../debug/output.h" // IWYU pragma: keep
|
||||
|
||||
Dispatcher::Dispatcher() : life(nullptr) {}
|
||||
|
||||
Thread *Dispatcher::active() const { return life; }
|
||||
|
||||
void Dispatcher::go(Thread *first) {
|
||||
assert(active() == nullptr);
|
||||
setActive(first);
|
||||
first->go();
|
||||
}
|
||||
|
||||
void Dispatcher::dispatch(Thread *next) {
|
||||
Thread *current = active();
|
||||
assert(current != nullptr);
|
||||
if (current != next) {
|
||||
setActive(next);
|
||||
|
||||
current->resume(next);
|
||||
}
|
||||
}
|
||||
53
kernel/thread/dispatcher.h
Normal file
53
kernel/thread/dispatcher.h
Normal file
@@ -0,0 +1,53 @@
|
||||
// vim: set noet ts=4 sw=4:
|
||||
|
||||
/*! \file
|
||||
* \brief \ref Dispatcher for \ref Thread threads
|
||||
*/
|
||||
#pragma once
|
||||
#include "../thread/thread.h"
|
||||
#include "../types.h"
|
||||
|
||||
/*! \brief The dispatcher dispatches threads and puts the scheduler's
|
||||
* decisions into action.
|
||||
* \ingroup thread
|
||||
*
|
||||
* The dispatcher manages the life pointer that refers to the currently
|
||||
* active thread and performs the actual switching of processes.
|
||||
* For single-core systems, a single life pointer is sufficient, as only a
|
||||
* single thread can be active at any one time. On multi-core systems,
|
||||
* every CPU core needs its own life pointer.
|
||||
*/
|
||||
class Dispatcher {
|
||||
Thread* life;
|
||||
|
||||
/*! \brief set the currently active thread
|
||||
* \param thread active Thread
|
||||
*/
|
||||
void setActive(Thread* thread) { life = thread; }
|
||||
|
||||
public:
|
||||
/*! \brief constructor
|
||||
*
|
||||
*/
|
||||
Dispatcher();
|
||||
|
||||
/*! \brief Returns the thread running on the CPU core calling this method
|
||||
*
|
||||
*/
|
||||
Thread* active() const;
|
||||
|
||||
/*! \brief This method stores first as life pointer for this CPU core and
|
||||
* triggers the execution of first. Only to be used for the first thread
|
||||
* running on a CPU.
|
||||
* \param first First thread to be executed on this CPU core.
|
||||
*
|
||||
*/
|
||||
void go(Thread* first);
|
||||
|
||||
/*! \brief Updates the life pointer to next and issues a thread change from
|
||||
* the old to the new life pointer.
|
||||
* \param next Next thread to be executed.
|
||||
*
|
||||
*/
|
||||
void dispatch(Thread* next);
|
||||
};
|
||||
31
kernel/thread/idlethread.cc
Normal file
31
kernel/thread/idlethread.cc
Normal file
@@ -0,0 +1,31 @@
|
||||
#include "idlethread.h"
|
||||
|
||||
#include "../arch/core.h"
|
||||
#include "../arch/lapic.h"
|
||||
#include "../debug/output.h"
|
||||
#include "../interrupt/guard.h"
|
||||
#include "../thread/scheduler.h"
|
||||
|
||||
void IdleThread::action() {
|
||||
while (true) {
|
||||
Core::Interrupt::disable();
|
||||
if (Guard::unsafeConstAccess()
|
||||
.scheduler.isEmpty()) { // XXX: not a fan ...
|
||||
// To save energy we can disable the timer on a core as soon as no
|
||||
// clock needs to be managed. This function is called from the idle
|
||||
// loop.
|
||||
bool can_sleep =
|
||||
!Guard::unsafeConstAccess().bellringer.bellPending();
|
||||
if (can_sleep) {
|
||||
LAPIC::Timer::setMasked(true);
|
||||
}
|
||||
Core::idle();
|
||||
// We woke up. Start ticking again
|
||||
LAPIC::Timer::setMasked(false);
|
||||
} else {
|
||||
Core::Interrupt::enable();
|
||||
Guarded g = Guard::enter();
|
||||
g.vault().scheduler.resume();
|
||||
}
|
||||
}
|
||||
}
|
||||
28
kernel/thread/idlethread.h
Normal file
28
kernel/thread/idlethread.h
Normal file
@@ -0,0 +1,28 @@
|
||||
/*! \file
|
||||
* \brief \ref IdleThread executed by the \ref Scheduler if no other \ref
|
||||
* Thread is ready
|
||||
*/
|
||||
|
||||
#pragma once
|
||||
#include "../types.h"
|
||||
#include "thread.h"
|
||||
|
||||
/*! \brief Thread that is executed when there is nothing to do for this core.
|
||||
* \ingroup thread
|
||||
*
|
||||
* Using the IdleThread simplifies the idea of waiting and is an answer to the
|
||||
* questions that arise once the ready queue is empty.
|
||||
*
|
||||
* \note Instance of this class should *never* be inserted into the scheduler's
|
||||
* ready queue, as the IdleThread should only be executed if there is no
|
||||
* proper work to do.
|
||||
*/
|
||||
class IdleThread : public Thread {
|
||||
public:
|
||||
explicit IdleThread() : Thread() {}
|
||||
|
||||
/*! \brief Wait for a thread to become ready and sleep in the meantime.
|
||||
*
|
||||
*/
|
||||
void action() override;
|
||||
};
|
||||
61
kernel/thread/scheduler.cc
Normal file
61
kernel/thread/scheduler.cc
Normal file
@@ -0,0 +1,61 @@
|
||||
// vim: set noet ts=4 sw=4:
|
||||
|
||||
#include "scheduler.h"
|
||||
|
||||
#include "../arch/core.h"
|
||||
#include "../debug/assert.h" // IWYU pragma: keep
|
||||
|
||||
Scheduler::Scheduler() {}
|
||||
|
||||
Thread *Scheduler::getNext() {
|
||||
Thread *next = readylist.dequeue();
|
||||
if (next == nullptr) {
|
||||
next = &idleThread;
|
||||
}
|
||||
|
||||
assert(next != nullptr && "No thread available!");
|
||||
return next;
|
||||
}
|
||||
|
||||
void Scheduler::schedule() { dispatcher.go(getNext()); }
|
||||
|
||||
void Scheduler::ready(Thread *that) {
|
||||
assert(that != nullptr && "nullptr pointer not allowed for ready list!");
|
||||
|
||||
assert(static_cast<Thread *>(&idleThread) != that &&
|
||||
"IdleThread must not be placed in ready list!");
|
||||
|
||||
readylist.enqueue(*that);
|
||||
}
|
||||
|
||||
void Scheduler::resume(bool ready) {
|
||||
Thread *me = dispatcher.active();
|
||||
assert(me != nullptr && "Pointer to active thread should never be nullptr");
|
||||
|
||||
if (true) {
|
||||
// Be careful, never put the idle thread into the ready list
|
||||
bool is_idle_thread = static_cast<Thread *>(&idleThread) == me;
|
||||
|
||||
if (ready && readylist.is_empty()) {
|
||||
return;
|
||||
} else if (!is_idle_thread) {
|
||||
if (ready) readylist.enqueue(*me);
|
||||
}
|
||||
}
|
||||
|
||||
dispatcher.dispatch(getNext());
|
||||
}
|
||||
|
||||
void Scheduler::exit() {
|
||||
Thread *next = getNext();
|
||||
|
||||
dispatcher.dispatch(next);
|
||||
}
|
||||
|
||||
void Scheduler::kill(Thread *that) {
|
||||
if (dispatcher.active() == that) {
|
||||
exit();
|
||||
}
|
||||
}
|
||||
|
||||
bool Scheduler::isEmpty() const { return readylist.is_empty(); }
|
||||
135
kernel/thread/scheduler.h
Normal file
135
kernel/thread/scheduler.h
Normal file
@@ -0,0 +1,135 @@
|
||||
// vim: set noet ts=4 sw=4:
|
||||
|
||||
/*! \file
|
||||
*
|
||||
* \brief \ref Scheduler to manage the \ref Thread "threads"
|
||||
*/
|
||||
|
||||
#pragma once
|
||||
#include "../object/queue.h"
|
||||
#include "../types.h"
|
||||
#include "dispatcher.h"
|
||||
#include "idlethread.h"
|
||||
#include "thread.h"
|
||||
|
||||
/*! \brief The scheduler plans the threads' execution order and, from this,
|
||||
* selects the next thread to be running.
|
||||
* \ingroup thread
|
||||
*
|
||||
* The scheduler manages the ready queue (a private \ref Queue object),
|
||||
* that is the list of threads that are ready to execute. The scheduler
|
||||
* arranges threads in a FIFO order, that is, when a thread is set ready, it
|
||||
* will be appended to the end of the queue, while threads to be executed are
|
||||
* taken from the front of the queue.
|
||||
*/
|
||||
class Scheduler {
|
||||
/*! \brief a Dispatcher object, providing the low level context switching
|
||||
* routines.
|
||||
*/
|
||||
Dispatcher dispatcher;
|
||||
|
||||
/*! \brief Helper to retrieve next Thread
|
||||
* \return pointer of next thread
|
||||
*
|
||||
*/
|
||||
Thread* getNext();
|
||||
|
||||
/*! \brief List of threads, ready to be run
|
||||
*/
|
||||
Queue<Thread> readylist;
|
||||
|
||||
/*! \brief Idle thread
|
||||
*/
|
||||
IdleThread idleThread;
|
||||
|
||||
public:
|
||||
Scheduler();
|
||||
|
||||
/*! \brief Start scheduling
|
||||
*
|
||||
* This method starts the scheduling by removing the first thread from
|
||||
* the ready queue and activating it. \MPStuBS needs to call this method
|
||||
* once for every CPU core to dispatch the first thread.
|
||||
*
|
||||
*/
|
||||
void schedule();
|
||||
|
||||
/*! \brief Include a thread in scheduling decisions.
|
||||
*
|
||||
* This method will register a thread for scheduling. It will be appended
|
||||
* to the ready queue and dispatched once its time has come.
|
||||
* \param that \ref Thread to be scheduled
|
||||
*
|
||||
*/
|
||||
void ready(Thread* that);
|
||||
|
||||
/*! \brief (Self-)termination of the calling thread.
|
||||
*
|
||||
* This method can be used by a thread to exit itself. The calling
|
||||
* thread will not be appended to the ready queue; a reschedule will be
|
||||
* issued.
|
||||
*
|
||||
*/
|
||||
void exit();
|
||||
|
||||
/*! \brief Kills the passed thread
|
||||
*
|
||||
* This method is used to kill the \ref Thread `that`.
|
||||
* For \OOStuBS, it is sufficient to remove `that` from the ready queue
|
||||
* and, thereby, exclude the thread from scheduling.
|
||||
* For \MPStuBS, a simple removal is not sufficient, as the thread might
|
||||
* currently be running on another CPU core. In this case, the thread needs
|
||||
* to be marked as *dying* (a flag checked by resume prior to enqueuing
|
||||
* into the ready queue)
|
||||
*/
|
||||
/*!
|
||||
* Note: The thread should be able to kill itself.
|
||||
*
|
||||
*
|
||||
*/
|
||||
void kill(Thread* that);
|
||||
|
||||
/*! \brief Issue a thread change
|
||||
*
|
||||
* This method issues the change of the currently active thread without
|
||||
* requiring the calling thread to be aware of the other threads.
|
||||
* Scheduling decisions, i.e. which thread will be run next, are made by
|
||||
* the scheduler itself with the knowledge of the currently ready threads.
|
||||
* The currently active thread is appended to the end of the queue; the
|
||||
* first thread in the queue will be activated (to implement the FIFO
|
||||
* policy).
|
||||
*
|
||||
* \param ready If `false`, the currently active thread will not be
|
||||
* enqueued for scheduling again.
|
||||
*
|
||||
*/
|
||||
void resume(bool ready = true);
|
||||
|
||||
/*! \brief return the active thread from the dispatcher
|
||||
*/
|
||||
Thread* active() const { return dispatcher.active(); }
|
||||
|
||||
/*! \brief Checks whether the ready queue is empty.
|
||||
*
|
||||
*/
|
||||
bool isEmpty() const;
|
||||
};
|
||||
|
||||
/*! \brief Pretty print of Thread info
|
||||
* (optional)
|
||||
*/
|
||||
template <typename T>
|
||||
T& operator<<(T& out, const Thread* thread) {
|
||||
if (thread == nullptr) {
|
||||
out << "(Thread nullptr)";
|
||||
} else {
|
||||
out << "Thread " << thread->id;
|
||||
}
|
||||
return out;
|
||||
}
|
||||
|
||||
template <typename T>
|
||||
T& operator<<(T& out, const Thread& thread) {
|
||||
out << &thread;
|
||||
return out;
|
||||
}
|
||||
36
kernel/thread/thread.cc
Normal file
36
kernel/thread/thread.cc
Normal file
@@ -0,0 +1,36 @@
|
||||
// vim: set noet ts=4 sw=4:
|
||||
|
||||
#include "thread.h"
|
||||
|
||||
#include "../debug/kernelpanic.h"
|
||||
#include "../interrupt/guard.h"
|
||||
#include "debug/output.h"
|
||||
|
||||
// counter for ID
|
||||
static size_t idCounter = 1;
|
||||
|
||||
void Thread::kickoff(uintptr_t param1, uintptr_t param2, uintptr_t param3) {
|
||||
Thread *thread = reinterpret_cast<Thread *>(param1);
|
||||
assert(thread != nullptr && "Kickoff got nullptr pointer to Thread");
|
||||
(void)param2; // will be used later
|
||||
(void)param3; // will be used later
|
||||
// The core must have entered level 1/2 to cause a thread to be scheduled.
|
||||
Guard::leave();
|
||||
|
||||
thread->action();
|
||||
}
|
||||
|
||||
Thread::Thread() : queue_link(nullptr), id(idCounter++), kill_flag(false) {
|
||||
void *tos = reinterpret_cast<void *>(reserved_stack_space + STACK_SIZE);
|
||||
prepareContext(tos, context, kickoff, reinterpret_cast<uintptr_t>(this), 0,
|
||||
0);
|
||||
}
|
||||
|
||||
void Thread::resume(Thread *next) {
|
||||
assert(next != nullptr && "Pointer to next Thread must not be nullptr!");
|
||||
context_switch(&next->context, &context);
|
||||
}
|
||||
|
||||
void Thread::go() { context_launch(&context); }
|
||||
|
||||
void Thread::action() { kernelpanic("Wrong entry / missing action in Thread"); }
|
||||
102
kernel/thread/thread.h
Normal file
102
kernel/thread/thread.h
Normal file
@@ -0,0 +1,102 @@
|
||||
// vim: set noet ts=4 sw=4:
|
||||
|
||||
/*! \file
|
||||
* \brief \ref Thread abstraction required for multithreading
|
||||
*/
|
||||
|
||||
/*! \defgroup thread Multithreading
|
||||
* \brief The Multithreading Subsystem
|
||||
*
|
||||
* The group Multithreading contains all elements that form the foundation
|
||||
* of CPU multiplexing. This module's objective is to provide the abstraction
|
||||
* thread that provides a virtualized CPU for the user's applications.
|
||||
*/
|
||||
|
||||
#pragma once
|
||||
#include "../arch/context.h"
|
||||
#include "../object/queue.h"
|
||||
#include "../types.h"
|
||||
|
||||
/// Stack size for each thread
|
||||
constexpr uint32_t STACK_SIZE = 4096;
|
||||
|
||||
/*! \brief The Thread is an object used by the scheduler.
|
||||
* \ingroup thread
|
||||
*/
|
||||
class Thread {
|
||||
private:
|
||||
/*! \brief pointer to the next element of the readylist
|
||||
*/
|
||||
Thread* queue_link;
|
||||
|
||||
friend class Queue<Thread>;
|
||||
friend class Semaphore;
|
||||
/*! \brief Memory reserved for this threads stack
|
||||
*/
|
||||
alignas(16) char reserved_stack_space[STACK_SIZE];
|
||||
|
||||
protected:
|
||||
/*! \brief Context of the thread, used for saving and restoring the register
|
||||
* values when context switching.
|
||||
*/
|
||||
Context context;
|
||||
|
||||
/*! \brief The thread's entry point.
|
||||
*
|
||||
* For the first activation of a thread, we need a "return address"
|
||||
* pointing to a function that will take care of calling C++ virtual
|
||||
* methods (e.g. \ref action()), based on the thread object pointer.
|
||||
* For this purpose, we use this `kickoff()` function.
|
||||
*
|
||||
* \note As this function is never actually called, but only executed by
|
||||
* returning from the co-routine's initial stack, it may never
|
||||
* return. Otherwise garbage values from the stack will be interpreted as
|
||||
* return address and the system might crash.
|
||||
*
|
||||
* \param param1 Thread to be started
|
||||
* \param param2 Second parameter (will be used later)
|
||||
* \param param3 Third parameter (will be used later)
|
||||
*/
|
||||
static void kickoff(uintptr_t param1, uintptr_t param2, uintptr_t param3);
|
||||
|
||||
public:
|
||||
/*! \brief Unique thread id */
|
||||
const size_t id;
|
||||
|
||||
/*! \brief Marker for a dying thread
|
||||
*/
|
||||
volatile bool kill_flag;
|
||||
|
||||
/*! \brief Constructor
|
||||
* Initializes the context using \ref prepareContext with the thread's
|
||||
* stack space.
|
||||
*
|
||||
*/
|
||||
explicit Thread();
|
||||
|
||||
/*! \brief Activates the first thread on this CPU.
|
||||
*
|
||||
* Calling the method starts the first thread on the calling CPU.
|
||||
* From then on, \ref Thread::resume() must be used for all subsequent
|
||||
* context switches.
|
||||
*
|
||||
*/
|
||||
void go();
|
||||
|
||||
/*! \brief Switches from the currently running thread to the `next` one.
|
||||
*
|
||||
* The values currently present in the callee-saved registers will be
|
||||
* stored in this threads context-structure, the corresponding values
|
||||
* belonging to `next` thread will be loaded.
|
||||
* \param next Pointer to the next thread.
|
||||
*
|
||||
*/
|
||||
void resume(Thread* next);
|
||||
|
||||
/*! \brief Method that contains the thread's program code.
|
||||
*
|
||||
* Derived classes are meant to override this method to provide
|
||||
* meaningful code to be run in this thread.
|
||||
*/
|
||||
virtual void action() = 0; // XXX: why is this not always pure virtual?
|
||||
};
|
||||
Reference in New Issue
Block a user