blob: 0f7ddc0c97862a3f8f0831e3ad9797cb7fe2a2d2 [file] [log] [blame]
// Copyright 2018 The Fuchsia Authors
//
// Use of this source code is governed by a MIT-style
// license that can be found in the LICENSE file or at
// https://opensource.org/licenses/MIT
#include "kernel/scheduler.h"
#include <assert.h>
#include <debug.h>
#include <inttypes.h>
#include <lib/counters.h>
#include <lib/kconcurrent/chainlock.h>
#include <lib/kconcurrent/chainlock_transaction.h>
#include <lib/ktrace.h>
#include <lib/zircon-internal/macros.h>
#include <platform.h>
#include <stdio.h>
#include <string.h>
#include <zircon/errors.h>
#include <zircon/listnode.h>
#include <zircon/time.h>
#include <zircon/types.h>
#include <new>
#include <arch/ops.h>
#include <arch/thread.h>
#include <ffl/string.h>
#include <kernel/auto_lock.h>
#include <kernel/auto_preempt_disabler.h>
#include <kernel/cpu.h>
#include <kernel/lockdep.h>
#include <kernel/mp.h>
#include <kernel/percpu.h>
#include <kernel/scheduler_internal.h>
#include <kernel/scheduler_state.h>
#include <kernel/thread.h>
#include <ktl/algorithm.h>
#include <ktl/forward.h>
#include <ktl/limits.h>
#include <ktl/move.h>
#include <ktl/pair.h>
#include <ktl/span.h>
#include <ktl/tuple.h>
#include <object/process_dispatcher.h>
#include <object/thread_dispatcher.h>
#include <vm/vm.h>
#include <ktl/enforce.h>
using ffl::Round;
using ::kconcurrent::SchedulerUtils;
// Counts the number of times we set the preemption timer to fire at a point
// that's prior to the time at which the CPU last entered the scheduler. See
// also the comment where this counter is used.
KCOUNTER(counter_preempt_past, "scheduler.preempt_past")
// Counts the number of times we dequeue a deadline thread whose finish_time is
// earlier than its eligible_time.
KCOUNTER(counter_deadline_past, "scheduler.deadline_past")
namespace {
// The minimum possible weight and its reciprocal.
constexpr SchedWeight kMinWeight = SchedulerState::ConvertPriorityToWeight(LOWEST_PRIORITY);
constexpr SchedWeight kReciprocalMinWeight = 1 / kMinWeight;
// Utility operator to make expressions more succinct that update thread times
// and durations of basic types using the fixed-point counterparts.
constexpr zx_time_t& operator+=(zx_time_t& value, SchedDuration delta) {
value += delta.raw_value();
return value;
}
inline zx_thread_state_t UserThreadState(const Thread* thread) TA_REQ_SHARED(thread->get_lock()) {
switch (thread->state()) {
case THREAD_INITIAL:
case THREAD_READY:
return ZX_THREAD_STATE_NEW;
case THREAD_RUNNING:
return ZX_THREAD_STATE_RUNNING;
case THREAD_BLOCKED:
case THREAD_BLOCKED_READ_LOCK:
case THREAD_SLEEPING:
return ZX_THREAD_STATE_BLOCKED;
case THREAD_SUSPENDED:
return ZX_THREAD_STATE_SUSPENDED;
case THREAD_DEATH:
return ZX_THREAD_STATE_DEAD;
default:
return UINT32_MAX;
}
}
constexpr int32_t kIdleWeight = ktl::numeric_limits<int32_t>::min();
// Writes a context switch record to the ktrace buffer. This is always enabled
// so that user mode tracing can track which threads are running.
inline void TraceContextSwitch(const Thread* current_thread, const Thread* next_thread,
cpu_num_t current_cpu)
TA_REQ_SHARED(current_thread->get_lock(), next_thread->get_lock()) {
const SchedulerState& current_state = current_thread->scheduler_state();
const SchedulerState& next_state = next_thread->scheduler_state();
KTRACE_CONTEXT_SWITCH(
"kernel:sched", current_cpu, UserThreadState(current_thread), current_thread->fxt_ref(),
next_thread->fxt_ref(),
("outgoing_weight", current_thread->IsIdle() ? kIdleWeight : current_state.weight()),
("incoming_weight", next_thread->IsIdle() ? kIdleWeight : next_state.weight()));
}
// Writes a thread wakeup record to the ktrace buffer. This is always enabled
// so that user mode tracing can track which threads are waking.
inline void TraceWakeup(const Thread* thread, cpu_num_t target_cpu)
TA_REQ_SHARED(thread->get_lock()) {
const SchedulerState& state = thread->scheduler_state();
KTRACE_THREAD_WAKEUP("kernel:sched", target_cpu, thread->fxt_ref(),
("weight", thread->IsIdle() ? kIdleWeight : state.weight()));
}
// Returns a delta value to additively update a predictor. Compares the given
// sample to the current value of the predictor and returns a delta such that
// the predictor either exponentially peaks or decays toward the sample. The
// rate of decay depends on the alpha parameter, while the rate of peaking
// depends on the beta parameter. The predictor is not permitted to become
// negative.
//
// A single-rate exponential moving average is updated as follows:
//
// Sn = Sn-1 + a * (Yn - Sn-1)
//
// This function updates the exponential moving average using potentially
// different rates for peak and decay:
//
// D = Yn - Sn-1
// [ Sn-1 + a * D if D < 0
// Sn = [
// [ Sn-1 + b * D if D >= 0
//
template <typename T, typename Alpha, typename Beta>
constexpr T PeakDecayDelta(T value, T sample, Alpha alpha, Beta beta) {
const T delta = sample - value;
return ktl::max<T>(delta >= 0 ? T{beta * delta} : T{alpha * delta}, -value);
}
// Reset this CPU's preemption timer to fire at |deadline|.
//
// |current_cpu| is the current CPU.
//
// |now| is the time that was latched when the CPU entered the scheduler.
//
// Must be called with interrupts disabled.
void PreemptReset(cpu_num_t current_cpu, zx_time_t now, zx_time_t deadline) {
// Setting a preemption time that's prior to the point at which the CPU
// entered the scheduler indicates at worst a bug, or at best a wasted
// reschedule.
//
// What do we mean by wasted reschedule? When leaving the scheduler, if the
// preemption timer was set to a point prior to entering the scheduler, it
// will immediately fire once we've re-enabled interrupts. When it fires,
// we'll then re-enter the scheduler (provided that preemption is not
// disabled) without running the the current ask for any appreciable amount of
// time. Instead, we should have set the preemption timer to a point that's
// after the latched entry time and avoided an unnecessary interrupt and trip
// through the scheduler.
//
// TODO(https://fxbug.dev/42182770): For now, simply count the number of times
// this happens. Once we have eliminated all causes of "preemption time in
// the past" (whether they are bugs or simply missed optimization
// opportunities) replace this counter with a DEBUG_ASSERT.
//
// Aside from simple bugs, how can we end up with a preemption time that's
// earlier than the time at which we last entered the scheduler? Consider the
// case where the current thread, A, is blocking and the run queue contains
// just one thread, B, that's deadline scheduled and whose finish time has
// already elapsed. In other words, B's finish time is earlier than the time
// at which we last entered the scheduler. In this case, we'll end up
// scheduling B and setting the preemption timer to B's finish time. Longer
// term, we should consider resetting or reactivating all eligible tasks whose
// finish times would be earlier than the point at which we entered the
// scheduler. See also |counter_deadline_past|.
if (deadline < now) {
kcounter_add(counter_preempt_past, 1);
}
percpu::Get(current_cpu).timer_queue.PreemptReset(deadline);
}
} // anonymous namespace
// Records details about the threads entering/exiting the run queues for various
// CPUs, as well as which task on each CPU is currently active. These events are
// used for trace analysis to compute statistics about overall utilization,
// taking CPU affinity into account.
inline void Scheduler::TraceThreadQueueEvent(const fxt::InternedString& name,
const Thread* thread) const {
// Traces marking the end of a queue/dequeue operation have arguments encoded
// as follows:
//
// arg0[ 0..64] : TID
//
// arg1[ 0..15] : CPU availability mask.
// arg1[16..19] : CPU_ID of the affected queue.
// arg1[20..27] : Number of runnable tasks on this CPU after the queue event.
// arg1[28..28] : 1 == fair, 0 == deadline
// arg1[29..29] : 1 == eligible, 0 == ineligible
// arg1[30..30] : 1 == idle thread, 0 == normal thread
//
if constexpr (SCHEDULER_QUEUE_TRACING_ENABLED) {
const zx_time_t now = current_time(); // TODO(johngro): plumb this in from above
const bool fair = IsFairThread(thread);
const bool eligible = fair || (thread->scheduler_state().start_time_ <= now);
const size_t cnt = fair_run_queue_.size() + deadline_run_queue_.size() +
((active_thread_ && !active_thread_->IsIdle()) ? 1 : 0);
const uint64_t arg0 = thread->tid();
const uint64_t arg1 =
(thread->scheduler_state().GetEffectiveCpuMask(mp_get_active_mask()) & 0xFFFF) |
(ktl::clamp<uint64_t>(this_cpu_, 0, 0xF) << 16) |
(ktl::clamp<uint64_t>(cnt, 0, 0xFF) << 20) | ((fair ? 1 : 0) << 28) |
((eligible ? 1 : 0) << 29) | ((thread->IsIdle() ? 1 : 0) << 30);
ktrace_probe(TraceAlways, TraceContext::Cpu, name, arg0, arg1);
}
}
void Scheduler::Dump(FILE* output_target) {
Guard<MonitoredSpinLock, NoIrqSave> queue_guard{&queue_lock_, SOURCE_TAG};
fprintf(output_target,
"\ttweight=%s nfair=%d ndeadline=%d vtime=%" PRId64 " period=%" PRId64 " tema=%" PRId64
" tutil=%s\n",
Format(weight_total_).c_str(), runnable_fair_task_count_, runnable_deadline_task_count_,
virtual_time_.raw_value(), scheduling_period_grans_.raw_value(),
total_expected_runtime_ns_.raw_value(), Format(total_deadline_utilization_).c_str());
if (active_thread_ != nullptr) {
AssertInScheduler(*active_thread_);
const SchedulerState& state = const_cast<const Thread*>(active_thread_)->scheduler_state();
const EffectiveProfile& ep = state.effective_profile();
if (ep.IsFair()) {
fprintf(output_target,
"\t-> name=%s weight=%s start=%" PRId64 " finish=%" PRId64 " ts=%" PRId64
" ema=%" PRId64 "\n",
active_thread_->name(), Format(ep.fair.weight).c_str(), state.start_time_.raw_value(),
state.finish_time_.raw_value(), state.time_slice_ns_.raw_value(),
state.expected_runtime_ns_.raw_value());
} else {
fprintf(output_target,
"\t-> name=%s deadline=(%" PRId64 ", %" PRId64 ") start=%" PRId64 " finish=%" PRId64
" ts=%" PRId64 " ema=%" PRId64 "\n",
active_thread_->name(), ep.deadline.capacity_ns.raw_value(),
ep.deadline.deadline_ns.raw_value(), state.start_time_.raw_value(),
state.finish_time_.raw_value(), state.time_slice_ns_.raw_value(),
state.expected_runtime_ns_.raw_value());
}
}
for (const Thread& thread : deadline_run_queue_) {
AssertInScheduler(thread);
const SchedulerState& state = thread.scheduler_state();
const EffectiveProfile& ep = state.effective_profile();
fprintf(output_target,
"\t name=%s deadline=(%" PRId64 ", %" PRId64 ") start=%" PRId64 " finish=%" PRId64
" ts=%" PRId64 " ema=%" PRId64 "\n",
thread.name(), ep.deadline.capacity_ns.raw_value(), ep.deadline.deadline_ns.raw_value(),
state.start_time_.raw_value(), state.finish_time_.raw_value(),
state.time_slice_ns_.raw_value(), state.expected_runtime_ns_.raw_value());
}
for (const Thread& thread : fair_run_queue_) {
AssertInScheduler(thread);
const SchedulerState& state = thread.scheduler_state();
const EffectiveProfile& ep = state.effective_profile();
fprintf(output_target,
"\t name=%s weight=%s start=%" PRId64 " finish=%" PRId64 " ts=%" PRId64
" ema=%" PRId64 "\n",
thread.name(), Format(ep.fair.weight).c_str(), state.start_time_.raw_value(),
state.finish_time_.raw_value(), state.time_slice_ns_.raw_value(),
state.expected_runtime_ns_.raw_value());
}
}
void Scheduler::DumpActiveThread(FILE* output_target) {
Guard<MonitoredSpinLock, NoIrqSave> queue_guard{&queue_lock_, SOURCE_TAG};
if (active_thread_ != nullptr) {
AssertInScheduler(*active_thread_);
fprintf(output_target, "thread: pid=%lu tid=%lu\n", active_thread_->pid(),
active_thread_->tid());
ThreadDispatcher* user_thread = active_thread_->user_thread();
if (user_thread != nullptr) {
ProcessDispatcher* process = user_thread->process();
char name[ZX_MAX_NAME_LEN]{};
[[maybe_unused]] zx_status_t status = process->get_name(name);
DEBUG_ASSERT(status == ZX_OK);
fprintf(output_target, "process: name=%s\n", name);
}
}
}
SchedWeight Scheduler::GetTotalWeight() const {
Guard<MonitoredSpinLock, IrqSave> guard{&queue_lock_, SOURCE_TAG};
return weight_total_;
}
size_t Scheduler::GetRunnableTasks() const {
Guard<MonitoredSpinLock, IrqSave> guard{&queue_lock_, SOURCE_TAG};
const int64_t total_runnable_tasks = runnable_fair_task_count_ + runnable_deadline_task_count_;
return static_cast<size_t>(total_runnable_tasks);
}
// Performs an augmented binary search for the task with the earliest finish
// time that also has a start time equal to or later than the given eligible
// time. An optional predicate may be supplied to filter candidates based on
// additional conditions.
//
// The tree is ordered by start time and is augmented by maintaining an
// additional invariant: each task node in the tree stores the minimum finish
// time of its descendents, including itself, in addition to its own start and
// finish time. The combination of these three values permits traversinng the
// tree along a perfect partition of minimum finish times with eligible start
// times.
//
// See fbl/wavl_tree_best_node_observer.h for an explanation of how the
// augmented invariant is maintained.
Thread* Scheduler::FindEarliestEligibleThread(RunQueue* run_queue, SchedTime eligible_time) {
return FindEarliestEligibleThread(run_queue, eligible_time, [](const auto iter) { return true; });
}
template <typename Predicate>
Thread* Scheduler::FindEarliestEligibleThread(RunQueue* run_queue, SchedTime eligible_time,
Predicate&& predicate) {
auto GetSchedulerState = [this](const Thread& t)
TA_REQ(this->queue_lock_) -> const SchedulerState& {
AssertInScheduler(t);
return t.scheduler_state();
};
// Early out if there is no eligible thread.
if (run_queue->is_empty() || GetSchedulerState(run_queue->front()).start_time_ > eligible_time) {
return nullptr;
}
// Deduces either Predicate& or const Predicate&, preserving the const
// qualification of the predicate.
decltype(auto) accept = ktl::forward<Predicate>(predicate);
auto node = run_queue->root();
auto subtree = run_queue->end();
auto path = run_queue->end();
// Descend the tree, with |node| following the path from the root to a leaf,
// such that the path partitions the tree into two parts: the nodes on the
// left represent eligible tasks, while the nodes on the right represent tasks
// that are not eligible. Eligible tasks are both in the left partition and
// along the search path, tracked by |path|.
while (node) {
const SchedulerState& node_state = GetSchedulerState(*node);
if (node_state.start_time_ <= eligible_time) {
if (!path || GetSchedulerState(*path).finish_time_ > node_state.finish_time_) {
path = node;
}
if (auto left = node.left();
!subtree || (left && (GetSchedulerState(*subtree).min_finish_time_ >
GetSchedulerState(*left).min_finish_time_))) {
subtree = left;
}
node = node.right();
} else {
node = node.left();
}
}
if (!subtree) {
return path && accept(path) ? path.CopyPointer() : nullptr;
}
const SchedulerState& subtree_state = GetSchedulerState(*subtree);
if ((subtree_state.min_finish_time_ >= GetSchedulerState(*path).finish_time_) && accept(path)) {
return path.CopyPointer();
}
// Find the node with the earliest finish time among the descendants of the
// subtree with the smallest minimum finish time.
node = subtree;
do {
const SchedulerState& node_state = GetSchedulerState(*node);
if ((subtree_state.min_finish_time_ == node_state.finish_time_) && accept(node)) {
return node.CopyPointer();
}
if (auto left = node.left();
left && (node_state.min_finish_time_ == GetSchedulerState(*left).min_finish_time_)) {
node = left;
} else {
node = node.right();
}
} while (node);
return nullptr;
}
Scheduler* Scheduler::Get() { return Get(arch_curr_cpu_num()); }
Scheduler* Scheduler::Get(cpu_num_t cpu) { return &percpu::Get(cpu).scheduler; }
void Scheduler::InitializeThread(Thread* thread, const SchedulerState::BaseProfile& profile) {
new (&thread->scheduler_state()) SchedulerState{profile};
thread->scheduler_state().expected_runtime_ns_ =
profile.IsFair() ? kDefaultMinimumGranularity : profile.deadline.capacity_ns;
}
// Initialize the first thread to run on the current CPU. Called from
// thread_construct_first, this method will initialize the thread's scheduler
// state, then mark the thread as being "active" in its cpu's scheduler.
void Scheduler::InitializeFirstThread(Thread* thread) {
cpu_num_t current_cpu = arch_curr_cpu_num();
// Construct our scheduler state and assign a "priority"
InitializeThread(thread, SchedulerState::BaseProfile{HIGHEST_PRIORITY});
// Fill out other details about the thread, making sure to assign it to the
// current CPU with hard affinity.
SchedulerState& ss = thread->scheduler_state();
ss.state_ = THREAD_RUNNING;
ss.curr_cpu_ = current_cpu;
ss.last_cpu_ = current_cpu;
ss.hard_affinity_ = cpu_num_to_mask(current_cpu);
// Finally, make sure that the thread is the active thread for the scheduler,
// and that the weight_total bookkeeping is accurate.
{
Scheduler* sched = Get(current_cpu);
Guard<MonitoredSpinLock, NoIrqSave> queue_guard{&sched->queue_lock_, SOURCE_TAG};
sched->AssertInScheduler(*thread);
SchedulerQueueState& sqs = thread->scheduler_queue_state();
sqs.active = true;
sched->active_thread_ = thread;
sched->weight_total_ = ss.effective_profile_.fair.weight;
sched->runnable_fair_task_count_++;
sched->UpdateTotalExpectedRuntime(ss.expected_runtime_ns_);
}
}
// Remove the impact of a CPUs first thread from the scheduler's bookkeeping.
//
// During initial startup, threads are not _really_ being scheduled, yet they
// can still do things like obtain locks and block, resulting in profile
// inheritance. In order to hold the scheduler's bookkeeping invariants, we
// assign these threads a fair weight, and include it in the total fair weight
// tracked by the scheduler instance. When the thread either becomes the idle
// thread (as the boot CPU first thread does), or exits (as secondary CPU first
// threads do), it is important that we remove this weight from the total
// bookkeeping. However, this is not as simple as just changing the thread's
// weight via ChangeWeight, as idle threads are special cases who contribute no
// weight to the total.
//
// So, this small method simply fixes up the bookkeeping before allowing the
// thread to move on to become the idle thread (boot CPU), or simply exiting
// (secondary CPU).
void Scheduler::RemoveFirstThread(Thread* thread) {
cpu_num_t current_cpu = arch_curr_cpu_num();
Scheduler* sched = Get(current_cpu);
SchedulerState& ss = thread->scheduler_state();
// Since this is becoming an idle thread, it must have been one of the CPU's
// first threads. It should already be bound to this core with hard affinity.
// Assert this.
DEBUG_ASSERT(ss.last_cpu_ == current_cpu);
DEBUG_ASSERT(ss.curr_cpu_ == current_cpu);
DEBUG_ASSERT(ss.hard_affinity_ == cpu_num_to_mask(current_cpu));
{
Guard<MonitoredSpinLock, NoIrqSave> queue_guard{&sched->queue_lock_, SOURCE_TAG};
sched->AssertInScheduler(*thread);
SchedulerQueueState& sqs = thread->scheduler_queue_state();
// We are becoming the idle thread. We should currently be running with a
// fair (not deadline) profile, and we should not be holding any locks
// (therefore, we should not be inheriting any profile pressure).
DEBUG_ASSERT(ss.base_profile_.IsFair());
DEBUG_ASSERT(ss.inherited_profile_values_.total_weight == SchedWeight{0});
DEBUG_ASSERT(ss.inherited_profile_values_.uncapped_utilization == SchedUtilization{0});
// We should also be the currently active thread on this core, but no
// longer. We are about to either exit, or "UnblockIdle".
DEBUG_ASSERT(sched->active_thread_ == thread);
DEBUG_ASSERT(sched->runnable_fair_task_count_ > 0);
sqs.active = false;
sched->active_thread_ = nullptr;
sched->weight_total_ -= ss.effective_profile_.fair.weight;
sched->runnable_fair_task_count_--;
sched->UpdateTotalExpectedRuntime(-ss.expected_runtime_ns_);
ss.base_profile_.fair.weight = SchedulerState::ConvertPriorityToWeight(IDLE_PRIORITY);
ss.effective_profile_.MarkBaseProfileChanged();
ss.RecomputeEffectiveProfile();
}
}
// Removes the thread at the head of the first eligible run queue. If there is
// an eligible deadline thread, it takes precedence over available fair
// threads. If there is no eligible work, attempt to steal work from other busy
// CPUs.
Thread* Scheduler::DequeueThread(SchedTime now, Guard<MonitoredSpinLock, NoIrqSave>& queue_guard) {
percpu& self = percpu::Get(this_cpu_);
if (self.idle_power_thread.pending_power_work()) {
return &self.idle_power_thread.thread();
}
if (IsDeadlineThreadEligible(now)) {
return DequeueDeadlineThread(now);
}
if (likely(!fair_run_queue_.is_empty())) {
return DequeueFairThread();
}
// Release the queue lock while attempting to steal work, leaving IRQs
// disabled. Latch our scale up factor to use while determining whether or
// not we can steal a given thread before we drop our lock.
Thread* thread;
SchedPerformanceScale scale_up_factor = performance_scale_reciprocal_.load();
queue_guard.CallUnlocked([&] {
ChainLockTransaction::AssertActive();
thread = StealWork(now, scale_up_factor);
});
// If we successfully stole a thread, it should now be in the Rescheduling
// transient state. It needed to be marked as such to prevent PI and
// Migration code from messing with its (new) scheduler bookkeeping while we
// were not holding that scheduler's lock.
//
// Now that we have re-obtained the scheduler lock, go ahead an clear the
// Rescheduling flag so that the stolen thread looks exactly like a thread
// that had been dequeued.
//
// TODO(johngro): reconsider this approach. We need to hold both the thread's
// lock and this scheduler's lock in order to add it to this scheduler. The
// thread's lock must be acquired before the scheduler's lock, which happens
// at the end of StealWork. After that, however, we need to drop both locks
// before returning from StealWork, only to immediately re-acquire the queue
// lock and return the dequeued thread (without the dequeued thread's lock
// held).
//
// If we changed the semantics of Dequeue thread to return a _locked_ thread,
// we could have StealWork return a thread which was in the Stolen transient
// state instead. Then we could lock the thread, and re-lock our scheduler's
// queue, and return a locked thread to RescheduleCommon. This would mean
// that we don't have to drop the scheduler's lock later on in order to finish
// the reschedule (or migration) operation.
if (thread != nullptr) {
AssertInScheduler(*thread);
SchedulerQueueState& sqs = thread->scheduler_queue_state();
DEBUG_ASSERT(sqs.transient_state == SchedulerQueueState::TransientState::Rescheduling);
sqs.transient_state = SchedulerQueueState::TransientState::None;
return thread;
}
return &self.idle_power_thread.thread();
}
// Attempts to steal work from other busy CPUs and move it to the local run
// queues. Returns a pointer to the stolen thread that is now associated with
// the local Scheduler instance, or nullptr is no work was stolen.
Thread* Scheduler::StealWork(SchedTime now, SchedPerformanceScale scale_up_factor) {
using TransientState = SchedulerQueueState::TransientState;
ktrace::Scope trace = LOCAL_KTRACE_BEGIN_SCOPE(DETAILED, "StealWork");
const cpu_num_t current_cpu = this_cpu();
const cpu_mask_t current_cpu_mask = cpu_num_to_mask(current_cpu);
const cpu_mask_t active_cpu_mask = mp_get_active_mask();
Thread* thread = nullptr;
const CpuSearchSet& search_set = percpu::Get(current_cpu).search_set;
for (const auto& entry : search_set.const_iterator()) {
if (entry.cpu != current_cpu && active_cpu_mask & cpu_num_to_mask(entry.cpu)) {
Scheduler* const queue = Get(entry.cpu);
// Only steal across clusters if the target is above the load threshold.
if (cluster() != entry.cluster &&
queue->predicted_queue_time_ns() <= kInterClusterThreshold) {
continue;
}
Guard<MonitoredSpinLock, NoIrqSave> queue_guard{&queue->queue_lock_, SOURCE_TAG};
// Note that in the lambdas below we will be making use of both the
// MarkHasSchedulerAccess (but only on |queue|) and
// MarkHasOwnedThreadAccess. We just acquired |queue|'s queue lock, and
// each thread we will be examining is a member of one of |queue|'s run
// queues (either fair or deadline). This should satisfy the requirements
// of both no-op checks.
// Returns true if the given thread can run on this CPU. Static analysis
// needs to be disabled, but this should be fine. We only need R/O access
// to the thread's scheduler state, which we have because we are holding
// the queue lock for a thread which belongs to that queue (see below).
const auto check_affinity = [current_cpu_mask,
active_cpu_mask](const Thread& thread) -> bool {
MarkHasOwnedThreadAccess(thread);
return current_cpu_mask & thread.scheduler_state().GetEffectiveCpuMask(active_cpu_mask);
};
// Common routine for stealing from a RunQueue when we find a thread we
// are interested in.
const auto StealFromQueue = [check_affinity, queue](RunQueue& rq, Thread& thread) {
// check_affinity is only used in a DEBUG_ASSERT. Ideally we'd annotate
// it with [[maybe_unused]], but we can't do that in a lambda capture.
ktl::ignore = check_affinity;
MarkHasOwnedThreadAccess(thread);
MarkHasSchedulerAccess(*queue);
DEBUG_ASSERT(!thread.has_migrate_fn());
DEBUG_ASSERT(check_affinity(thread));
DEBUG_ASSERT((&rq == &queue->fair_run_queue_) || (&rq == &queue->deadline_run_queue_));
rq.erase(thread);
queue->RemoveForTransition(&thread, TransientState::Stolen);
queue->TraceThreadQueueEvent("tqe_deque_steal_work"_intern, &thread);
};
// Returns true if the given thread in the run queue meets the criteria to
// run on this CPU. Don't attempt to steal any threads which are
// currently in the process of being scheduled.
const auto deadline_predicate = [check_affinity, scale_up_factor](const auto iter) -> bool {
const Thread& t = *iter;
MarkHasOwnedThreadAccess(t);
const SchedulerState& state = t.scheduler_state();
const SchedulerQueueState& sqs = t.scheduler_queue_state();
const EffectiveProfile& ep = state.effective_profile_;
const SchedUtilization scaled_utilization = ep.deadline.utilization * scale_up_factor;
const bool is_scheduleable = scaled_utilization <= kThreadUtilizationMax;
const bool transition_in_progress = sqs.transient_state != TransientState::None;
return !transition_in_progress && check_affinity(t) && is_scheduleable &&
!t.has_migrate_fn();
};
// Attempt to find a deadline thread that can run on this CPU.
thread =
queue->FindEarliestEligibleThread(&queue->deadline_run_queue_, now, deadline_predicate);
if (thread != nullptr) {
StealFromQueue(queue->deadline_run_queue_, *thread);
break;
}
// Returns true if the given thread in the run queue meets the criteria to
// run on this CPU.
//
// See the arguments for |deadline_predicate| (above) for why we can
// disable static analysis here.
const auto fair_predicate = [check_affinity](const auto iter) -> bool {
const Thread& t = *iter;
MarkHasOwnedThreadAccess(t);
const SchedulerQueueState& sqs = t.scheduler_queue_state();
const bool transition_in_progress = sqs.transient_state != TransientState::None;
return !transition_in_progress && check_affinity(t) && !t.has_migrate_fn();
};
// TODO(eieio): Revisit the eligibility time parameter if/when moving to
// WF2Q.
queue->UpdateTimeline(now);
SchedTime eligible_time = queue->virtual_time_;
for (auto iter = queue->fair_run_queue_.cbegin(); iter.IsValid();) {
const Thread& earliest_thread = *iter;
MarkHasOwnedThreadAccess(earliest_thread);
// Skip the first thread is it is in the process of being rescheduled.
// We cannot steal that one.
const SchedulerQueueState& sqs = earliest_thread.scheduler_queue_state();
const bool reschedule_in_progress = sqs.transient_state == TransientState::Rescheduling;
if (reschedule_in_progress) {
++iter;
continue;
}
const SchedTime earliest_start = earliest_thread.scheduler_state().start_time_;
eligible_time = ktl::max(eligible_time, earliest_start);
break;
}
thread =
queue->FindEarliestEligibleThread(&queue->fair_run_queue_, eligible_time, fair_predicate);
if (thread != nullptr) {
StealFromQueue(queue->fair_run_queue_, *thread);
break;
}
}
}
if (thread) {
// Associate the thread with this Scheduler, but don't enqueue it. It
// will run immediately on this CPU as if dequeued from a local queue.
ChainLockTransaction& active_clt = ChainLockTransaction::ActiveRef();
active_clt.Restart(CLT_TAG("Scheduler::StealWork (restart)"));
UnconditionalChainLockGuard thread_guard{thread->get_lock()};
active_clt.Finalize();
Guard<MonitoredSpinLock, NoIrqSave> queue_guard{&queue_lock_, SOURCE_TAG};
// The use of the no-op assert which gives us access to the thread's
// scheduler-variables requires a bit of explanation. The thread is in the
// process of being stolen. Its scheduler state cpu_id still indicates that
// it is a member of its previous scheduler, but it is no longer present in
// that scheduler. Because of this, reschedule operations on that scheduler
// will not be able to consider the thread, and will not be able to access
// the scheduler variables.
//
// A PI operation targeting the thread which runs before we acquire the
// thread's lock exclusively (just above this one) will end up acquiring
// the old scheduler's lock (because of the stale cpu_id), but it must have
// happened-before this code (because we hold the thread's lock), and will
// therefore also be able to see that the thread is in the process of being
// stolen (because it synchronized with that state when it acquired the old
// scheduler's queue lock, which was what was being held the last time the
// steal_in_progress flag was mutated. The PI operation will _never_ mutate
// any of these flags. This is a requirement, although there is no good way
// to statically enforce it.
//
// Finally, we have just acquired our own queue_lock in order to place the
// thread into our run queue. We know that no one can have mutated the
// flag, since no other reschedule operation can find the thread while it is
// moving, and PI ops will never mutate the flag. We were the ones to last
// mutate the flag while holding the thread's old scheduler queue lock, so
// our view of the flag should be coherent as well.
//
MarkHasOwnedThreadAccess(*thread);
FinishTransition(now, thread);
}
return thread;
}
// Dequeues the eligible thread with the earliest virtual finish time. The
// caller must ensure that there is at least one thread in the queue.
Thread* Scheduler::DequeueFairThread() {
ktrace::Scope trace = LOCAL_KTRACE_BEGIN_SCOPE(DETAILED, "dequeue_fair_thread");
// Snap the virtual clock to the earliest start time.
//
// It should be safe to use the no-op assert here. We know we are holding
// this scheduler's queue lock (a requirement to call the function), and we
// know that the thread must be a member of this scheduler (since we just
// found it in the fair run queue).
const auto& earliest_thread = fair_run_queue_.front();
MarkHasOwnedThreadAccess(earliest_thread);
const auto earliest_start = earliest_thread.scheduler_state().start_time_;
const SchedTime eligible_time = ktl::max(virtual_time_, earliest_start);
// Find the eligible thread with the earliest virtual finish time.
// Note: Currently, fair tasks are always eligible when added to the run
// queue, such that this search is equivalent to taking the front element of
// a tree sorted by finish time, instead of start time. However, when moving
// to the WF2Q algorithm, eligibility becomes a factor. Using the eligibility
// query now prepares for migrating the algorithm and also avoids having two
// different template instantiations of fbl::WAVLTree to support the fair and
// deadline disciplines.
Thread* const eligible_thread = FindEarliestEligibleThread(&fair_run_queue_, eligible_time);
DEBUG_ASSERT_MSG(eligible_thread != nullptr,
"virtual_time=%" PRId64 ", eligible_time=%" PRId64 " , start_time=%" PRId64
", finish_time=%" PRId64 ", min_finish_time=%" PRId64 "!",
virtual_time_.raw_value(), eligible_time.raw_value(),
earliest_thread.scheduler_state().start_time_.raw_value(),
earliest_thread.scheduler_state().finish_time_.raw_value(),
earliest_thread.scheduler_state().min_finish_time_.raw_value());
// Same argument as before for the use of the no-op assert. We are holding
// the queue lock, and we just found this thread in the scheduler.
MarkHasOwnedThreadAccess(*eligible_thread);
virtual_time_ = eligible_time;
fair_run_queue_.erase(*eligible_thread);
TraceThreadQueueEvent("tqe_deque_fair"_intern, eligible_thread);
return eligible_thread;
}
// Dequeues the eligible thread with the earliest deadline. The caller must
// ensure that there is at least one eligible thread in the queue.
Thread* Scheduler::DequeueDeadlineThread(SchedTime eligible_time) {
ktrace::Scope trace = LOCAL_KTRACE_BEGIN_SCOPE(DETAILED, "dequeue_deadline_thread");
Thread* const eligible_thread = FindEarliestEligibleThread(&deadline_run_queue_, eligible_time);
DEBUG_ASSERT_MSG(eligible_thread != nullptr, "eligible_time=%" PRId64, eligible_time.raw_value());
MarkHasOwnedThreadAccess(*eligible_thread); // See DequeueFairThread
deadline_run_queue_.erase(*eligible_thread);
TraceThreadQueueEvent("tqe_deque_deadline"_intern, eligible_thread);
const SchedulerState& state = const_cast<const Thread*>(eligible_thread)->scheduler_state();
if (state.finish_time_ <= eligible_time) {
kcounter_add(counter_deadline_past, 1);
}
trace = KTRACE_END_SCOPE(("start time", Round<uint64_t>(state.start_time_)),
("finish time", Round<uint64_t>(state.finish_time_)));
return eligible_thread;
}
// Returns the eligible thread with the earliest deadline that is also earlier
// than the given deadline. Returns nullptr if no threads meet this criteria or
// the run queue is empty.
Thread* Scheduler::FindEarlierDeadlineThread(SchedTime eligible_time, SchedTime finish_time) {
Thread* const eligible_thread = FindEarliestEligibleThread(&deadline_run_queue_, eligible_time);
if (eligible_thread != nullptr) {
MarkHasOwnedThreadAccess(*eligible_thread); // See DequeueFairThread
const bool found_earlier_deadline =
const_cast<const Thread*>(eligible_thread)->scheduler_state().finish_time_ < finish_time;
return found_earlier_deadline ? eligible_thread : nullptr;
} else {
return nullptr;
}
}
// Returns the time that the next deadline task will become eligible or infinite
// if there are no ready deadline tasks or there is pending work for the idle
// thread to perform.
SchedTime Scheduler::GetNextEligibleTime() {
if (deadline_run_queue_.is_empty()) {
return SchedTime{ZX_TIME_INFINITE};
}
const Thread& front = deadline_run_queue_.front();
MarkHasOwnedThreadAccess(front);
return front.scheduler_state().start_time_;
}
// Dequeues the eligible thread with the earliest deadline that is also earlier
// than the given deadline. Returns nullptr if no threads meet the criteria or
// the run queue is empty.
Thread* Scheduler::DequeueEarlierDeadlineThread(SchedTime eligible_time, SchedTime finish_time) {
ktrace::Scope trace = LOCAL_KTRACE_BEGIN_SCOPE(DETAILED, "dequeue_earlier_deadline_thread");
Thread* const eligible_thread = FindEarlierDeadlineThread(eligible_time, finish_time);
if (eligible_thread != nullptr) {
MarkHasOwnedThreadAccess(*eligible_thread);
deadline_run_queue_.erase(*eligible_thread);
TraceThreadQueueEvent("tqe_deque_earlier_deadline"_intern, eligible_thread);
}
return eligible_thread;
}
// Selects a thread to run. Performs any necessary maintenance if the current
// thread is changing, depending on the reason for the change.
Thread* Scheduler::EvaluateNextThread(SchedTime now, Thread* current_thread, bool timeslice_expired,
SchedDuration total_runtime_ns,
Guard<MonitoredSpinLock, NoIrqSave>& queue_guard) {
using TransientState = SchedulerQueueState::TransientState;
ktrace::Scope trace = LOCAL_KTRACE_BEGIN_SCOPE(DETAILED, "find_thread");
const bool is_idle = current_thread->IsIdle();
const bool is_active = current_thread->state() == THREAD_READY;
const bool is_deadline = IsDeadlineThread(current_thread);
const bool is_new_deadline_eligible = IsDeadlineThreadEligible(now);
const cpu_num_t current_cpu = arch_curr_cpu_num();
const cpu_mask_t current_cpu_mask = cpu_num_to_mask(current_cpu);
const cpu_mask_t active_mask = mp_get_active_mask();
// Returns true when the given thread requires active migration.
const auto needs_migration = [active_mask,
current_cpu_mask](const Thread& thread) TA_REQ(this->queue_lock_) {
// We are examining threads owned by this scheduler while holding the queue lock. We should
// have read-only access to scheduler_state, and exclusive access to scheduler_queue_state.
MarkHasOwnedThreadAccess(thread);
// Threads may be created and resumed before the thread init level. Work
// around an empty active mask by assuming only the current cpu is available.
return active_mask != 0 &&
((thread.scheduler_state().GetEffectiveCpuMask(active_mask) & current_cpu_mask) == 0);
};
Thread* next_thread = nullptr;
if (is_active && needs_migration(*current_thread)) {
// Avoid putting the current thread into the run queue in any of the paths
// below if it needs active migration. Let the migration loop below handle
// moving the thread. This avoids an edge case where time slice expiration
// coincides with an action that requires migration. Migration should take
// precedence over time slice expiration.
next_thread = current_thread;
} else if (is_active && likely(!is_idle)) {
if (timeslice_expired) {
// If the timeslice expired insert the current thread into the run queue.
QueueThread(current_thread, Placement::Insertion, now, total_runtime_ns);
} else if (is_new_deadline_eligible && is_deadline) {
// The current thread is deadline scheduled and there is at least one
// eligible deadline thread in the run queue: select the eligible thread
// with the earliest deadline, which may still be the current thread.
const SchedTime deadline_ns = current_thread->scheduler_state().finish_time_;
if (Thread* const earlier_thread = DequeueEarlierDeadlineThread(now, deadline_ns);
earlier_thread != nullptr) {
QueueThread(current_thread, Placement::Preemption, now, total_runtime_ns);
next_thread = earlier_thread;
} else {
// The current thread still has the earliest deadline.
next_thread = current_thread;
}
} else if (is_new_deadline_eligible && !is_deadline) {
// The current thread is fair scheduled and there is at least one eligible
// deadline thread in the run queue: return this thread to the run queue.
QueueThread(current_thread, Placement::Preemption, now, total_runtime_ns);
} else {
// The current thread has remaining time and no eligible contender.
next_thread = current_thread;
}
} else if (!is_active && likely(!is_idle)) {
// The current thread is no longer ready, remove its accounting. Note,
// active_thread_ can either equal current_thread at this point, or be
// nullptr if the thread has already been removed from bookkeeping. Remove
// handles this edge case and does not remove the thread from bookkeeping if
// it has already been removed.
DEBUG_ASSERT((active_thread_ == current_thread) || (active_thread_ == nullptr));
MarkHasOwnedThreadAccess(*current_thread);
Remove(current_thread);
}
// The current thread is no longer running or has returned to the run queue,
// select another thread to run.
if (next_thread == nullptr) {
next_thread = DequeueThread(now, queue_guard);
}
// If the next thread needs *active* migration, call the migration function,
// migrate the thread, and select another thread to run.
//
// Most migrations are passive. Passive migration happens whenever a thread
// becomes READY and a different CPU is selected than the last CPU the thread
// ran on.
//
// Active migration happens under the following conditions:
// 1. The CPU affinity of a thread that is READY or RUNNING is changed to
// exclude the CPU it is currently active on.
// 2. Passive migration, or active migration due to #1, selects a different
// CPU for a thread with a migration function. Migration to the next CPU
// is delayed until the migration function is called on the last CPU.
// 3. A thread that is READY or RUNNING is relocated by the periodic load
// balancer. NOT YET IMPLEMENTED.
//
cpu_mask_t cpus_to_reschedule_mask = 0;
for (; needs_migration(*next_thread); next_thread = DequeueThread(now, queue_guard)) {
MarkHasOwnedThreadAccess(*next_thread);
// Remove accounting from this run queue and flag the thread as being in the
// process of migration. If the next thread is the same thing as the
// current thread, be sure to replace the Rescheduling transient state with
// a Migrating transient state.
if (next_thread == current_thread) {
AssertInScheduler(*current_thread);
DEBUG_ASSERT(current_thread->scheduler_queue_state().transient_state ==
TransientState::Rescheduling);
current_thread->scheduler_queue_state().transient_state = TransientState::None;
}
RemoveForTransition(next_thread, TransientState::Migrating);
// Now that the thread has been removed from this scheduler and has been
// marked as being in the middle of a migration, we need to finish the
// transition by adding it to its new target scheduler, but only if the
// thread being migrated is not the current thread.
//
// Doing this involves dropping our current scheduler's queue lock, then
// obtaining the target scheduler's lock while holding the migrating
// thread's lock. Thread's locks must always be obtained before obtaining a
// scheduler's queue lock.
//
// If the migrating thread happens to be the current thread, we need to
// defer this operation until a bit later in the reschedule operation. We
// need to continue to hold the current thread's lock until after the
// point where we have obtain the next thread's lock, and have re-acquired
// our queue lock (which we we need to drop temporarily in order to obtain
// the next thread's lock).
//
// Once we hold all of the required locks, we can finish the thread's
// transition into its new scheduler, calling the migration function and
// clearing its transient state in the process.
if (current_thread != next_thread) {
queue_guard.CallUnlocked([&] {
// Lock the next thread, then call the Before stage its migrate function
// (if any). This is its last chance to run on this CPU.
ChainLockTransaction::AssertActive();
ChainLockTransaction& active_clt = ChainLockTransaction::ActiveRef();
active_clt.Restart(CLT_TAG("Scheduler::EvaluateNextThread (restart)"));
UnconditionalChainLockGuard next_thread_guard{next_thread->get_lock()};
active_clt.Finalize();
next_thread->CallMigrateFnLocked(Thread::MigrateStage::Before);
// Lock the target scheduler and finish the thread's transition to its
// new CPU. The After stage of migration will be run the next time the
// thread becomes scheduled.
const cpu_num_t target_cpu = FindTargetCpu(next_thread, FindTargetCpuReason::Migrating);
Scheduler* const target = Get(target_cpu);
Guard<MonitoredSpinLock, NoIrqSave> target_queue_guard{&target->queue_lock_, SOURCE_TAG};
MarkHasOwnedThreadAccess(*next_thread);
DEBUG_ASSERT(next_thread->scheduler_queue_state().transient_state ==
TransientState::Migrating);
target->FinishTransition(now, next_thread);
cpus_to_reschedule_mask |= cpu_num_to_mask(target_cpu);
});
}
}
// Issue reschedule IPIs to CPUs with migrated threads.
if (cpus_to_reschedule_mask) {
mp_reschedule(cpus_to_reschedule_mask, 0);
}
return next_thread;
}
cpu_num_t Scheduler::FindTargetCpu(Thread* thread, FindTargetCpuReason reason) {
ktrace::Scope trace = LOCAL_KTRACE_BEGIN_SCOPE(DETAILED, "find_target");
const cpu_num_t current_cpu = arch_curr_cpu_num();
const cpu_mask_t current_cpu_mask = cpu_num_to_mask(current_cpu);
const cpu_mask_t active_mask = mp_get_active_mask();
// Determine the set of CPUs the thread is allowed to run on.
//
// Threads may be created and resumed before the thread init level. Work around
// an empty active mask by assuming the current cpu is scheduleable.
const SchedulerState& thread_state = const_cast<const Thread*>(thread)->scheduler_state();
const cpu_mask_t available_mask =
active_mask != 0 ? thread_state.GetEffectiveCpuMask(active_mask) : current_cpu_mask;
DEBUG_ASSERT_MSG(available_mask != 0,
"thread=%s affinity=%#x soft_affinity=%#x active=%#x "
"idle=%#x arch_ints_disabled=%d",
thread->name(), thread_state.hard_affinity_, thread_state.soft_affinity_,
active_mask, mp_get_idle_mask(), arch_ints_disabled());
LOCAL_KTRACE(DETAILED, "target_mask", ("online", mp_get_online_mask()), ("active", active_mask));
// If our thread is unblocking (or coming out of sleep/suspend), and it has
// run on a CPU in the past, and it has a migration function whose Before
// phase has not been called yet, then we must always send it back to the last
// CPU it ran on.
//
// When the thread is selected to run on its last CPU, if it needs to be
// migrated, the Before stage of the migration will be executed, and the
// scheduler will then immediately re-assign the thread to a different
// scheduler to complete the migration operation.
//
const cpu_num_t last_cpu = thread_state.last_cpu_;
if ((reason == FindTargetCpuReason::Unblocking) && thread->has_migrate_fn() &&
!thread->migrate_pending() && (last_cpu != INVALID_CPU)) {
trace = KTRACE_END_SCOPE(("last_cpu", last_cpu), ("target_cpu", last_cpu));
return last_cpu;
}
// Find the best target CPU starting at the last CPU the task ran on, if any.
// Alternatives are considered in order of best to worst potential cache
// affinity.
const cpu_num_t starting_cpu = last_cpu != INVALID_CPU ? last_cpu : current_cpu;
const CpuSearchSet& search_set = percpu::Get(starting_cpu).search_set;
// TODO(https://fxbug.dev/42180608): Working on isolating a low-frequency panic due to
// apparent memory corruption of percpu intersecting CpuSearchSet, resulting
// in an invalid entry pointer and/or entry count. Adding an assert to help
// catch the corruption and include additional context. This assert is enabled
// in non-eng builds, however, the small impact is acceptable for production.
ASSERT_MSG(search_set.cpu_count() <= SMP_MAX_CPUS,
"current_cpu=%u starting_cpu=%u active_mask=%x thread=%p search_set=%p cpu_count=%zu "
"entries=%p",
current_cpu, starting_cpu, active_mask, &thread, &search_set, search_set.cpu_count(),
search_set.const_iterator().data());
// A combination of a scheduler and its latched scale-up factor. Whenever we
// consider a scheduler, we observe its current scale factor exactly once, so
// that we can be sure that the value remains consistent for all of the
// comparisons we do in |compare| and |is_sufficient| (below). Note that we
// skip obtaining the queue lock when we observe the performance scale factor.
//
// Finding a target CPU is a best effort heuristic, and we would really rather
// not be fighting over scheduler's queue lock while we do it. Other places
// in the scheduler code use the queue lock to protect these scale factors,
// but here it should be sufficient to simply atomically load the scale
// factor, which is also atomically written during its only update location in
// RescheduleCommon, which avoids formal C++ data races. All of these atomic
// accesses are relaxed, however, meaning that their values have no defined
// ordering relationship relative to other non-locked queue values (like
// predicted queue time).
struct CandidateQueue {
CandidateQueue() = default;
explicit CandidateQueue(const Scheduler* s)
: queue{s}, scale_up_factor{LatchScaleUpFactor(s)} {}
const Scheduler* queue{nullptr};
SchedPerformanceScale scale_up_factor{1};
static SchedPerformanceScale LatchScaleUpFactor(const Scheduler* s) {
return [s]()
TA_NO_THREAD_SAFETY_ANALYSIS { return s->performance_scale_reciprocal_.load(); }();
}
};
// Compares candidate queues and returns true if |queue_a| is a better
// alternative than |queue_b|. This is used by the target selection loop to
// determine whether the next candidate is better than the current target.
const auto compare = [&thread_state](const CandidateQueue& a, const CandidateQueue& b) {
const SchedDuration a_predicted_queue_time_ns = a.queue->predicted_queue_time_ns();
const SchedDuration b_predicted_queue_time_ns = b.queue->predicted_queue_time_ns();
ktrace::Scope trace_compare = LOCAL_KTRACE_BEGIN_SCOPE(
DETAILED, "compare", ("predicted queue time a", Round<uint64_t>(a_predicted_queue_time_ns)),
("predicted queue time b", Round<uint64_t>(b_predicted_queue_time_ns)));
const EffectiveProfile& ep = thread_state.effective_profile_;
if (ep.IsFair()) {
// CPUs in the same logical cluster are considered equivalent in terms of
// cache affinity. Choose the least loaded among the members of a cluster.
if (a.queue->cluster() == b.queue->cluster()) {
ktl::pair a_pair{a_predicted_queue_time_ns, a.queue->predicted_deadline_utilization()};
ktl::pair b_pair{b_predicted_queue_time_ns, b.queue->predicted_deadline_utilization()};
return a_pair < b_pair;
}
// Only consider crossing cluster boundaries if the current candidate is
// above the threshold.
return b_predicted_queue_time_ns > kInterClusterThreshold &&
a_predicted_queue_time_ns < b_predicted_queue_time_ns;
} else {
const SchedUtilization utilization = ep.deadline.utilization;
const SchedUtilization scaled_utilization_a = utilization * a.scale_up_factor;
const SchedUtilization scaled_utilization_b = utilization * a.scale_up_factor;
ktl::pair a_pair{scaled_utilization_a, a_predicted_queue_time_ns};
ktl::pair b_pair{scaled_utilization_b, b_predicted_queue_time_ns};
ktl::pair a_prime{a.queue->predicted_deadline_utilization(), a_pair};
ktl::pair b_prime{b.queue->predicted_deadline_utilization(), b_pair};
return a_prime < b_prime;
}
};
// Determines whether the current target is sufficiently good to terminate the
// selection loop.
const auto is_sufficient = [&thread_state](const CandidateQueue& q) {
const SchedDuration candidate_queue_time_ns = q.queue->predicted_queue_time_ns();
ktrace::Scope trace_is_sufficient = LOCAL_KTRACE_BEGIN_SCOPE(
DETAILED, "is_sufficient",
("intra cluster threshold", Round<uint64_t>(kIntraClusterThreshold)),
("candidate q.queue time", Round<uint64_t>(candidate_queue_time_ns)));
const EffectiveProfile& ep = thread_state.effective_profile_;
if (ep.IsFair()) {
return candidate_queue_time_ns <= kIntraClusterThreshold;
}
const SchedUtilization predicted_utilization = q.queue->predicted_deadline_utilization();
const SchedUtilization utilization = ep.deadline.utilization;
const SchedUtilization scaled_utilization = utilization * q.scale_up_factor;
return candidate_queue_time_ns <= kIntraClusterThreshold &&
scaled_utilization <= kThreadUtilizationMax &&
predicted_utilization + scaled_utilization <= kCpuUtilizationLimit;
};
// Loop over the search set for CPU the task last ran on to find a suitable
// target.
cpu_num_t target_cpu = INVALID_CPU;
CandidateQueue target_queue{};
for (const auto& entry : search_set.const_iterator()) {
const cpu_num_t candidate_cpu = entry.cpu;
const bool candidate_available = available_mask & cpu_num_to_mask(candidate_cpu);
const CandidateQueue candidate_queue{Get(candidate_cpu)};
if (candidate_available &&
(target_queue.queue == nullptr || compare(candidate_queue, target_queue))) {
target_cpu = candidate_cpu;
target_queue = candidate_queue;
// Stop searching at the first sufficiently unloaded CPU.
if (is_sufficient(target_queue)) {
break;
}
}
}
DEBUG_ASSERT(target_cpu != INVALID_CPU);
trace = KTRACE_END_SCOPE(("last_cpu", last_cpu), ("target_cpu", target_cpu));
return target_cpu;
}
void Scheduler::UpdateTimeline(SchedTime now) {
ktrace::Scope trace = LOCAL_KTRACE_BEGIN_SCOPE(DETAILED, "update_vtime");
const auto runtime_ns = now - last_update_time_ns_;
last_update_time_ns_ = now;
if (weight_total_ > SchedWeight{0}) {
virtual_time_ += runtime_ns;
}
trace = KTRACE_END_SCOPE(
("runtime", Round<uint64_t>(runtime_ns)),
("virtual time",
KTRACE_ANNOTATED_VALUE(AssertHeld(queue_lock_), Round<uint64_t>(virtual_time_))));
}
void Scheduler::RescheduleCommon(Thread* const current_thread, SchedTime now,
EndTraceCallback end_outer_trace) {
using TransientState = SchedulerQueueState::TransientState;
ktrace::Scope trace =
LOCAL_KTRACE_BEGIN_SCOPE(DETAILED, "reschedule_common", ("now", Round<uint64_t>(now)));
DEBUG_ASSERT(current_thread == Thread::Current::Get());
const cpu_num_t current_cpu = arch_curr_cpu_num();
SchedulerState* const current_state = &current_thread->scheduler_state();
// No spinlocks should be held as we come into a reschedule operation. Only
// the current thread's lock should be held.
DEBUG_ASSERT_MSG(const uint32_t locks_held = arch_num_spinlocks_held();
locks_held == 0, "arch_num_spinlocks_held() == %u\n", locks_held);
DEBUG_ASSERT(!arch_blocking_disallowed());
DEBUG_ASSERT_MSG(const cpu_num_t tcpu = this_cpu();
current_cpu == tcpu, "current_cpu=%u this_cpu=%u", current_cpu, tcpu);
ChainLockTransaction& active_clt = ChainLockTransaction::ActiveRef();
active_clt.AssertNumLocksHeld(1);
CPU_STATS_INC(reschedules);
// Prepare for rescheduling by backing up the chain lock context and switching
// to the special "sched token" which we use during reschedule operations in
// order to guarantee that we will win arbitration with other active
// transactions which are not reschedule operations. We will restore this
// context at the end of the operation (whether we context switched or not).
const kconcurrent::RescheduleContext orig_chainlock_context =
SchedulerUtils::PrepareForReschedule();
Guard<MonitoredSpinLock, NoIrqSave> queue_guard{&queue_lock_, SOURCE_TAG};
UpdateTimeline(now);
// When calling into reschedule, the current thread is only allowed to be in a
// limited number of states, depending on where it came from.
//
// RUNNING : Thread is the currently active thread on this CPU.
// BLOCKED : Thread has been inserted into its blocked queue and is in the process of
// de-activating.
// SUSPENDED : Thread is in the process of suspending (but is not in any queue).
// DEAD : Thread is in the process of dying, this is its final reschedule.
//
// In particular, however, the thread may not be in the READY state. If the
// current thread were in the ready state on the way into RescheduleCommon
// (before its scheduler's queue lock was held), it would mean that the thread
// could have been subject to theft by another scheduler.
//
// Now that we hold the scheduler's lock, we can go ahead and transition a
// RUNNING thread's state to READY. We also mark the thread as being in the
// middle of a reschedule, which will prevent it from being stolen out from
// under us until after the reschedule is complete.
[&]() TA_REQ(current_thread->get_lock(), queue_lock()) {
AssertInScheduler(*current_thread);
DEBUG_ASSERT(current_thread->state() != THREAD_READY);
DEBUG_ASSERT_MSG(
current_thread->scheduler_queue_state().transient_state == TransientState::None,
"Unexpected transient state at the start of RescheduleCommon (tid %lu, "
"state %u, transient_state %u",
current_thread->tid(), static_cast<uint32_t>(current_thread->state()),
static_cast<uint32_t>(current_thread->scheduler_queue_state().transient_state));
if (current_thread->state() == THREAD_RUNNING) {
current_thread->set_ready();
current_thread->scheduler_queue_state().transient_state = TransientState::Rescheduling;
}
}();
// Assert that the current thread is a member of this scheduler, and that we
// have access to the thread's scheduler owned variables. Note, that as soon
// as we call "EvaluateNextThread", the thread's curr_cpu_ state may end up
// being reset to INVALID_CPU (if the thread is no longer ready), meaning that
// this is the last time we own and have access to these variables.
AssertInScheduler(*current_thread);
const SchedDuration total_runtime_ns = now - start_of_current_time_slice_ns_;
const SchedDuration actual_runtime_ns = now - current_state->last_started_running_;
current_state->last_started_running_ = now;
current_thread->UpdateRuntimeStats(current_thread->state());
// Update the runtime accounting for the thread that just ran.
current_state->runtime_ns_ += actual_runtime_ns;
// Adjust the rate of the current thread when demand changes. Changes in
// demand could be due to threads entering or leaving the run queue, or due
// to weights changing in the current or enqueued threads.
if (IsThreadAdjustable(current_thread) && weight_total_ != scheduled_weight_total_ &&
total_runtime_ns < current_state->time_slice_ns_) {
ktrace::Scope trace_adjust_rate = LOCAL_KTRACE_BEGIN_SCOPE(DETAILED, "adjust_rate");
EffectiveProfile& ep = current_state->effective_profile_;
scheduled_weight_total_ = weight_total_;
const SchedDuration time_slice_ns = CalculateTimeslice(current_thread);
const SchedDuration remaining_time_slice_ns =
time_slice_ns * ep.fair.normalized_timeslice_remainder;
const bool timeslice_changed = time_slice_ns != ep.fair.initial_time_slice_ns;
const bool timeslice_remaining = total_runtime_ns < remaining_time_slice_ns;
// Update the preemption timer if necessary.
if (timeslice_changed && timeslice_remaining) {
target_preemption_time_ns_ = start_of_current_time_slice_ns_ + remaining_time_slice_ns;
const SchedTime preemption_time_ns = ClampToDeadline(target_preemption_time_ns_);
DEBUG_ASSERT(preemption_time_ns <= target_preemption_time_ns_);
PreemptReset(current_cpu, now.raw_value(), preemption_time_ns.raw_value());
}
ep.fair.initial_time_slice_ns = time_slice_ns;
current_state->time_slice_ns_ = remaining_time_slice_ns;
trace_adjust_rate =
KTRACE_END_SCOPE(("remaining time slice", Round<uint64_t>(remaining_time_slice_ns)),
("total runtime", Round<uint64_t>(total_runtime_ns)));
}
// Update the time slice of a deadline task before evaluating the next task.
if (IsDeadlineThread(current_thread)) {
// Scale the actual runtime of the deadline task by the relative performance
// of the CPU, effectively increasing the capacity of the task in proportion
// to the performance ratio. The remaining time slice may become negative
// due to scheduler overhead.
current_state->time_slice_ns_ -= ScaleDown(actual_runtime_ns);
}
// Rounding in the scaling above may result in a small non-zero time slice
// when the time slice should expire from the perspective of the target
// preemption time. Use a small epsilon to avoid tripping the consistency
// check below.
const SchedDuration deadline_time_slice_epsilon{100};
// Fair and deadline tasks have different time slice accounting strategies:
// - A fair task expires when the total runtime meets or exceeds the time
// slice, which is updated only when the thread is adjusted or returns to
// the run queue.
// - A deadline task expires when the remaining time slice is exhausted,
// updated incrementally on every reschedule, or when the absolute deadline
// is reached, which may occur with remaining time slice if the task wakes
// up late.
const bool timeslice_expired =
IsFairThread(current_thread)
? total_runtime_ns >= current_state->time_slice_ns_
: now >= current_state->finish_time_ ||
current_state->time_slice_ns_ <= deadline_time_slice_epsilon;
// Check the consistency of the target preemption time and the current time
// slice.
[[maybe_unused]] const auto& ep = current_state->effective_profile_;
DEBUG_ASSERT_MSG(
now < target_preemption_time_ns_ || timeslice_expired,
"capacity_ns=%" PRId64 " deadline_ns=%" PRId64 " now=%" PRId64
" target_preemption_time_ns=%" PRId64 " total_runtime_ns=%" PRId64
" actual_runtime_ns=%" PRId64 " finish_time=%" PRId64 " time_slice_ns=%" PRId64
" start_of_current_time_slice_ns=%" PRId64,
IsDeadlineThread(current_thread) ? ep.deadline.capacity_ns.raw_value() : 0,
IsDeadlineThread(current_thread) ? ep.deadline.deadline_ns.raw_value() : 0, now.raw_value(),
target_preemption_time_ns_.raw_value(), total_runtime_ns.raw_value(),
actual_runtime_ns.raw_value(), current_state->finish_time_.raw_value(),
current_state->time_slice_ns_.raw_value(), start_of_current_time_slice_ns_.raw_value());
// Select the next thread to run.
Thread* const next_thread =
EvaluateNextThread(now, current_thread, timeslice_expired, total_runtime_ns, queue_guard);
DEBUG_ASSERT(next_thread != nullptr);
const bool thread_changed{current_thread != next_thread};
// Flush pending preemptions.
mp_reschedule(current_thread->preemption_state().preempts_pending(), 0);
current_thread->preemption_state().preempts_pending_clear();
// If the next_thread is not the same as the current thread, we will need to
// (briefly) drop the scheduler's queue lock in order to acquire the next
// thread's lock exclusively. If we have selected the same thread as before,
// we can skip this step.
//
// If we do need to acquire next_thread's lock, we use the same chain lock
// token that was used to acquire current_thread's lock. We know this is safe
// because:
//
// TODO(johngro): THIS IS NOT SAFE - FIX THIS COMMENT
//
// This turned out to not be safe after all. The deadlock potential below is
// why we need to switch to the special "scheduler" token at the start of the
// operation.
//
// While there cannot be any cycles in this individual operation (trivial to
// show since there are two different locks here), deadlock is still possible.
//
// The flow is:
// 1) T0 is running on CPU X, and is locked entering a reschedule operation.
// 2) T1 is selected as the next thread to run, and is being locked
// exclusively (after dropping the scheduler's lock). At the same time:
// 3) T2 is running on CPU Y, and is blocking on an OWQ which currently has T0
// as the owner. T1 has been selected as the new owner.
// 4) T2 and the queue have been locked by the BAAO operation.
// 5) T1 is the start of the new owner chain and is locked.
// 6) The next step in the BAAO locking is to lock the old owner chain,
// starting from T0.
//
// So:
// X has T0 and wants T1.
// Y has T1 and wants T0.
//
// CPU Y is in a sequence which obeys the backoff protocol, so as long as its
// token value > X's token value, Y will back off and we will be OK. If not,
// however, we deadlock. The reschedule operation cannot backoff and drop
// T0's lock, so it is stuck trying to get T1 from Y.
//
// TODO(johngro): END TODO
//
// 1) current_thread is currently running. Because of this, we know that it
// is the target node of it's PI graph.
// 2) next thread is currently READY, which means it is also the target node
// of it's PI graph.
// 3) PI graphs always have exactly one target node.
// 4) Therefore, current and next cannot be members of the same graph, and can
// both be acquired unconditionally.
//
if (thread_changed) {
// Mark the next_thread as "becoming scheduled" while we drop the lock.
// This will prevent another scheduler from stealing this thread out from
// under as while we have the queue_lock dropped. To make static analysis
// happy, we have to prove that this thread is currently in this scheduler,
// and that we hold the scheduler's queue_lock_.
AssertInScheduler(*next_thread);
SchedulerQueueState& sqs = next_thread->scheduler_queue_state();
// The new thread we just selected should not currently be in a transient
// state. It now needs to be in the Rescheduling state while we drop our
// current scheduler's lock in order to obtain the thread's lock
// exclusively.
DEBUG_ASSERT(sqs.transient_state == TransientState::None);
sqs.transient_state = TransientState::Rescheduling;
// The ChainLockTransaction we were in when we entered RescheduleCommon has
// already been finalized. We need to restart it in order to obtain new
// locks.
active_clt.Restart(CLT_TAG("Scheduler::RescheduleCommon (restart)"));
// Now drop the queue lock, obtain the next thread's lock, and re-acquire the queue lock.
//
// TODO(johngro): Need to talk to eieio@ to make absolutely sure that the
// current scheduler's bookkeeping is coherent at this point in time. IOW -
// if (while we had the lock dropped) someone grabbed the lock to record the
// consequences of a PI propagation, or stole a thread from our ready queue,
// we need to make sure that _their_ updating of the scheduler's bookkeeping
// will make sense and leave the bookkeeping in a valid state.
//
// Right now, I _think_ that this is the case. So far, I don't think that
// we have actually mutated the scheduler specific state in any way which
// matters. We have simple decided that we are going to switch to a
// different thread, and picked that thread out. We are now committed to
// running that thread, regardless of any external changes to our
// bookkeeping. This should be fine, any externally made changes should
// result in a reschedule being queued where (if our current choice is no
// longer the correct thread) we will pick a new best thread.
queue_guard.CallUnlocked([next_thread]() TA_ACQ(next_thread->get_lock()) {
ChainLockTransaction::AssertActive();
next_thread->get_lock().AcquireUnconditionally();
});
// Now we have all of the locks we need for the reschedule operation. Go
// ahead and re-finalize the transaction.
active_clt.Finalize();
}
// Now that we are sure that next_thread is properly locked, if we are
// actually changing threads, trace the activation of the next thread before
// context switching. We can also clear the resched-in-progress flag on the next thread.
next_thread->get_lock().AssertHeld();
if (thread_changed) {
AssertInScheduler(*next_thread);
SchedulerQueueState& sqs = next_thread->scheduler_queue_state();
DEBUG_ASSERT(sqs.transient_state == TransientState::Rescheduling);
sqs.transient_state = TransientState::None;
}
// Update the state of the current and next thread.
const SchedulerQueueState& current_queue_state = current_thread->scheduler_queue_state();
SchedulerState* const next_state = &next_thread->scheduler_state();
next_thread->set_running();
next_state->last_cpu_ = current_cpu;
DEBUG_ASSERT(next_state->curr_cpu_ == current_cpu);
active_thread_ = next_thread;
// Handle any pending migration work.
next_thread->CallMigrateFnLocked(Thread::MigrateStage::After);
// Update the expected runtime of the current thread and the per-CPU total.
// Only update the thread and aggregate values if the current thread is still
// associated with this CPU or is no longer ready.
const bool current_is_associated =
!current_queue_state.active || current_state->curr_cpu_ == current_cpu;
if (!current_thread->IsIdle() && current_is_associated &&
(timeslice_expired || current_thread != next_thread)) {
ktrace::Scope trace_update_ema = LOCAL_KTRACE_BEGIN_SCOPE(DETAILED, "update_expected_runtime");
// Adjust the runtime for the relative performance of the CPU to account for
// different performance levels in the estimate. The relative performance
// scale is in the range (0.0, 1.0], such that the adjusted runtime is
// always less than or equal to the monotonic runtime.
const SchedDuration adjusted_total_runtime_ns = ScaleDown(total_runtime_ns);
current_state->banked_runtime_ns_ += adjusted_total_runtime_ns;
if (timeslice_expired || !current_queue_state.active) {
const SchedDuration delta_ns =
PeakDecayDelta(current_state->expected_runtime_ns_, current_state->banked_runtime_ns_,
kExpectedRuntimeAlpha, kExpectedRuntimeBeta);
current_state->expected_runtime_ns_ += delta_ns;
current_state->banked_runtime_ns_ = SchedDuration{0};
// Adjust the aggregate value by the same amount. The adjustment is only
// necessary when the thread is still active on this CPU.
if (current_queue_state.active) {
UpdateTotalExpectedRuntime(delta_ns);
}
}
}
// Update the current performance scale only after any uses in the reschedule
// path above to ensure the scale is applied consistently over the interval
// between reschedules (i.e. not earlier than the requested update).
//
// Updating the performance scale also results in updating the target
// preemption time below when the current thread is deadline scheduled.
//
// TODO(eieio): Apply a minimum value threshold to the userspace value.
// TODO(eieio): Shed load when total utilization is above kCpuUtilizationLimit.
const bool performance_scale_updated = performance_scale_ != pending_user_performance_scale_;
if (performance_scale_updated) {
performance_scale_ = pending_user_performance_scale_;
performance_scale_reciprocal_ = 1 / performance_scale_;
}
if (next_thread->IsIdle()) {
mp_set_cpu_idle(current_cpu);
} else {
mp_set_cpu_busy(current_cpu);
}
if (current_thread->IsIdle()) {
percpu::Get(current_cpu).stats.idle_time += actual_runtime_ns;
}
if (next_thread->IsIdle()) {
ktrace::Scope trace_stop_preemption = LOCAL_KTRACE_BEGIN_SCOPE(DETAILED, "idle");
next_state->last_started_running_ = now;
// If there are no tasks to run in the future or there is idle/power work to
// perform, disable the preemption timer. Otherwise, set the preemption
// time to the earliest eligible time.
target_preemption_time_ns_ = percpu::Get(this_cpu_).idle_power_thread.pending_power_work()
? SchedTime(ZX_TIME_INFINITE)
: GetNextEligibleTime();
PreemptReset(current_cpu, now.raw_value(), target_preemption_time_ns_.raw_value());
} else if (timeslice_expired || thread_changed) {
ktrace::Scope trace_start_preemption = LOCAL_KTRACE_BEGIN_SCOPE(DETAILED, "next_slice");
// Re-compute the time slice and deadline for the new thread based on the
// latest state.
target_preemption_time_ns_ = NextThreadTimeslice(next_thread, now);
// Update the thread's runtime stats to record the amount of time that it spent in the run
// queue.
next_thread->UpdateRuntimeStats(next_thread->state());
next_state->last_started_running_ = now;
start_of_current_time_slice_ns_ = now;
scheduled_weight_total_ = weight_total_;
// Adjust the preemption time to account for a deadline thread becoming
// eligible before the current time slice expires.
const SchedTime preemption_time_ns =
IsFairThread(next_thread)
? ClampToDeadline(target_preemption_time_ns_)
: ClampToEarlierDeadline(target_preemption_time_ns_, next_state->finish_time_);
DEBUG_ASSERT(preemption_time_ns <= target_preemption_time_ns_);
PreemptReset(current_cpu, now.raw_value(), preemption_time_ns.raw_value());
trace_start_preemption =
KTRACE_END_SCOPE(("preemption_time", Round<uint64_t>(preemption_time_ns)),
("target preemption time", Round<uint64_t>(target_preemption_time_ns_)));
// Emit a flow end event to match the flow begin event emitted when the
// thread was enqueued. Emitting in this scope ensures that thread just
// came from the run queue (and is not the idle thread).
LOCAL_KTRACE_FLOW_END(FLOW, "sched_latency", next_state->flow_id(),
("tid", next_thread->tid()));
} else {
ktrace::Scope trace_continue = LOCAL_KTRACE_BEGIN_SCOPE(DETAILED, "continue");
DEBUG_ASSERT(current_thread == next_thread);
// Update the target preemption time for consistency with the updated CPU
// performance scale.
if (performance_scale_updated && IsDeadlineThread(next_thread)) {
target_preemption_time_ns_ = NextThreadTimeslice(next_thread, now);
}
// The current thread should continue to run. A throttled deadline thread
// might become eligible before the current time slice expires. Figure out
// whether to set the preemption time earlier to switch to the newly
// eligible thread.
//
// The preemption time should be set earlier when either:
// * Current is a fair thread and a deadline thread will become eligible
// before its time slice expires.
// * Current is a deadline thread and a deadline thread with an earlier
// deadline will become eligible before its time slice expires.
//
// Note that the target preemption time remains set to the ideal
// preemption time for the current task, even if the preemption timer is set
// earlier. If a task that becomes eligible is stolen before the early
// preemption is handled, this logic will reset to the original target
// preemption time.
const SchedTime preemption_time_ns =
IsFairThread(next_thread)
? ClampToDeadline(target_preemption_time_ns_)
: ClampToEarlierDeadline(target_preemption_time_ns_, next_state->finish_time_);
DEBUG_ASSERT(preemption_time_ns <= target_preemption_time_ns_);
PreemptReset(current_cpu, now.raw_value(), preemption_time_ns.raw_value());
trace_continue =
KTRACE_END_SCOPE(("preemption_time", Round<uint64_t>(preemption_time_ns)),
("target preemption time", Round<uint64_t>(target_preemption_time_ns_)));
}
// Assert that there is no path beside running the idle thread can leave the
// preemption timer unarmed. However, the preemption timer may or may not be
// armed when running the idle thread.
// TODO(eieio): In the future, the preemption timer may be canceled when there
// is only one task available to run. Revisit this assertion at that time.
DEBUG_ASSERT(next_thread->IsIdle() || percpu::Get(current_cpu).timer_queue.PreemptArmed());
// Almost done, we need to handle the actual context switch (if any).
if (thread_changed) {
LOCAL_KTRACE(
DETAILED, "switch_threads",
("total threads", runnable_fair_task_count_ + runnable_deadline_task_count_),
("total weight", weight_total_.raw_value()),
("current thread time slice",
Round<uint64_t>(current_thread->scheduler_state().time_slice_ns_)),
("next thread time slice", Round<uint64_t>(next_thread->scheduler_state().time_slice_ns_)));
// Our current thread should have been flagged as either Rescheduling or
// Migrating at this point.
bool finish_migrate_current{false};
if (current_thread->scheduler_queue_state().transient_state == TransientState::Rescheduling) {
// If the current thread was simply Rescheduling, we simply need to clear
// that flag. Other CPUs are free to attempt to steal this thread now;
// they will not succeed until after the actual context switch is done and
// we drop the current thread's lock (even through we are just about to
// drop the current scheduler's queue lock).
current_thread->scheduler_queue_state().transient_state = TransientState::None;
} else if (current_thread->scheduler_queue_state().transient_state ==
TransientState::Migrating) {
// Looks like the current thread needs to finish its migration. Do so
// after we have dropped this scheduler's queue lock.
finish_migrate_current = true;
} else {
DEBUG_ASSERT(current_thread->scheduler_queue_state().transient_state == TransientState::None);
DEBUG_ASSERT((current_thread->state() != THREAD_RUNNING) &&
(current_thread->state() != THREAD_READY));
}
TraceThreadQueueEvent("tqe_astart"_intern, next_thread);
// Release queue lock before context switching.
queue_guard.Release();
// Now finish any pending migration of the current thread. Again, this will
// make it available to be selected to run on another CPU, or be stolen by
// another CPU. This is OK, as with clearing the rescheduling flag (above),
// the other CPUs operation cannot complete until after the context switch
// is finished and we have dropped the current thread's lock.
if (finish_migrate_current) {
// Call the Before stage of the thread's migration function as it leaves
// this CPU.
current_thread->CallMigrateFnLocked(Thread::MigrateStage::Before);
const cpu_num_t target_cpu = FindTargetCpu(current_thread, FindTargetCpuReason::Migrating);
DEBUG_ASSERT((target_cpu != INVALID_CPU) && (target_cpu != this_cpu_));
Scheduler* const target = Get(target_cpu);
{
Guard<MonitoredSpinLock, NoIrqSave> target_queue_guard{&target->queue_lock_, SOURCE_TAG};
target->FinishTransition(now, current_thread);
}
// Fire off an IPI to the CPU we just moved the thread to.
mp_reschedule(cpu_num_to_mask(target_cpu), 0);
}
TraceContextSwitch(current_thread, next_thread, current_cpu);
// We invoke the context switch functions before context switching, so that
// they have a chance to correctly perform the actions required. Doing so
// after context switching may lead to an invalid CPU state.
current_thread->CallContextSwitchFnLocked();
next_thread->CallContextSwitchFnLocked();
// Notes about Thread aspace rules:
//
// Typically, it is only safe for the current thread to access its aspace
// member directly, as only a running thread can change its own aspace, and
// if a thread is running, then its process must also be alive and therefore
// its aspace must also be alive.
//
// Context switching is a bit of an edge case. The current thread is
// becoming the next thread. Both aspaces must still be alive (even if the
// current thread is in the process of becoming rescheduled for the very
// last time as it exits), and neither one can change its own aspace right
// now (they are not really running).
//
// Because of this, it should be OK for us to directly access the aspaces
// during the context switch, without needing to either check the thread's
// state, or add any references to the VmAstate object.
[&]() TA_NO_THREAD_SAFETY_ANALYSIS {
if (current_thread->aspace() != next_thread->aspace()) {
vmm_context_switch(current_thread->aspace(), next_thread->aspace());
}
}();
CPU_STATS_INC(context_switches);
// Prevent the scheduler durations from spanning the context switch.
// Some context switches do not resume within this method on the other
// thread, which results in unterminated durations. All of the callers
// with durations tail-call this method, so terminating the duration
// here should not cause significant inaccuracy of the outer duration.
trace.End();
if (end_outer_trace) {
end_outer_trace();
}
if constexpr (SCHEDULER_QUEUE_TRACING_ENABLED) {
const uint64_t arg0 = 0;
const uint64_t arg1 = (ktl::clamp<uint64_t>(this_cpu_, 0, 0xF) << 16);
ktrace_probe(TraceAlways, TraceContext::Cpu, "tqe_afinish"_intern, arg0, arg1);
}
// Remember the pointer to the current thread's active ChainLockTransaction.
// When the current thread is (eventually) scheduled again (on this CPU, or
// a different CPU) will will eventually need to restore the thread's CLT
// pointer (which exists somewhere up the thread's stack) to the per-cpu
// data structure so it can properly unwind.
// Time for the context switch. Before actually performing the switch,
// record the current thread as the "previous thread" for the next thread
// becoming scheduled. The next thread needs to know which thread preceded
// it in order to drop its lock before unwinding, and our current stack is
// not going to be available to it.
DEBUG_ASSERT(next_thread->scheduler_state().previous_thread_ == nullptr);
next_thread->scheduler_state().previous_thread_ = current_thread;
arch_context_switch(current_thread, next_thread);
// We made it to the other side of the switch. We have switched stacks, so
// the local variable meanings have become redefined.
//
// ++ The value of current_thread after the context switch is the value of
// next_thread before the switch.
// ++ The value of next_thread after the context switch is the thread that
// the new current_thread context-switched to the last time it ran
// through here. We don't even know if this thread exists now, so its
// value is junk.
// ++ The value of current_thread before the switch has been stashed in
// the new current thread's scheduler_state's previous thread member.
//
// We need to drop the old current thread's lock, while continuing to hold
// the new current thread's lock as we unwind.
// Scheduler::LockHandoffInternal will take care of dropping the previous
// current_thread's lock, but at this point, the static analyzer is going to
// be extremely confused because it does not (and cannot) know about the
// stack-swap which just happened. It thinks that we are still holding
// next_thread's lock (and we are; it is now current thread) and that we
// need to drop it (it is wrong about this, we need to drop the previous
// current thread's lock). So, just lie to it. Tell it that we have dropped
// next_thread's lock using a no-op assert (we don't actually want to
// examine the state of the lock in any way, since next thread is no longer
// valid).
Scheduler::LockHandoffInternal(orig_chainlock_context, current_thread);
next_thread->get_lock().MarkReleased();
} else {
// No context switch was needed. Our current thread should have been
// flagged as being in the process of rescheduling. We can clear this flag
// now.
DEBUG_ASSERT(current_thread->scheduler_queue_state().transient_state ==
TransientState::Rescheduling);
current_thread->scheduler_queue_state().transient_state = TransientState::None;
// Restore our reschedule context and drop our queue guard before unwinding.
SchedulerUtils::RestoreRescheduleContext(orig_chainlock_context);
queue_guard.Release();
}
}
void Scheduler::TrampolineLockHandoff() { SchedulerUtils::TrampolineLockHandoff(); }
void Scheduler::LockHandoffInternal(const kconcurrent::RescheduleContext& ctx,
Thread* const current_thread) {
DEBUG_ASSERT(arch_ints_disabled());
Thread* const previous_thread = current_thread->scheduler_state().previous_thread_;
DEBUG_ASSERT(previous_thread != nullptr);
current_thread->scheduler_state().previous_thread_ = nullptr;
previous_thread->get_lock().AssertAcquired();
SchedulerUtils::PostContextSwitchLockHandoff(ctx, previous_thread);
}
void Scheduler::UpdatePeriod() {
ktrace::Scope trace = LOCAL_KTRACE_BEGIN_SCOPE(DETAILED, "update_period");
DEBUG_ASSERT(runnable_fair_task_count_ >= 0);
DEBUG_ASSERT(minimum_granularity_ns_ > 0);
DEBUG_ASSERT(target_latency_grans_ > 0);
const int64_t num_tasks = runnable_fair_task_count_;
const int64_t normal_tasks = Round<int64_t>(target_latency_grans_);
// The scheduling period stretches when there are too many tasks to fit
// within the target latency.
scheduling_period_grans_ = SchedDuration{num_tasks > normal_tasks ? num_tasks : normal_tasks};
trace = KTRACE_END_SCOPE(("task count", num_tasks));
}
SchedDuration Scheduler::CalculateTimeslice(const Thread* thread) {
ktrace::Scope trace = LOCAL_KTRACE_BEGIN_SCOPE(DETAILED, "calculate_timeslice");
const SchedulerState& state = thread->scheduler_state();
const EffectiveProfile& ep = state.effective_profile_;
// Calculate the relative portion of the scheduling period.
const SchedWeight proportional_time_slice_grans =
scheduling_period_grans_ * ep.fair.weight / weight_total_;
// Ensure that the time slice is at least the minimum granularity.
const int64_t time_slice_grans = Round<int64_t>(proportional_time_slice_grans);
const int64_t minimum_time_slice_grans = time_slice_grans > 0 ? time_slice_grans : 1;
// Calculate the time slice in nanoseconds.
const SchedDuration time_slice_ns = minimum_time_slice_grans * minimum_granularity_ns_;
trace = KTRACE_END_SCOPE(
("weight", ep.fair.weight.raw_value()),
("total weight", KTRACE_ANNOTATED_VALUE(AssertHeld(queue_lock_), weight_total_.raw_value())));
return time_slice_ns;
}
SchedTime Scheduler::ClampToDeadline(SchedTime completion_time) {
return ktl::min(completion_time, GetNextEligibleTime());
}
SchedTime Scheduler::ClampToEarlierDeadline(SchedTime completion_time, SchedTime finish_time) {
const Thread* const thread = FindEarlierDeadlineThread(completion_time, finish_time);
if (thread != nullptr) {
AssertInScheduler(*thread);
return ktl::min(completion_time, thread->scheduler_state().start_time_);
}
return completion_time;
}
SchedTime Scheduler::NextThreadTimeslice(Thread* thread, SchedTime now) {
ktrace::Scope trace = LOCAL_KTRACE_BEGIN_SCOPE(DETAILED, "next_timeslice");
SchedulerState* const state = &thread->scheduler_state();
EffectiveProfile& ep = state->effective_profile_;
SchedTime target_preemption_time_ns;
if (IsFairThread(thread)) {
// Calculate the next time slice and the deadline when the time slice is
// completed.
const SchedDuration time_slice_ns = CalculateTimeslice(thread);
const SchedDuration remaining_time_slice_ns =
time_slice_ns * ep.fair.normalized_timeslice_remainder;
DEBUG_ASSERT(time_slice_ns > 0);
DEBUG_ASSERT(remaining_time_slice_ns > 0);
ep.fair.initial_time_slice_ns = time_slice_ns;
state->time_slice_ns_ = remaining_time_slice_ns;
target_preemption_time_ns = now + remaining_time_slice_ns;
DEBUG_ASSERT_MSG(state->time_slice_ns_ > 0 && target_preemption_time_ns > now,
"time_slice_ns=%" PRId64 " now=%" PRId64 " target_preemption_time_ns=%" PRId64,
state->time_slice_ns_.raw_value(), now.raw_value(),
target_preemption_time_ns.raw_value());
trace =
KTRACE_END_SCOPE(("time slice", Round<uint64_t>(state->time_slice_ns_)),
("target preemption time", Round<uint64_t>(target_preemption_time_ns)));
} else {
// Calculate the deadline when the remaining time slice is completed. The
// time slice is maintained by the deadline queuing logic, no need to update
// it here. The target preemption time is based on the time slice scaled by
// the performance of the CPU and clamped to the deadline. This increases
// capacity on slower processors, however, bandwidth isolation is preserved
// because CPU selection attempts to keep scaled total capacity below one.
const SchedDuration scaled_time_slice_ns = ScaleUp(state->time_slice_ns_);
target_preemption_time_ns =
ktl::min<SchedTime>(now + scaled_time_slice_ns, state->finish_time_);
trace =
KTRACE_END_SCOPE(("scaled time slice", Round<uint64_t>(scaled_time_slice_ns)),
("target preemption time", Round<uint64_t>(target_preemption_time_ns)));
}
return target_preemption_time_ns;
}
void Scheduler::QueueThread(Thread* thread, Placement placement, SchedTime now,
SchedDuration total_runtime_ns) {
ktrace::Scope trace = LOCAL_KTRACE_BEGIN_SCOPE(DETAILED, "queue_thread");
DEBUG_ASSERT(thread->state() == THREAD_READY);
DEBUG_ASSERT(!thread->IsIdle());
DEBUG_ASSERT(placement != Placement::Association);
SchedulerState* const state = &thread->scheduler_state();
EffectiveProfile& ep = state->effective_profile_;
if (IsFairThread(thread)) {
// Account for the consumed fair time slice. The consumed time is zero when
// the thread is unblocking, migrating, or adjusting queue position. The
// remaining time slice may become negative due to scheduler overhead.
state->time_slice_ns_ -= total_runtime_ns;
// Compute the ratio of remaining time slice to ideal time slice. This may
// be less than 1.0 due to time slice consumed or due to previous preemption
// by a deadline task or both.
//
// Note: it is important to ignore the initial_time_slice_ns and
// normalized_timeslice_remainder members of the effective profile during an
// insertion operation. These member variables lost their meaning when the
// thread blocked, if they were even defined at the time of blocking at all.
const SchedRemainder normalized_timeslice_remainder =
(placement == Placement::Insertion)
? SchedRemainder{0}
: state->time_slice_ns_ / ktl::max(ep.fair.initial_time_slice_ns, SchedDuration{1});
DEBUG_ASSERT_MSG(normalized_timeslice_remainder <= SchedRemainder{1},
"time_slice_ns=%" PRId64 " initial_time_slice_ns=%" PRId64
" remainder=%" PRId64 "\n",
state->time_slice_ns_.raw_value(), ep.fair.initial_time_slice_ns.raw_value(),
normalized_timeslice_remainder.raw_value());
// If we are unblocking (placement is Insertion), or we have exhausted our
// timeslice, then grant the thread a new timeslice by establishing a new
// start time, and setting the normalized remainder to 1.
//
// Note that we use our recomputed normalized timeslice remainder to
// determine whether or not our timeslice has expired (in the non
// Placement::Insertion case). This is important. If we chose to use
// time_slice_ns_ to make this decision, and ended up recomputing NTSR only
// after we had determined that time_slice_ns_ was positive, it is possible
// to end up with a positive time_slice_ns_, but a NTSR which is zero (which
// will trigger asserts when the thread is next scheduled).
if (normalized_timeslice_remainder <= 0) {
state->start_time_ = ktl::max(state->finish_time_, virtual_time_);
ep.fair.normalized_timeslice_remainder = SchedRemainder{1};
} else if (placement == Placement::Preemption) {
DEBUG_ASSERT(state->time_slice_ns_ > 0);
ep.fair.normalized_timeslice_remainder = normalized_timeslice_remainder;
}
const SchedDuration scheduling_period_ns = scheduling_period_grans_ * minimum_granularity_ns_;
const SchedWeight rate = kReciprocalMinWeight * ep.fair.weight;
const SchedDuration delta_norm = scheduling_period_ns / rate;
state->finish_time_ = state->start_time_ + delta_norm;
DEBUG_ASSERT_MSG(state->start_time_ < state->finish_time_,
"start=%" PRId64 " finish=%" PRId64 " delta_norm=%" PRId64 "\n",
state->start_time_.raw_value(), state->finish_time_.raw_value(),
delta_norm.raw_value());
} else {
// Both a new insertion into the run queue or a re-insertion due to
// preemption can happen after the time slice and/or deadline expires.
if (placement == Placement::Insertion || placement == Placement::Preemption) {
ktrace::Scope deadline_trace = LOCAL_KTRACE_BEGIN_SCOPE(
DETAILED, "deadline_op",
("placement", placement == Placement::Insertion ? "insertion" : "preemption"));
// Determine how much time is left before the deadline. This might be less
// than the remaining time slice or negative if the thread blocked.
const SchedDuration time_until_deadline_ns = state->finish_time_ - now;
if (time_until_deadline_ns <= 0 || state->time_slice_ns_ <= 0) {
const SchedTime period_finish_ns = state->start_time_ + ep.deadline.deadline_ns;
state->start_time_ = now >= period_finish_ns ? now : period_finish_ns;
state->finish_time_ = state->start_time_ + ep.deadline.deadline_ns;
state->time_slice_ns_ = ep.deadline.capacity_ns;
}
deadline_trace =
KTRACE_END_SCOPE(("time until deadline", Round<uint64_t>(time_until_deadline_ns)),
("time slice", Round<uint64_t>(state->time_slice_ns_)));
}
DEBUG_ASSERT_MSG(state->start_time_ < state->finish_time_,
"start=%" PRId64 " finish=%" PRId64 " capacity=%" PRId64 "\n",
state->start_time_.raw_value(), state->finish_time_.raw_value(),
state->time_slice_ns_.raw_value());
}
// Only update the generation, enqueue time, and emit a flow event if this
// is an insertion, preemption, or migration. In contrast, an adjustment only
// changes the queue position in the same queue due to a parameter change and
// should not perform these actions.
if (placement != Placement::Adjustment) {
if (placement == Placement::Migration) {
// Connect the flow into the previous queue to the new queue.
LOCAL_KTRACE_FLOW_STEP(FLOW, "sched_latency", state->flow_id(), ("tid", thread->tid()));
} else {
// Reuse this member to track the time the thread enters the run queue. It
// is not read outside of the scheduler unless the thread state is
// THREAD_RUNNING.
state->last_started_running_ = now;
state->flow_id_ = NextFlowId();
LOCAL_KTRACE_FLOW_BEGIN(FLOW, "sched_latency", state->flow_id(), ("tid", thread->tid()));
}
// The generation count must always be updated when changing between CPUs,
// as each CPU has its own generation count.
state->generation_ = ++generation_count_;
}
// Insert the thread into the appropriate run queue after the generation count
// is potentially updated above.
if (IsFairThread(thread)) {
fair_run_queue_.insert(thread);
} else {
deadline_run_queue_.insert(thread);
}
if (placement != Placement::Adjustment) {
TraceThreadQueueEvent("tqe_enque"_intern, thread);
}
trace = KTRACE_END_SCOPE(("start time", Round<uint64_t>(state->start_time_)),
("finish time", Round<uint64_t>(state->finish_time_)));
}
void Scheduler::Insert(SchedTime now, Thread* thread, Placement placement) {
ktrace::Scope trace = LOCAL_KTRACE_BEGIN_SCOPE(DETAILED, "insert");
DEBUG_ASSERT(thread->state() == THREAD_READY);
DEBUG_ASSERT(!thread->IsIdle());
SchedulerQueueState& queue_state = thread->scheduler_queue_state();
SchedulerState& state = thread->scheduler_state();
const EffectiveProfile& ep = state.effective_profile_;
// Ensure insertion happens only once, even if Unblock is called multiple times.
if (queue_state.OnInsert()) {
// Insertion can happen from a different CPU. Set the thread's current
// CPU to the one this scheduler instance services.
state.curr_cpu_ = this_cpu();
UpdateTotalExpectedRuntime(state.expected_runtime_ns_);
if (IsFairThread(thread)) {
runnable_fair_task_count_++;
DEBUG_ASSERT(runnable_fair_task_count_ > 0);
UpdateTimeline(now);
UpdatePeriod();
weight_total_ += ep.fair.weight;
DEBUG_ASSERT(weight_total_ > 0);
} else {
UpdateTotalDeadlineUtilization(ep.deadline.utilization);
runnable_deadline_task_count_++;
DEBUG_ASSERT(runnable_deadline_task_count_ != 0);
}
TraceTotalRunnableThreads();
if (placement != Placement::Association) {
QueueThread(thread, placement, now);
} else {
// Connect the flow into the previous queue to the new queue.
LOCAL_KTRACE_FLOW_STEP(FLOW, "sched_latency", state.flow_id(), ("tid", thread->tid()));
}
}
}
void Scheduler::RemoveForTransition(Thread* thread,
SchedulerQueueState::TransientState target_state) {
using TransientState = SchedulerQueueState::TransientState;
ktrace::Scope trace = LOCAL_KTRACE_BEGIN_SCOPE(DETAILED, "remove");
// RemoveForTransition is used when channels are being actively migrated, and
// when they are being stolen, but also as just common shared code for when
// Scheduler::Remove is called. In this case, the |target_state| will just be
// None.
DEBUG_ASSERT_MSG((target_state == TransientState::None) ||
(target_state == TransientState::Migrating) ||
(target_state == TransientState::Stolen),
"Bad target transient state! (%u)\n", static_cast<uint32_t>(target_state));
DEBUG_ASSERT(!thread->IsIdle());
SchedulerQueueState& queue_state = thread->scheduler_queue_state();
const SchedulerState& state = const_cast<const Thread*>(thread)->scheduler_state();
const EffectiveProfile& ep = state.effective_profile_;
DEBUG_ASSERT_MSG(queue_state.transient_state == TransientState::None,
"Attempting to transition a thread which is already transitioning! (%u)\n",
static_cast<uint32_t>(queue_state.transient_state));
DEBUG_ASSERT(!queue_state.InQueue());
queue_state.transient_state = target_state;
// Ensure that removal happens only once, even if Block() is called multiple times.
if (queue_state.OnRemove()) {
UpdateTotalExpectedRuntime(-state.expected_runtime_ns_);
if (IsFairThread(thread)) {
DEBUG_ASSERT(runnable_fair_task_count_ > 0);
runnable_fair_task_count_--;
UpdatePeriod();
weight_total_ -= ep.fair.weight;
DEBUG_ASSERT(weight_total_ >= 0);
} else {
UpdateTotalDeadlineUtilization(-ep.deadline.utilization);
DEBUG_ASSERT(runnable_deadline_task_count_ > 0);
runnable_deadline_task_count_--;
}
TraceTotalRunnableThreads();
}
}
void Scheduler::FinishTransition(SchedTime now, Thread* thread) {
// We had better either be in the process of being stolen or migrated.
using TransientState = SchedulerQueueState::TransientState;
SchedulerQueueState& sqs = thread->scheduler_queue_state();
// Note, we don't have to reset curr_cpu_ in the thread's scheduler state.
// Insert (called below) will do that for us.
if (IsFairThread(thread)) {
thread->scheduler_state().start_time_ = SchedNs(0);
thread->scheduler_state().finish_time_ = SchedNs(0);
}
if (sqs.transient_state == TransientState::Migrating) {
// Active migration uses a Placement of Insertion.
Insert(now, thread, Placement::Insertion);
// Our transition is complete, update our transient state.
sqs.transient_state = TransientState::None;
} else {
// If the transition wasn't an active migration, it must be that the thread
// was stolen.
DEBUG_ASSERT_MSG(sqs.transient_state == TransientState::Stolen,
"Bad transient state when finishing transition! (%u)\n",
static_cast<uint32_t>(sqs.transient_state));
// Add ourselves to our new scheduler's run queue. Stolen threads use a
// placement of Association.
Insert(now, thread, Placement::Association);
// Our transition is not quite done yet. We were just stolen, which means
// that we are in the process of becoming the active thread on this
// scheduler (but have not done so yet). Change our transient state to
// "Rescheduling"
sqs.transient_state = TransientState::Rescheduling;
}
}
void Scheduler::ValidateInvariantsUnconditional() const {
using ProfileDirtyFlag = SchedulerState::ProfileDirtyFlag;
auto ObserveFairThread = [&](const Thread& t) TA_REQ(this->queue_lock_,
chainlock_transaction_token) {
// We are holding the scheduler's queue lock, and observing thread which are
// either active, or members of our queues. Tell the static analyzer that
// it is OK for us to access a thread's effective profile in a read-only
// fashion, even though we don't explicitly hold the thread's lock.
[&t]() {
MarkHasOwnedThreadAccess(t);
const auto& ss = t.scheduler_state();
const auto& ep = ss.effective_profile();
ASSERT_MSG(ep.IsFair(), "Fair thread %" PRIu64 " has non-fair effective profile", t.tid());
}();
// If we have dirty tracking enabled for effective profiles (only enabled in
// builds with extra scheduler state validation enabled) we can try to make
// some extra checks.
//
// 1) If we know that the current base profile is clean, we can assert that
// the base profile is fair.
// 2) If we know that the current IPVs are clean, we can assert that the IPV
// utilization is zero.
//
// In order to safely observe these things, however, we need to be holding
// the thread's lock. Threads can change their base profile or IPVs (and
// the flags which indicate if they are dirty or nor) while only holding
// their lock. They do not need to be holding the scheduler's queue lock if
// the thread happens to be running or ready.
//
// So, _try_ to get the thread's lock if we don't already hold it, and if we
// succeed, go ahead a perform our checks. If we fail to get the lock for
// any reason, just skip the checks this time.
if constexpr (EffectiveProfile::kDirtyTrackingEnabled) {
auto ExtraChecks = [&t]() TA_REQ(t.get_lock()) -> void {
const auto& ss = t.scheduler_state();
const auto& ep = ss.effective_profile();
const auto& bp = ss.base_profile_;
const auto& ipv = ss.inherited_profile_values_;
if (!(ep.dirty_flags() & ProfileDirtyFlag::BaseDirty)) {
ASSERT_MSG(bp.IsFair(), "Fair thread %" PRIu64 " has clean, but non-fair, base profile",
t.tid());
}
if (!(ep.dirty_flags() & ProfileDirtyFlag::InheritedDirty)) {
ASSERT_MSG(ipv.uncapped_utilization == SchedUtilization{0},
"Fair thread %" PRIu64
" has clean IPV, but non-zero inherited utilization (%" PRId64 ")",
t.tid(), ipv.uncapped_utilization.raw_value());
}
};
if (t.get_lock().is_held()) {
t.get_lock().MarkHeld();
ExtraChecks();
} else if (t.get_lock().TryAcquire<ChainLock::FinalizedTransactionAllowed::Yes>()) {
ExtraChecks();
t.get_lock().Release();
}
}
};
auto ObserveDeadlineThread = [&](const Thread& t) TA_REQ(this->queue_lock_,
chainlock_transaction_token) {
// See above for the locking rules for accessing the pieces of effective profile.
[&t]() {
MarkHasOwnedThreadAccess(t);
const auto& ss = t.scheduler_state();
const auto& ep = ss.effective_profile();
ASSERT_MSG(ep.IsDeadline(), "Deadline thread %" PRIu64 " has non-deadline effective profile",
t.tid());
}();
if constexpr (EffectiveProfile::kDirtyTrackingEnabled) {
auto ExtraChecks = [&t]() TA_REQ(t.get_lock()) -> void {
const auto& ss = t.scheduler_state();
const auto& ep = ss.effective_profile();
const auto& bp = ss.base_profile_;
const auto& ipv = ss.inherited_profile_values_;
if (ep.dirty_flags() == ProfileDirtyFlag::Clean) {
ASSERT_MSG(
bp.IsDeadline() || (ipv.uncapped_utilization > SchedUtilization{0}),
"Deadline thread %" PRIu64
" has a clean effective profile, but neither a deadline base profile (%s), nor a "
"non-zero inherited utilization (%" PRId64 ")",
t.tid(), bp.IsFair() ? "Fair" : "Deadline", ipv.uncapped_utilization.raw_value());
}
};
if (t.get_lock().is_held()) {
t.get_lock().MarkHeld();
ExtraChecks();
} else if (t.get_lock().TryAcquire<ChainLock::FinalizedTransactionAllowed::Yes>()) {
ExtraChecks();
t.get_lock().Release();
}
}
};
for (const auto& t : fair_run_queue_) {
ObserveFairThread(t);
}
for (const auto& t : deadline_run_queue_) {
ObserveDeadlineThread(t);
}
ASSERT(active_thread_ != nullptr);
const Thread& active_thread = *active_thread_;
MarkHasOwnedThreadAccess(active_thread); // This should be safe, see above.
if (active_thread.scheduler_state().effective_profile().IsFair()) {
ObserveFairThread(active_thread);
} else {
ASSERT(active_thread.scheduler_state().effective_profile().IsDeadline());
ObserveDeadlineThread(active_thread);
}
}
void Scheduler::Block(Thread* const current_thread) {
ktrace::Scope trace = LOCAL_KTRACE_BEGIN_SCOPE(COMMON, "sched_block");
DEBUG_ASSERT(current_thread == Thread::Current::Get());
current_thread->canary().Assert();
DEBUG_ASSERT(current_thread->state() != THREAD_RUNNING);
const SchedTime now = CurrentTime();
Scheduler::Get()->RescheduleCommon(current_thread, now, trace.Completer());
}
void Scheduler::Unblock(Thread* thread) {
ktrace::Scope trace = LOCAL_KTRACE_BEGIN_SCOPE(COMMON, "sched_unblock");
ChainLockTransaction::ActiveRef().AssertFinalized();
thread->canary().Assert();
const SchedTime now = CurrentTime();
const cpu_num_t target_cpu = FindTargetCpu(thread, FindTargetCpuReason::Unblocking);
Scheduler* const target = Get(target_cpu);
TraceWakeup(thread, target_cpu);
{
Guard<MonitoredSpinLock, NoIrqSave> queue_guard_{&target->queue_lock_, SOURCE_TAG};
// TODO(johngro): What if the CPU is now offline? Do we need to retry?
// This thread is now owned by this scheduler.
thread->scheduler_state().curr_cpu_ = target_cpu;
thread->set_ready();
target->AssertInScheduler(*thread);
target->Insert(now, thread);
thread->UpdateRuntimeStats(thread->state());
}
trace.End();
thread->get_lock().Release();
RescheduleMask(cpu_num_to_mask(target_cpu));
}
void Scheduler::Unblock(Thread::UnblockList list) {
ktrace::Scope trace = LOCAL_KTRACE_BEGIN_SCOPE(COMMON, "sched_unblock_list");
ChainLockTransaction::ActiveRef().AssertFinalized();
const SchedTime now = CurrentTime();
cpu_mask_t cpus_to_reschedule_mask = 0;
Thread* thread;
while ((thread = list.pop_back()) != nullptr) {
thread->canary().Assert();
DEBUG_ASSERT(!thread->IsIdle());
thread->get_lock().AssertAcquired();
const cpu_num_t target_cpu = FindTargetCpu(thread, FindTargetCpuReason::Unblocking);
Scheduler* const target = Get(target_cpu);
TraceWakeup(thread, target_cpu);
thread->UpdateRuntimeStats(thread->state());
{
Guard<MonitoredSpinLock, NoIrqSave> queue_guard_{&target->queue_lock_, SOURCE_TAG};
// TODO(johngro): What if the CPU is now offline? Do we need to retry?
// This thread is now owned by this scheduler.
thread->scheduler_state().curr_cpu_ = target_cpu;
thread->set_ready();
target->AssertInScheduler(*thread);
target->Insert(now, thread);
}
cpus_to_reschedule_mask |= cpu_num_to_mask(target_cpu);
thread->get_lock().Release();
}
trace.End();
RescheduleMask(cpus_to_reschedule_mask);
}
void Scheduler::UnblockIdle(Thread* thread) {
SchedulerState* const state = &thread->scheduler_state();
DEBUG_ASSERT(thread->IsIdle());
DEBUG_ASSERT(ktl::popcount(state->hard_affinity_) == 1);
{
Guard<MonitoredSpinLock, NoIrqSave> queue_guard{&Get()->queue_lock_, SOURCE_TAG};
thread->set_ready();
state->curr_cpu_ = lowest_cpu_set(state->hard_affinity_);
}
}
void Scheduler::Yield(Thread* const current_thread) {
ktrace::Scope trace = LOCAL_KTRACE_BEGIN_SCOPE(COMMON, "sched_yield");
DEBUG_ASSERT(current_thread == Thread::Current::Get());
DEBUG_ASSERT(!current_thread->IsIdle());
if (IsFairThread(current_thread)) {
Scheduler& current_scheduler = *Get();
const SchedTime now = CurrentTime();
{
// TODO(eieio,johngro): What is this protecting?
Guard<MonitoredSpinLock, NoIrqSave> queue_guard{&current_scheduler.queue_lock_, SOURCE_TAG};
SchedulerState& current_state = current_thread->scheduler_state();
EffectiveProfile& ep = current_thread->scheduler_state().effective_profile_;
// Update the virtual timeline in preparation for snapping the thread's
// virtual finish time to the current virtual time.
current_scheduler.UpdateTimeline(now);
// Set the time slice to expire now.
current_state.time_slice_ns_ = SchedDuration{0};
// The thread is re-evaluated with zero lag against other competing threads
// and may skip lower priority threads with similar arrival times.
current_state.finish_time_ = current_scheduler.virtual_time_;
ep.fair.initial_time_slice_ns = current_state.time_slice_ns_;
ep.fair.normalized_timeslice_remainder = SchedRemainder{1};
}
current_scheduler.RescheduleCommon(current_thread, now, trace.Completer());
}
}
void Scheduler::Preempt() {
Thread* current_thread = Thread::Current::Get();
SingletonChainLockGuardIrqSave thread_guard{current_thread->get_lock(),
CLT_TAG("Scheduler::Preempt")};
PreemptLocked(current_thread);
}
void Scheduler::PreemptLocked(Thread* current_thread) {
ktrace::Scope trace = LOCAL_KTRACE_BEGIN_SCOPE(COMMON, "sched_preempt");
SchedulerState& current_state = current_thread->scheduler_state();
const cpu_num_t current_cpu = arch_curr_cpu_num();
DEBUG_ASSERT(current_state.curr_cpu_ == current_cpu);
DEBUG_ASSERT(current_state.last_cpu_ == current_state.curr_cpu_);
DEBUG_ASSERT(current_thread->state() == THREAD_RUNNING);
const SchedTime now = CurrentTime();
Get()->RescheduleCommon(current_thread, now, trace.Completer());
}
void Scheduler::Reschedule(Thread* const current_thread) {
ktrace::Scope trace = LOCAL_KTRACE_BEGIN_SCOPE(COMMON, "sched_reschedule");
DEBUG_ASSERT(current_thread == Thread::Current::Get());
SchedulerState* const current_state = &current_thread->scheduler_state();
const cpu_num_t current_cpu = arch_curr_cpu_num();
const bool preempt_enabled = current_thread->preemption_state().EvaluateTimesliceExtension();
// Pend the preemption rather than rescheduling if preemption is disabled or
// if there is more than one spinlock held.
// TODO(https://fxbug.dev/42143537): Remove check when spinlocks imply preempt disable.
if (!preempt_enabled || arch_num_spinlocks_held() > 1 || arch_blocking_disallowed()) {
current_thread->preemption_state().preempts_pending_add(cpu_num_to_mask(current_cpu));
return;
}
DEBUG_ASSERT(current_state->curr_cpu_ == current_cpu);
DEBUG_ASSERT(current_state->last_cpu_ == current_state->curr_cpu_);
const SchedTime now = CurrentTime();
Get()->RescheduleCommon(current_thread, now, trace.Completer());
}
void Scheduler::RescheduleInternal(Thread* const current_thread) {
ktrace::Scope trace = LOCAL_KTRACE_BEGIN_SCOPE(COMMON, "sched_resched_internal");
Get()->RescheduleCommon(current_thread, CurrentTime(), trace.Completer());
}
void Scheduler::Migrate(Thread* thread) {
ktrace::Scope trace = LOCAL_KTRACE_BEGIN_SCOPE(COMMON, "sched_migrate");
SchedulerState* const state = &thread->scheduler_state();
const cpu_mask_t effective_cpu_mask = state->GetEffectiveCpuMask(mp_get_active_mask());
const cpu_mask_t thread_cpu_mask = cpu_num_to_mask(state->curr_cpu_);
const bool stale_curr_cpu = (thread_cpu_mask & effective_cpu_mask) == 0;
cpu_mask_t cpus_to_reschedule_mask = 0;
if (stale_curr_cpu) {
if (thread->state() == THREAD_RUNNING) {
// The CPU the thread is running on will take care of the actual migration.
cpus_to_reschedule_mask |= thread_cpu_mask;
} else if (thread->state() == THREAD_READY) {
bool removed_thread_for_migrate{false};
do { // Explicit scope top hold the lock for this thread's current scheduler.
Scheduler& thread_sched = *Get(state->curr_cpu_);
Guard<MonitoredSpinLock, NoIrqSave> queue_guard{&thread_sched.queue_lock_, SOURCE_TAG};
thread_sched.AssertInScheduler(*thread);
SchedulerQueueState& sqs = thread->scheduler_queue_state();
const cpu_num_t current_cpu = arch_curr_cpu_num();
// If the thread is currently in a transient state (rescheduling, being
// stolen, or migrating), don't do anything else. When the thread gets
// to wherever it is finally going, if that location turns out to be
// incompatible with its affinity masks, a new CPU should end up being
// selected instead of wherever it was going.
if (sqs.transient_state != SchedulerQueueState::TransientState::None) {
break;
}
// Try find a new CPU to run this thread on. Things are simple if the
// thread has no migration function, or if the thread has a migration
// function, but has already called the Before stage of migration. We
// can just remove the thread from this queue, and put it into a
// different one.
//
// If the thread does have a migration function, and does need the
// Before stage to be called, we can do that too, but only if we are
// currently running on the last CPU the thread ran on. Otherwise, the
// best we can do is to poke the thread's last CPU and have it
// reschedule. The next time this thread comes up in the queue, it will
// be actively migrated by the CPU it last ran on.
if (thread->has_migrate_fn() && !thread->migrate_pending() &&
(thread->scheduler_state().last_cpu_ != INVALID_CPU) &&
(thread->scheduler_state().last_cpu_ != current_cpu)) {
cpus_to_reschedule_mask |= thread_cpu_mask;
break;
}
// Looks like we can take care of migrating the thread right now. There
// is either no migration function to run, or we are able to run it on
// this CPU. Remove the thread, drop its old scheduler's lock, and
// finish the migration.
thread_sched.EraseFromQueue(thread);
thread_sched.Remove(thread, SchedulerQueueState::TransientState::Migrating);
removed_thread_for_migrate = true;
} while (false);
if (removed_thread_for_migrate) {
// Before obtaining any new locks, try to call the thread's migration
// function. CallMigrateFnLocked will do nothing if we have no
// migration function, or if we have already started the migration (eg;
// called the Before stage).
thread->CallMigrateFnLocked(Thread::MigrateStage::Before);
// Find a new CPU for this thread and add it to the queue.
const cpu_num_t target_cpu = FindTargetCpu(thread, FindTargetCpuReason::Migrating);
Scheduler* target = Get(target_cpu);
Guard<MonitoredSpinLock, NoIrqSave> queue_guard{&target->queue_lock_, SOURCE_TAG};
MarkHasOwnedThreadAccess(*thread);
target->FinishTransition(CurrentTime(), thread);
// Reschedule both CPUs to handle the run queue changes.
cpus_to_reschedule_mask |= cpu_num_to_mask(target_cpu) | thread_cpu_mask;
}
}
}
// The operation is finished. Drop the thread's lock before we call into
// reschedule so that we are certain to not be holding any threads' locks
// during the RescheduleMask operation (which might result in an immediate
// preempt).
trace.End();
thread->get_lock().Release();
RescheduleMask(cpus_to_reschedule_mask);
}
void Scheduler::MigrateUnpinnedThreads() {
ktrace::Scope trace = LOCAL_KTRACE_BEGIN_SCOPE(COMMON, "sched_migrate_unpinned");
const cpu_num_t current_cpu = arch_curr_cpu_num();
const cpu_mask_t current_cpu_mask = cpu_num_to_mask(current_cpu);
cpu_mask_t cpus_to_reschedule_mask = 0;
// Flag this scheduler as being in the process de-activating. If a thread
// with a migration function unblocks and needs to become READY while we are
// in the middle of MigrateUnpinnedThreads, we will allow it to join this
// CPU's ready-queue knowing that we are going to immediately call the
// thread's migration function (on this CPU, as required) before reassigning
// the thread to a different CPU.
//
// Note: Because of the nature of the global thread-lock, this race is not
// currently possible (Threads who are blocked cannot unblock while we hold
// the global thread lock, which we currently do). Once the thread lock has
// been removed, however, this race will become an important edge case to
// handle.
const SchedTime now = CurrentTime();
Scheduler* const current = Get(current_cpu);
current->cpu_deactivating_.store(true, ktl::memory_order_release);
// Flag this CPU as being inactive. This will prevent this CPU from being
// selected as a target for scheduling threads.
mp_set_curr_cpu_active(false);
// Call all migrate functions for threads last run on the current CPU who are
// not currently READY. The next time these threads unblock or wake up, the
// will be assigned to a different CPU, and we will have already called their
// migrate function for them. When we are finished with this pass, the only
// threads who have a migrate function still to be called should only exist in
// this scheduler's READY queue, because:
//
// 1) New threads or threads who have not recently run on this CPU cannot be
// assigned to this CPU (because we cleared our active flag)
// 2a) Threads who were not ready but needed to have their migration function
// called were found and had their migration function called during
// CallMigrateFnForCpu --- OR ---
// 2b) The non-ready thread with a migration function unblocked and joined the
// ready queue of this scheduler while it was being deactivated. We will
// call their migration functions next as we move all of the READY threads
// off of this scheduler and over to a different one.
//
Thread::CallMigrateFnForCpu(current_cpu);
// There should no longer be any non-ready threads who need to run a
// migration function on this CPU, and there should not be any new one until
// we set this CPU back to being active. We can clear the transitory
// "cpu_deactivating_" flag now.
DEBUG_ASSERT(current->cpu_deactivating_.load(ktl::memory_order_relaxed));
current->cpu_deactivating_.store(false, ktl::memory_order_release);
// Now move any READY threads who can be moved over to a temporary list,
// flagging them as in the process of currently migrating.
Thread::UnblockList migrating_threads;
{
Guard<MonitoredSpinLock, NoIrqSave> queue_guard{&current->queue_lock_, SOURCE_TAG};
auto MoveThreads = [&migrating_threads, current, current_cpu_mask](
RunQueue& run_queue,
const fxt::InternedString& trace_tag) TA_REQ(current->queue_lock_) {
DEBUG_ASSERT((&run_queue == &current->fair_run_queue_) ||
(&run_queue == &current->deadline_run_queue_));
for (RunQueue::iterator iter = run_queue.begin(); iter.IsValid();) {
Thread& consider = *(iter++);
// The no-op version of this assertion should be sufficient here. We
// are holding the scheduler's queue lock, and iterating over the
// scheduler's run queues. All of the threads in those queues are
// clearly owned by this scheduler right now.
MarkHasOwnedThreadAccess(consider);
const SchedulerState& ss = const_cast<const Thread&>(consider).scheduler_state();
// If the thread can run on any other CPU, take it off of this one.
// Flag it as migrating as we do, and add it to our list of migrating
// threads.
if (ss.hard_affinity_ != current_cpu_mask) {
current->TraceThreadQueueEvent(trace_tag, &consider);
run_queue.erase(consider);
current->RemoveForTransition(&consider, SchedulerQueueState::TransientState::Migrating);
migrating_threads.push_back(&consider);
}
}
};
MoveThreads(current->fair_run_queue_, "tqe_deque_migrate_unpinned_fair"_intern);
MoveThreads(current->deadline_run_queue_, "tqe_deque_migrate_unpinned_deadline"_intern);
}
// OK, now that we have dropped our queue lock, go over our list of migrating
// threads and finish the migration process. Don't make any assumptions about
// the fair/deadline nature of the threads as we migrate. Since we dropped
// the scheduler's queue lock, it is possible that these threads have changed
// effective priority since being remove from their old scheduler, but before
// arriving at a new one.
while (!migrating_threads.is_empty()) {
Thread* thread = migrating_threads.pop_front();
SingletonChainLockGuardIrqSave guard{thread->get_lock(),
CLT_TAG("Scheduler::MigrateUnpinnedThreads")};
// Call the Before stage of the thread's migration function as it leaves
// this CPU.
thread->CallMigrateFnLocked(Thread::MigrateStage::Before);
// Now, pick a CPU (other than this one) for the thread to go to, then put
// it there.
const cpu_num_t target_cpu = FindTargetCpu(thread, FindTargetCpuReason::Migrating);
Scheduler* const target = Get(target_cpu);
DEBUG_ASSERT(target != current);
Guard<MonitoredSpinLock, NoIrqSave> target_queue_guard{&target->queue_lock_, SOURCE_TAG};
// Finish the transition and add the thread to the new target queue. The
// After stage of the migration function will be called on the new CPU the
// next time the thread becomes scheduled.
MarkHasOwnedThreadAccess(*thread);
target->FinishTransition(now, thread);
// Arrange a reschedule on our target scheduler.
cpus_to_reschedule_mask |= cpu_num_to_mask(target_cpu);
}
trace.End();
RescheduleMask(cpus_to_reschedule_mask);
}
void Scheduler::TimerTick(SchedTime now) {
ktrace::Scope trace = LOCAL_KTRACE_BEGIN_SCOPE(COMMON, "sched_timer_tick");
Thread::Current::preemption_state().PreemptSetPending();
}
void Scheduler::InitializePerformanceScale(SchedPerformanceScale scale) {
// This happens early in boot, before the scheduler is actually running. We
// should be able to treat these assignments as if they were done in a
// constructor and skip obtaining the queue lock.
DEBUG_ASSERT(scale > 0);
[&]() TA_NO_THREAD_SAFETY_ANALYSIS {
performance_scale_ = scale;
default_performance_scale_ = scale;
pending_user_performance_scale_ = scale;
performance_scale_reciprocal_ = 1 / scale;
}();
}
void Scheduler::UpdatePerformanceScales(zx_cpu_performance_info_t* info, size_t count) {
DEBUG_ASSERT(count <= percpu::processor_count());
InterruptDisableGuard irqd;
cpu_num_t cpus_to_reschedule_mask = 0;
for (auto& entry : ktl::span{info, count}) {
DEBUG_ASSERT(entry.logical_cpu_number <= percpu::processor_count());
cpus_to_reschedule_mask |= cpu_num_to_mask(entry.logical_cpu_number);
Scheduler* scheduler = Scheduler::Get(entry.logical_cpu_number);
Guard<MonitoredSpinLock, NoIrqSave> guard{&scheduler->queue_lock_, SOURCE_TAG};
// TODO(eieio): Apply a minimum value threshold and update the entry if
// the requested value is below it.
scheduler->pending_user_performance_scale_ = ToSchedPerformanceScale(entry.performance_scale);
// Return the original performance scale.
entry.performance_scale = ToUserPerformanceScale(scheduler->performance_scale());
}
RescheduleMask(cpus_to_reschedule_mask);
}
void Scheduler::GetPerformanceScales(zx_cpu_performance_info_t* info, size_t count) {
DEBUG_ASSERT(count <= percpu::processor_count());
for (cpu_num_t i = 0; i < count; i++) {
Scheduler* scheduler = Scheduler::Get(i);
Guard<MonitoredSpinLock, IrqSave> guard{&scheduler->queue_lock_, SOURCE_TAG};
info[i].logical_cpu_number = i;
info[i].performance_scale = ToUserPerformanceScale(scheduler->pending_user_performance_scale_);
}
}
void Scheduler::GetDefaultPerformanceScales(zx_cpu_performance_info_t* info, size_t count) {
DEBUG_ASSERT(count <= percpu::processor_count());
for (cpu_num_t i = 0; i < count; i++) {
Scheduler* scheduler = Scheduler::Get(i);
Guard<MonitoredSpinLock, IrqSave> guard{&scheduler->queue_lock_, SOURCE_TAG};
info[i].logical_cpu_number = i;
info[i].performance_scale = ToUserPerformanceScale(scheduler->default_performance_scale_);
}
}
template <Affinity AffinityType>
cpu_mask_t Scheduler::SetCpuAffinity(Thread& thread, cpu_mask_t affinity) {
if constexpr (AffinityType == Affinity::Hard) {
DEBUG_ASSERT_MSG(
(affinity & mp_get_active_mask()) != 0,
"Attempted to set affinity mask to %#x, which has no overlap of active CPUs %#x.", affinity,
mp_get_active_mask());
}
auto AssignAffinity = [](Thread& thread, cpu_mask_t affinity)
TA_NO_THREAD_SAFETY_ANALYSIS -> cpu_mask_t {
cpu_mask_t previous_affinity;
if constexpr (AffinityType == Affinity::Hard) {
previous_affinity = thread.scheduler_state().hard_affinity_;
thread.scheduler_state().hard_affinity_ = affinity;
} else {
static_assert(AffinityType == Affinity::Soft);
previous_affinity = thread.scheduler_state().soft_affinity_;
thread.scheduler_state().soft_affinity_ = affinity;
}
return previous_affinity;
};
// Acquire the thread's lock unconditionally, and without using a guard. The
// call to Scheduler::Migrate will drop the lock for us before potentially
// rescheduling.
ChainLockTransactionIrqSave clt{CLT_TAG("Scheduler::SetCpuAffinity")};
for (;; clt.Relax()) {
thread.get_lock().AcquireUnconditionally();
// set the affinity mask. Note: to mutate the scheduler state (instead of
// just reading it), we need to hold both the thread's lock, as well as the
// lock of any container the thread happens to be in at the time (if any).
//
// TODO(johngro): It is really easy to mess this requirement up. Is there a
// better way to statically assert these requirements?
cpu_mask_t previous_affinity;
if ((thread.state() == THREAD_RUNNING) || (thread.state() == THREAD_READY)) {
// We are assigned to a scheduler, we need to hold its lock while mutate
// our affinity. That said, we have all of the chain locks we need, so go
// ahead and finalize our transaction now.
clt.Finalize();
SchedulerState& ss = thread.scheduler_state();
DEBUG_ASSERT(ss.curr_cpu() != INVALID_CPU);
Scheduler* scheduler = Scheduler::Get(ss.curr_cpu());
Guard<MonitoredSpinLock, NoIrqSave> queue_guard{&scheduler->queue_lock(), SOURCE_TAG};
previous_affinity = AssignAffinity(thread, affinity);
} else if (thread.state() == THREAD_BLOCKED) {
// We are in a wait queue, we need to obtain the wait queue's lock, and be
// prepared to back off if needed.
WaitQueue* wq = thread.wait_queue_state().blocking_wait_queue_;
DEBUG_ASSERT(wq != nullptr);
if (wq->get_lock().Acquire() == ChainLock::LockResult::kBackoff) {
thread.get_lock().Release();
continue;
}
clt.Finalize();
wq->get_lock().AssertAcquired();
previous_affinity = AssignAffinity(thread, affinity);
wq->get_lock().Release();
} else {
// No container, we have all the locks we need.
clt.Finalize();
previous_affinity = AssignAffinity(thread, affinity);
}
// Is our new affinity mask incompatible with our current CPU? If so, let
// let Scheduler::Migrate deal with the rest of the migration, it will drop
// our lock for us. Otherwise, just drop the thread's lock and get out.
if ((affinity & cpu_num_to_mask(arch_curr_cpu_num())) == 0) {
Scheduler::Migrate(&thread);
} else {
thread.get_lock().Release();
}
return previous_affinity;
}
}
template cpu_mask_t Scheduler::SetCpuAffinity<Affinity::Hard>(Thread& thread, cpu_mask_t affinity);
template cpu_mask_t Scheduler::SetCpuAffinity<Affinity::Soft>(Thread& thread, cpu_mask_t affinity);