406 lines
12 KiB
C
406 lines
12 KiB
C
// SPDX-License-Identifier: ISC
|
|
|
|
#include <zinnia/assert.h>
|
|
#include <zinnia/atomic.h>
|
|
#include <zinnia/clock.h>
|
|
#include <zinnia/irq.h>
|
|
#include <zinnia/list.h>
|
|
#include <zinnia/percpu.h>
|
|
#include <zinnia/pmap.h>
|
|
#include <zinnia/print.h>
|
|
#include <zinnia/process.h>
|
|
#include <zinnia/sched.h>
|
|
#include <zinnia/spin.h>
|
|
#include <zinnia/stack.h>
|
|
#include <zinnia/tailq.h>
|
|
#include <zinnia/turnstile.h>
|
|
#include <zinnia/utils.h>
|
|
#include <zinnia/vma.h>
|
|
#include <arch/cpu.h>
|
|
#include <arch/sched.h>
|
|
#include <stdint.h>
|
|
#include "internal.h"
|
|
|
|
static void idle_func(uintptr_t) {
|
|
while (1) {
|
|
irq_set_state(true);
|
|
irq_wait();
|
|
}
|
|
}
|
|
|
|
constexpr int32_t SCHED_CPU_IDLE = -1;
|
|
constexpr int32_t SCHED_CPU_CLAIMED = -2;
|
|
|
|
static _Atomic(struct scheduler*) all_cpus;
|
|
|
|
static struct percpu* cpu_of(struct scheduler* sched) {
|
|
return containerof(sched, struct percpu, sched);
|
|
}
|
|
|
|
static void preempt_tick(struct timer* timer) {
|
|
struct scheduler* sched = containerof(timer, struct scheduler, preempt_timer);
|
|
sched->need_resched = true;
|
|
}
|
|
|
|
static void start_time_slice(struct scheduler* sched, struct task* task, uint64_t now) {
|
|
sched->need_resched = false;
|
|
sched->slice_start = now;
|
|
if (task == sched->idle_task)
|
|
timer_cancel(&sched->preempt_timer);
|
|
else
|
|
timer_arm(&sched->preempt_timer, now + task->time_slice);
|
|
}
|
|
|
|
void sched_init(struct scheduler* sched) {
|
|
spin_init(&sched->lock);
|
|
atomic_store(&sched->kick_pending, false, ATOMIC_RELAXED);
|
|
for (size_t i = 0; i < SCHED_PRIO_COUNT; i++)
|
|
tailq_init(&sched->run_queue[i]);
|
|
|
|
struct task* idle_task;
|
|
assert(task_create("idle", process_kernel(), idle_func, 0, &idle_task) == 0, "Unable to create idle task!");
|
|
idle_task->last_rq = sched;
|
|
sched->idle_task = idle_task;
|
|
|
|
struct task* boot_task;
|
|
assert(task_create_current("boot", process_kernel(), &boot_task) == 0, "Unable to create boot task!");
|
|
boot_task->state = TASK_STATE_RUNNING;
|
|
boot_task->last_rq = sched;
|
|
atomic_store(&boot_task->on_cpu, true, ATOMIC_RELAXED);
|
|
sched->current = boot_task;
|
|
atomic_store(&sched->current_priority, SCHED_PRIO_DEFAULT, ATOMIC_RELAXED);
|
|
|
|
sched->next_cpu = atomic_load(&all_cpus, ATOMIC_RELAXED);
|
|
while (!atomic_compare_exchange_weak(&all_cpus, &sched->next_cpu, sched, ATOMIC_RELEASE, ATOMIC_RELAXED))
|
|
;
|
|
|
|
timer_cpu_init();
|
|
timer_init(&sched->preempt_timer, preempt_tick);
|
|
sched->boost_epoch = 0;
|
|
start_time_slice(sched, boot_task, clock_get_elapsed_ns());
|
|
|
|
log("sched: Scheduler initialized\n");
|
|
}
|
|
|
|
// Asks the CPU of `sched` to reschedule as soon as it has interrupts enabled.
|
|
static void kick(struct scheduler* sched) {
|
|
if (!atomic_exchange(&sched->kick_pending, true, ATOMIC_RELAXED))
|
|
arch_sched_kick(cpu_of(sched));
|
|
}
|
|
|
|
static void rq_insert(struct scheduler* sched, struct task* task) {
|
|
uint8_t prio = atomic_load(&task->priority, ATOMIC_RELAXED);
|
|
tailq_insert_tail(&sched->run_queue[prio], task, next);
|
|
sched->ready_mask |= (uint64_t)1 << prio;
|
|
}
|
|
|
|
static void rq_remove(struct scheduler* sched, struct task* task) {
|
|
uint8_t prio = atomic_load(&task->priority, ATOMIC_RELAXED);
|
|
tailq_remove(&sched->run_queue[prio], task, next);
|
|
if (tailq_empty(&sched->run_queue[prio]))
|
|
sched->ready_mask &= ~((uint64_t)1 << prio);
|
|
}
|
|
|
|
// Returns -1 if no task is ready.
|
|
static int rq_top_priority(struct scheduler* sched) {
|
|
return sched->ready_mask ? 63 - __builtin_clzll(sched->ready_mask) : -1;
|
|
}
|
|
|
|
static uint8_t effective_prio(struct task* task) {
|
|
uint8_t prio = task->base_prio;
|
|
if (prio < SCHED_PRIO_WORK_HIGH)
|
|
prio -= MIN(task->penalty, prio);
|
|
return MAX(prio, task->lent_prio);
|
|
}
|
|
|
|
static void refresh_prio(struct task* task) {
|
|
if (task->blocked_ts == nullptr)
|
|
atomic_store(&task->priority, effective_prio(task), ATOMIC_RELAXED);
|
|
}
|
|
|
|
static size_t quantum(struct task* task) {
|
|
return SCHED_TIME_SLICE_NS * (task->penalty + 1);
|
|
}
|
|
|
|
static void feedback_reset(struct scheduler* sched, struct task* task) {
|
|
task->penalty = 0;
|
|
task->time_slice = quantum(task);
|
|
task->feedback_epoch = sched->boost_epoch;
|
|
refresh_prio(task);
|
|
}
|
|
|
|
static void boost(struct scheduler* sched) {
|
|
for (uint8_t prio = 0; prio < SCHED_PRIO_WORK_HIGH; prio++) {
|
|
struct task* t = tailq_first(&sched->run_queue[prio]);
|
|
while (t) {
|
|
struct task* after = tailq_next(t, next);
|
|
if (t->penalty && t->feedback_epoch < sched->boost_epoch) {
|
|
rq_remove(sched, t);
|
|
feedback_reset(sched, t);
|
|
rq_insert(sched, t);
|
|
}
|
|
t = after;
|
|
}
|
|
}
|
|
}
|
|
|
|
static void account(struct scheduler* sched, struct task* task, uint64_t now) {
|
|
if (task->feedback_epoch < sched->boost_epoch)
|
|
feedback_reset(sched, task);
|
|
|
|
uint64_t ran = now - sched->slice_start;
|
|
sched->slice_start = now;
|
|
if (ran < task->time_slice) {
|
|
task->time_slice -= ran;
|
|
return;
|
|
}
|
|
|
|
if (task->base_prio < SCHED_PRIO_WORK_HIGH && task->penalty < SCHED_FEEDBACK_DEPTH)
|
|
task->penalty++;
|
|
task->time_slice = quantum(task);
|
|
task->feedback_epoch = sched->boost_epoch;
|
|
refresh_prio(task);
|
|
sched->need_resched = true;
|
|
}
|
|
|
|
// Queues `task` on `sched` and preempts its CPU if `task` has a higher prio than what it's running.
|
|
static void enqueue(struct scheduler* sched, struct task* task) {
|
|
spin_lock(&sched->lock);
|
|
task->state = TASK_STATE_READY;
|
|
task->last_rq = sched;
|
|
if (task->feedback_epoch < sched->boost_epoch)
|
|
feedback_reset(sched, task);
|
|
refresh_prio(task);
|
|
rq_insert(sched, task);
|
|
bool preempt = atomic_load(&task->priority, ATOMIC_RELAXED) > atomic_load(&sched->current_priority, ATOMIC_RELAXED);
|
|
spin_unlock(&sched->lock);
|
|
|
|
if (preempt)
|
|
kick(sched);
|
|
}
|
|
|
|
void sched_add_task(struct scheduler* sched, struct task* task) {
|
|
assert(task != nullptr, "Tried to add NULL to the run queue");
|
|
enqueue(sched, task);
|
|
}
|
|
|
|
static struct task* next_task(struct scheduler* sched, uint64_t now) {
|
|
spin_lock(&sched->lock);
|
|
uint64_t epoch = now / SCHED_BOOST_INTERVAL_NS;
|
|
if (epoch > sched->boost_epoch) {
|
|
sched->boost_epoch = epoch;
|
|
boost(sched);
|
|
}
|
|
|
|
int prio = rq_top_priority(sched);
|
|
if (prio < 0) {
|
|
atomic_store(&sched->current_priority, SCHED_CPU_IDLE, ATOMIC_RELAXED);
|
|
spin_unlock(&sched->lock);
|
|
return sched->idle_task;
|
|
}
|
|
|
|
struct task* t = tailq_first(&sched->run_queue[prio]);
|
|
rq_remove(sched, t);
|
|
t->state = TASK_STATE_RUNNING;
|
|
atomic_store(&sched->current_priority, prio, ATOMIC_RELAXED);
|
|
spin_unlock(&sched->lock);
|
|
|
|
return t;
|
|
}
|
|
|
|
static void finish_switch(struct task* prev) {
|
|
// The context of `prev` is fully saved now, so other CPUs can switch to it.
|
|
atomic_store(&prev->on_cpu, false, ATOMIC_RELEASE);
|
|
if (prev->state == TASK_STATE_DEAD)
|
|
task_destroy(prev);
|
|
}
|
|
|
|
static void do_reschedule(struct scheduler* sched, uint64_t now) {
|
|
atomic_store(&sched->kick_pending, false, ATOMIC_RELAXED);
|
|
struct task* from = sched->current;
|
|
struct task* to = next_task(sched, now);
|
|
|
|
if (from != to || sched->need_resched)
|
|
start_time_slice(sched, to, now);
|
|
|
|
// We'd end up on the same task, skip switching altogether.
|
|
if (from == to) {
|
|
irq_unlock();
|
|
return;
|
|
}
|
|
|
|
// A task that was woken by another CPU might not have finished switching away on its old CPU yet.
|
|
while (atomic_load(&to->on_cpu, ATOMIC_ACQUIRE))
|
|
arch_cpu_pause();
|
|
atomic_store(&to->on_cpu, true, ATOMIC_RELAXED);
|
|
|
|
sched->current = to;
|
|
pmap_set(&to->space->pmap);
|
|
|
|
struct percpu* cpu = percpu_get();
|
|
from->user_stack = cpu->user_stack;
|
|
cpu->kernel_stack = kernel_stack_top(&to->kernel_stack);
|
|
cpu->user_stack = to->user_stack;
|
|
|
|
struct irq_percpu irq_state = *percpu_ref(irq_data);
|
|
struct task* prev = arch_sched_switch(from, to);
|
|
*percpu_ref(irq_data) = irq_state;
|
|
finish_switch(prev);
|
|
irq_unlock();
|
|
}
|
|
|
|
void sched_reschedule(struct scheduler* sched) {
|
|
irq_lock();
|
|
struct task* current = sched->current;
|
|
uint64_t now = clock_get_elapsed_ns();
|
|
if (current != sched->idle_task && current->state == TASK_STATE_RUNNING) {
|
|
spin_lock(&sched->lock);
|
|
account(sched, current, now);
|
|
current->state = TASK_STATE_READY;
|
|
rq_insert(sched, current);
|
|
spin_unlock(&sched->lock);
|
|
}
|
|
do_reschedule(sched, now);
|
|
}
|
|
|
|
void sched_yield(struct scheduler* sched) {
|
|
irq_lock();
|
|
do_reschedule(sched, clock_get_elapsed_ns());
|
|
}
|
|
|
|
void sched_exit() {
|
|
irq_lock();
|
|
struct scheduler* sched = &percpu_get()->sched;
|
|
sched->current->state = TASK_STATE_DEAD;
|
|
do_reschedule(sched, clock_get_elapsed_ns());
|
|
unreachable();
|
|
}
|
|
|
|
void sched_block(struct scheduler* sched) {
|
|
irq_lock();
|
|
struct task* current = sched->current;
|
|
|
|
task_lock(current);
|
|
if (atomic_exchange(¤t->wake_pending, false, ATOMIC_ACQ_REL)) {
|
|
task_unlock(current);
|
|
irq_unlock();
|
|
return;
|
|
}
|
|
|
|
uint64_t now = clock_get_elapsed_ns();
|
|
spin_lock(&sched->lock);
|
|
account(sched, current, now);
|
|
spin_unlock(&sched->lock);
|
|
|
|
current->state = TASK_STATE_BLOCKED;
|
|
task_unlock(current);
|
|
|
|
do_reschedule(sched, now);
|
|
}
|
|
|
|
static struct scheduler* select_cpu(struct task* task) {
|
|
struct scheduler* last = task->last_rq ? task->last_rq : &percpu_get()->sched;
|
|
int prio = atomic_load(&task->priority, ATOMIC_RELAXED);
|
|
if (atomic_load(&last->current_priority, ATOMIC_RELAXED) < prio)
|
|
return last;
|
|
|
|
// Claim an idle CPU.
|
|
struct scheduler* s;
|
|
for (s = atomic_load(&all_cpus, ATOMIC_ACQUIRE); s; s = s->next_cpu) {
|
|
int idle = SCHED_CPU_IDLE;
|
|
if (atomic_compare_exchange(&s->current_priority, &idle, SCHED_CPU_CLAIMED, ATOMIC_RELAXED, ATOMIC_RELAXED))
|
|
return s;
|
|
}
|
|
return last;
|
|
}
|
|
|
|
void sched_wake_locked(struct task* task) {
|
|
if (task->state == TASK_STATE_BLOCKED)
|
|
enqueue(select_cpu(task), task);
|
|
else
|
|
atomic_store(&task->wake_pending, true, ATOMIC_RELEASE);
|
|
}
|
|
|
|
void sched_wake(struct task* task) {
|
|
task_lock(task);
|
|
sched_wake_locked(task);
|
|
task_unlock(task);
|
|
}
|
|
|
|
void sched_notify(struct task* task) {
|
|
task_lock(task);
|
|
if (task->state == TASK_STATE_BLOCKED) {
|
|
enqueue(select_cpu(task), task);
|
|
} else {
|
|
atomic_store(&task->wake_pending, true, ATOMIC_RELEASE);
|
|
if (task->state == TASK_STATE_RUNNING && task->last_rq && task->last_rq != &percpu_get()->sched)
|
|
kick(task->last_rq);
|
|
}
|
|
task_unlock(task);
|
|
}
|
|
|
|
bool sched_update_priority_locked(struct task* task) {
|
|
struct scheduler* sched = task->last_rq;
|
|
if (sched == nullptr) {
|
|
// The task was never scheduled, so it isn't queued anywhere.
|
|
uint8_t new_prio = effective_prio(task);
|
|
return atomic_exchange(&task->priority, new_prio, ATOMIC_RELAXED) != new_prio;
|
|
}
|
|
|
|
bool preempt = false;
|
|
spin_lock(&sched->lock);
|
|
uint8_t new_prio = effective_prio(task);
|
|
if (new_prio == atomic_load(&task->priority, ATOMIC_RELAXED)) {
|
|
spin_unlock(&sched->lock);
|
|
return false;
|
|
}
|
|
|
|
if (task->state == TASK_STATE_READY) {
|
|
rq_remove(sched, task);
|
|
atomic_store(&task->priority, new_prio, ATOMIC_RELAXED);
|
|
rq_insert(sched, task);
|
|
preempt = new_prio > atomic_load(&sched->current_priority, ATOMIC_RELAXED);
|
|
} else {
|
|
atomic_store(&task->priority, new_prio, ATOMIC_RELAXED);
|
|
if (task->state == TASK_STATE_RUNNING) {
|
|
atomic_store(&sched->current_priority, new_prio, ATOMIC_RELAXED);
|
|
preempt = rq_top_priority(sched) > new_prio;
|
|
}
|
|
}
|
|
spin_unlock(&sched->lock);
|
|
|
|
if (preempt)
|
|
kick(sched);
|
|
return true;
|
|
}
|
|
|
|
void sched_set_priority(struct task* task, uint8_t priority) {
|
|
assert(priority <= SCHED_PRIO_MAX, "Priority %u is out of range", priority);
|
|
task_lock(task);
|
|
task->base_prio = priority;
|
|
if (sched_update_priority_locked(task))
|
|
turnstile_reprioritize(task);
|
|
else
|
|
task_unlock(task);
|
|
}
|
|
|
|
void task_entry(task_fn_t entry, uintptr_t arg0, struct task* prev) {
|
|
finish_switch(prev);
|
|
|
|
*percpu_ref(irq_data) = (struct irq_percpu){
|
|
.depth = 1,
|
|
.saved_state = true,
|
|
};
|
|
irq_unlock();
|
|
|
|
entry(arg0);
|
|
sched_exit();
|
|
}
|
|
|
|
[[__init]]
|
|
static void sched_stage_init(struct boot_info*) {
|
|
sched_init(&percpu_get()->sched);
|
|
}
|
|
INIT_TASK(sched_stage, "core.sched", sched_stage_init);
|
|
INIT_DEPENDS(sched_stage, percpu_stage);
|