| // Copyright Microsoft and CHERIoT Contributors. | |
| // SPDX-License-Identifier: MIT | |
| #pragma once | |
| #include "common.h" | |
| #include <cdefs.h> | |
| #include <priv/riscv.h> | |
| #include <strings.h> | |
| #include <utils.hh> | |
| namespace | |
| { | |
| /// The total number of thread priorities. | |
| constexpr uint16_t ThreadPrioNum = 32U; | |
| // Forward declaration of MultiWaiter so that we can use a pointer to it in | |
| // thread structures. | |
| class MultiWaiterInternal; | |
| template<size_t NPrios> | |
| class ThreadImpl final : private utils::NoCopyNoMove | |
| { | |
| /** | |
| * The number of threads that are currently running. This is | |
| * decremented when a thread exits. | |
| * | |
| * This count does not include the idle thread. | |
| */ | |
| inline static uint16_t threadCount = CONFIG_THREADS_NUM; | |
| static_assert(CONFIG_THREADS_NUM < | |
| std::numeric_limits<decltype(threadCount)>::max()); | |
| public: | |
| enum class ThreadState : uint8_t | |
| { | |
| Ready, | |
| Suspended, | |
| /// The thread has exited and should never be scheduled. | |
| Exited | |
| }; | |
| enum class WakeReason : uint8_t | |
| { | |
| Timer, | |
| Futex, | |
| /** | |
| * Woken because one or more events registered with a multi-waiter | |
| * object has occurred. | |
| */ | |
| MultiWaiter, | |
| /// Woken up because the data structure is gone. | |
| Delete | |
| }; | |
| /** | |
| * The number of timer ticks elapsed since boot. We use uint64_t and | |
| * assume it never overflows. Note that uint64_t is not atomic on | |
| * 32-bit platforms, so this field is always modified with interrupts | |
| * disabled. | |
| */ | |
| static inline uint64_t ticksSinceBoot; | |
| /// All threads that are suspended must be on this list. | |
| static inline ThreadImpl *waitingList; | |
| /// Returns the current running thread. | |
| static ThreadImpl *current_get() | |
| { | |
| return current; | |
| } | |
| /** | |
| * Run the scheduler to pick the next thread to run. This returns a | |
| * sealed capability to the trusted stack of the new thread, ready to be | |
| * installed. | |
| */ | |
| static TrustedStack *schedule(TrustedStack *tstack) | |
| { | |
| ThreadImpl *th = current; | |
| if (th != nullptr) | |
| { | |
| if (th->state == ThreadState::Ready) | |
| { | |
| priorityList[th->priority] = th->next; | |
| } | |
| th->tStackPtr = tstack; | |
| } | |
| else | |
| { | |
| schedTStack = tstack; | |
| } | |
| current = priorityList[highestPriority]; | |
| if (current) | |
| { | |
| Debug::Assert(highestPriority == current->priority, | |
| "Thread {} is on the run queue for priority {}, " | |
| "but is currently priority {} (originally {})", | |
| current->threadId, | |
| highestPriority, | |
| current->priority, | |
| current->OriginalPriority); | |
| if (current->priority != current->OriginalPriority) | |
| { | |
| Debug::log( | |
| "Running thread {} with boosted priority ({} from {})", | |
| current->id_get(), | |
| current->priority, | |
| current->OriginalPriority); | |
| } | |
| return current->tStackPtr; | |
| } | |
| return schedTStack; | |
| } | |
| /** | |
| * When yielding inside the scheduler compartment, we almost always want | |
| * to re-enable interrupts before ecall. If we don't, then a thread with | |
| * interrupt enabled can just call a scheduler function with a long | |
| * timeout, essentially gaining the ability to indefinitely block | |
| * interrupts. Worse, if this is the only thread, then it blocks | |
| * interrupts forever for the whole system. | |
| */ | |
| static void yield_interrupt_enabled() | |
| { | |
| __asm volatile("ecall"); | |
| } | |
| static uint32_t yield_timed() | |
| { | |
| uint64_t ticksAtStart = ticksSinceBoot; | |
| yield_interrupt_enabled(); | |
| uint64_t elapsed = ticksSinceBoot - ticksAtStart; | |
| if (elapsed > std::numeric_limits<uint32_t>::max()) | |
| { | |
| Debug::Assert(false, "Thread slept for too long, likely a bug"); | |
| return std::numeric_limits<uint32_t>::max(); | |
| } | |
| return elapsed; | |
| } | |
| /** | |
| * Yield for up to the specified timeout. May return early if the | |
| * thread is on a sleep queue. The caller should check permissions on | |
| * the timeout parameter before the call, but it will be updated only | |
| * if it is not freed by another thread during this call. | |
| * | |
| * Returns true on timeout, false otherwise. | |
| */ | |
| bool suspend(Timeout *t, | |
| ThreadImpl **newSleepQueue, | |
| bool yieldUnconditionally = false) | |
| { | |
| if (t->remaining != 0) | |
| { | |
| suspend(t->remaining, newSleepQueue); | |
| } | |
| if ((t->remaining != 0) || yieldUnconditionally) | |
| { | |
| auto elapsed = yield_timed(); | |
| if (CHERI::Capability{t}.is_valid()) | |
| { | |
| t->elapse(elapsed); | |
| if (t->remaining > 0) | |
| { | |
| return false; | |
| } | |
| } | |
| } | |
| return true; | |
| } | |
| /** | |
| * The trusted stack for the idle thread. This trusted stack has a | |
| * depth of 0, so we only use it for the register file and no | |
| * compartment calls are allowed for the idle thread. | |
| * This is also a sealed handle from the switcher, which can only be | |
| * used for comparison. | |
| */ | |
| static inline TrustedStack *schedTStack; | |
| ThreadImpl(TrustedStack *tstack, uint16_t threadid, uint16_t priority) | |
| : threadId(threadid), | |
| priority(priority), | |
| OriginalPriority(priority), | |
| expiryTime(-1), | |
| state(ThreadState::Suspended), | |
| sleepQueue(nullptr), | |
| tStackPtr(tstack) | |
| { | |
| static_assert(NPrios < | |
| std::numeric_limits<decltype(priority)>::max()); | |
| // All threads are created in blocked state. | |
| timer_list_insert(&waitingList); | |
| } | |
| /** | |
| * Ready this thread. Remove self from the timer list, and optionally, | |
| * the list of the resource that this thread was blocked on. If thread | |
| * is readied not by the resource it was blocked on but by a timeout or | |
| * the resource disappearing, do some clean-ups. | |
| * @param the reason why this thread is now able to be scheduled | |
| */ | |
| bool ready(WakeReason reason) | |
| { | |
| int64_t ticksLeft; | |
| bool schedule = false; | |
| // We must be suspended. | |
| Debug::Assert(state == ThreadState::Suspended, | |
| "Waking thread that is in state {}, not suspended", | |
| state); | |
| // First, remove self from the timer waiting list. | |
| timer_list_remove(&waitingList); | |
| if (sleepQueue != nullptr) | |
| { | |
| // We were on a list waiting for some resource. Remove ourselves | |
| // from that list. | |
| list_remove(sleepQueue); | |
| sleepQueue = nullptr; | |
| } | |
| state = ThreadState::Ready; | |
| if (priorityList[priority] == nullptr) | |
| { | |
| // First thread at this priority ready. | |
| priorityMap |= (1U << priority); | |
| if (priority > highestPriority) | |
| { | |
| highestPriority = priority; | |
| schedule = true; | |
| } | |
| } | |
| if (reason == WakeReason::Timer || reason == WakeReason::Delete) | |
| { | |
| multiWaiter = nullptr; | |
| } | |
| list_insert(&priorityList[priority]); | |
| return schedule; | |
| } | |
| /** | |
| * Mutation-safe walker for a thread list. The first argument is the | |
| * list to walk, the second is the visitor that is invoked with each | |
| * thread. An optional third argument can be used to exit the loop | |
| * early. If this returns `true` then iteration will end before | |
| * reaching the end of the loop. | |
| * | |
| * It is safe to call `ready` on threads visited by this function. | |
| */ | |
| template<typename Visitor, | |
| typename TerminateCondition = decltype([]() { return false; })> | |
| __always_inline void static walk_thread_list( | |
| ThreadImpl *&list, | |
| Visitor &&visitor, | |
| TerminateCondition &&terminateCondition = TerminateCondition{}) | |
| { | |
| for (auto *thread = list; | |
| (thread != nullptr) && !terminateCondition();) | |
| { | |
| // Get the next pointer *before* any operation that might | |
| // remove this thread from the list. | |
| auto *next = thread->next; | |
| // The thread queues are actually circularly linked lists to | |
| // avoid a head and tail pointer for each queue, so we spot the | |
| // last thread if it's the end. | |
| next = (list == next) ? nullptr : next; | |
| visitor(thread); | |
| thread = next; | |
| } | |
| } | |
| /** | |
| * Suspend this thread. Take it off the ready list. If it is suspended | |
| * waiting on a resource, add it to the list of that resource. No | |
| * matter what, it has to be added to the timer list. | |
| */ | |
| void suspend(uint32_t waitTicks, ThreadImpl **newSleepQueue) | |
| { | |
| Debug::Assert(state == ThreadState::Ready, | |
| "Suspending thread that is in state {}, not ready", | |
| state); | |
| list_remove(&priorityList[priority]); | |
| state = ThreadState::Suspended; | |
| priority_map_remove(); | |
| if (newSleepQueue != nullptr) | |
| { | |
| list_insert(newSleepQueue); | |
| sleepQueue = newSleepQueue; | |
| } | |
| expiryTime = | |
| (waitTicks == UINT32_MAX ? -1 : ticksSinceBoot + waitTicks); | |
| timer_list_insert(&waitingList); | |
| } | |
| /** | |
| * Boost the thread's thread to `newPriority` if that is larger than | |
| * the original priority or reset to the original priority if not. | |
| */ | |
| void priority_boost(uint8_t newPriority) | |
| { | |
| newPriority = std::max(newPriority, OriginalPriority); | |
| if (newPriority == priority) | |
| { | |
| return; | |
| } | |
| // If this thread is currently runnable, move it to the right run | |
| // queue. | |
| if (state == ThreadState::Ready) | |
| { | |
| list_remove(&priorityList[priority]); | |
| list_insert(&priorityList[newPriority]); | |
| priorityMap |= 1U << newPriority; | |
| priority_map_remove(); | |
| } | |
| // Sleep queues are sorted by priority. If we're on a sleep queue, | |
| // remove ourself and add ourself back in the right place. | |
| if (sleepQueue != nullptr) | |
| { | |
| list_remove(sleepQueue); | |
| } | |
| priority = newPriority; | |
| if (sleepQueue != nullptr) | |
| { | |
| list_insert(sleepQueue); | |
| } | |
| } | |
| /** | |
| * Returns true if this thread is running with the highest priority of | |
| * any runnable threads. | |
| */ | |
| bool is_highest_priority() | |
| { | |
| Debug::Assert(priority <= highestPriority, | |
| "Priority ({}) should not be higher than the highest " | |
| "priority ({})!", | |
| priority, | |
| highestPriority); | |
| return priority == highestPriority; | |
| } | |
| /** | |
| * Cause the current thread to exit. It will be removed from the | |
| * scheduling queue. The caller is responsible for invoking the | |
| * scheduler to run another thread. | |
| * | |
| * Returns true if this was the last thread, false otherwise. | |
| */ | |
| static bool exit() | |
| { | |
| Debug::log("Thread exited, {} threads remaining", threadCount - 1); | |
| current->list_remove(&priorityList[current->priority]); | |
| current->state = ThreadState::Exited; | |
| return (--threadCount) == 0; | |
| } | |
| /** | |
| * Insert self into a list of threads. headPtr can be nullptr if we are | |
| * the first one on this list. The list is sorted by priority. Higher | |
| * priority is at head, lower at tail. | |
| */ | |
| void list_insert(ThreadImpl **headPtr) | |
| { | |
| ThreadImpl *head = *headPtr; | |
| if (head == nullptr) | |
| { | |
| next = prev = *headPtr = this; | |
| } | |
| else | |
| { | |
| ThreadImpl *iter = head->prev; | |
| ThreadImpl *iterNext; | |
| // Go back from tail, and stop at the first Thread whose | |
| // Priority >= ours. | |
| while (iter->priority < priority) | |
| { | |
| iter = iter->prev; | |
| if (iter == head->prev) | |
| { | |
| break; | |
| } | |
| } | |
| iterNext = iter->next; | |
| iter->next = iterNext->prev = this; | |
| next = iterNext; | |
| prev = iter; | |
| if (priority > head->priority) | |
| { | |
| *headPtr = this; | |
| } | |
| } | |
| } | |
| /// Same as list_insert(), but sorted on ExpiryTime. | |
| void timer_list_insert(ThreadImpl **headPtr) | |
| { | |
| ThreadImpl *head = *headPtr; | |
| Debug::Assert(state == ThreadState::Suspended, | |
| "Inserting thread into timer list that is in state " | |
| "{}, not suspended", | |
| state); | |
| if (head == nullptr) | |
| { | |
| timerNext = timerPrev = *headPtr = this; | |
| } | |
| else | |
| { | |
| ThreadImpl *iter = head->timerPrev; | |
| ThreadImpl *iterNext; | |
| // Go back from tail, and stop at the first Thread whose expiry | |
| // <= ours. | |
| while (iter->expiryTime > expiryTime) | |
| { | |
| iter = iter->timerPrev; | |
| if (iter == head->timerPrev) | |
| { | |
| break; | |
| } | |
| } | |
| iterNext = iter->timerNext; | |
| iter->timerNext = iterNext->timerPrev = this; | |
| timerNext = iterNext; | |
| timerPrev = iter; | |
| if (expiryTime < head->expiryTime) | |
| { | |
| *headPtr = this; | |
| } | |
| } | |
| } | |
| /// Remove self from the list headPtr points to. | |
| void list_remove(ThreadImpl **headPtr) | |
| { | |
| ThreadImpl *head = *headPtr; | |
| if (next == this) | |
| { | |
| // We are the only one left on this list, clear the head. | |
| Debug::Assert(prev == this, | |
| "Invalid loop in thread list. Next pointer " | |
| "points to this ({}) but previous pointer is {}", | |
| this, | |
| prev); | |
| *headPtr = nullptr; | |
| } | |
| else | |
| { | |
| ThreadImpl *prevThread = prev; | |
| ThreadImpl *nextThread = next; | |
| prevThread->next = nextThread; | |
| nextThread->prev = prevThread; | |
| if (head == this) | |
| { | |
| *headPtr = next; | |
| } | |
| } | |
| next = prev = nullptr; | |
| } | |
| /// Same as list_remove(), but for the timer list. | |
| void timer_list_remove(ThreadImpl **headPtr) | |
| { | |
| ThreadImpl *head = *headPtr; | |
| if (timerNext == this) | |
| { | |
| // We are the only one left on this list, clear the head. | |
| Debug::Assert(timerPrev == this, | |
| "Invalid loop in thread list. Next pointer " | |
| "points to this ({}) but previous pointer is {}", | |
| this, | |
| timerPrev); | |
| *headPtr = nullptr; | |
| } | |
| else | |
| { | |
| ThreadImpl *prevThread = timerPrev; | |
| ThreadImpl *nextThread = timerNext; | |
| prevThread->timerNext = nextThread; | |
| nextThread->timerPrev = prevThread; | |
| if (head == this) | |
| { | |
| *headPtr = timerNext; | |
| } | |
| } | |
| timerNext = timerPrev = nullptr; | |
| } | |
| uint16_t id_get() | |
| { | |
| return threadId; | |
| } | |
| uint8_t priority_get() | |
| { | |
| return priority; | |
| } | |
| ~ThreadImpl() | |
| { | |
| // We have static definition of threads. We only create threads in | |
| // the boot loader and never destroy threads. | |
| panic(); | |
| } | |
| /// Linked list fields. They are nullptr, unless blocked on a resource, | |
| /// which then get linked to the list of that resource. | |
| ///@{ | |
| ThreadImpl *prev; | |
| ThreadImpl *next; | |
| ///@} | |
| /// Linked list fields to the global timer list, when this thread is | |
| /// blocked with a timeout. | |
| ///@{ | |
| ThreadImpl *timerPrev; | |
| ThreadImpl *timerNext; | |
| ///@} | |
| /// Pointer to the list of the resource this thread is blocked on. | |
| ThreadImpl **sleepQueue; | |
| /// If suspended, when will this thread expire. The maximum value is | |
| /// special-cased to mean blocked indefinitely. | |
| uint64_t expiryTime; | |
| /// The number of cycles that this thread has been scheduled for. | |
| uint64_t cycles; | |
| /// The number of cycles accounted to the idle thread. | |
| static inline uint64_t idleThreadCycles; | |
| union | |
| { | |
| /// State associated with waiting on a futex. | |
| struct | |
| { | |
| /** | |
| * If this thread is blocked on a futex, this holds the address | |
| * of the futex. This is set to 0 if woken via timeout. | |
| */ | |
| ptraddr_t futexWaitAddress; | |
| /** | |
| * The thread that we're priority boosting. This is ignored if | |
| * `futexPriorityInheriting` is false. | |
| */ | |
| uint16_t futexPriorityBoostedThread : 16; | |
| /** | |
| * If this thread is waiting on a futex, should it be priority | |
| * boosting the current holder of the futex? | |
| */ | |
| bool futexPriorityInheriting : 1; | |
| }; | |
| /** | |
| * If this thread is blocked on a multiwaiter, this holds the | |
| * address of the multiwaiter object. | |
| */ | |
| MultiWaiterInternal *multiWaiter; | |
| }; | |
| TrustedStack *tStackPtr; | |
| private: | |
| /** | |
| * Helper to remove a thread from the priority map and update the | |
| * highest priority, if it was the last runnable thread at that | |
| * priority level. | |
| */ | |
| void priority_map_remove() | |
| { | |
| if (priorityList[priority] == nullptr) | |
| { | |
| // We just removed the last ready thread at this priority. | |
| priorityMap &= ~(1U << priority); | |
| if (priorityMap == 0) | |
| { | |
| highestPriority = 0; | |
| } | |
| else | |
| { | |
| // clz is only for 32-bit. | |
| static_assert(NPrios <= 32); | |
| uint_fast16_t topZeroes = clz(priorityMap); | |
| highestPriority = NPrios - 1 - topZeroes; | |
| } | |
| } | |
| } | |
| /// the current runnning thread | |
| static inline ThreadImpl *current; | |
| /// NPrios number of lists, each linking the threads of this priority | |
| static inline ThreadImpl *priorityList[NPrios]; | |
| /// A bit field indicating the presence of ready threads. A set bit at | |
| /// bit index N means there's at least one thread ready with priority N. | |
| static inline uint32_t priorityMap; | |
| /// the highest priority of all the current threads that are ready | |
| static inline uint16_t highestPriority; | |
| uint16_t threadId; | |
| /** | |
| * The current priority level for this thread. This may be influenced | |
| * by priority inheritance. | |
| */ | |
| uint8_t priority; | |
| /// The original priority level for this thread. This never changes. | |
| const uint8_t OriginalPriority; | |
| ThreadState state; | |
| }; | |
| using Thread = ThreadImpl<ThreadPrioNum>; | |
| } // namespace |