summaryrefslogtreecommitdiff
path: root/src
diff options
context:
space:
mode:
Diffstat (limited to 'src')
-rw-r--r--src/thread.c408
1 files changed, 221 insertions, 187 deletions
diff --git a/src/thread.c b/src/thread.c
index 70f036e..30f5541 100644
--- a/src/thread.c
+++ b/src/thread.c
@@ -1,5 +1,5 @@
/*
- * Copyright (c) 2017 Richard Braun.
+ * Copyright (c) 2017-2018 Richard Braun.
* Copyright (c) 2017 Jerko Lenstra.
*
* Permission is hereby granted, free of charge, to any person obtaining a
@@ -68,6 +68,25 @@ struct thread_list {
* The current thread, which is the one currently running on the processor,
* isn't in any of the thread lists.
*
+ * A thread may be be "voluntarily" preempted, when it yields the processor
+ * itself, or "involuntarily" preempted when an interrupt occurs. Since it's
+ * not possible to immediately yield the processor when preemption is
+ * disabled (including in the middle of an interrupt handler), preemption
+ * may only occur on specific events :
+ * - Calling thread_yield() with preemption enabled.
+ * - Preemption is reenabled, i.e. the preemption level goes back to 0.
+ * - Return from an interrupt.
+ *
+ * Calling thread_yield() is completely voluntary and is ignored in this
+ * discussion. Returning from an interrupt actually reenables preemption,
+ * so the only case discussed is reenabling preemption. The mechanism used
+ * to trigger scheduling when preemption is reenabled is the yield flag.
+ * When the scheduler determines that the current thread should yield the
+ * processor, e.g. because an interrupt handler has awaken a higher priority
+ * thread, it sets the yield flag of the run queue. When preemption is
+ * reenabled, the flag is checked, and if true, the current thread yields
+ * the processor.
+ *
* The idle thread runs when no other thread is in the running state.
*
* Interrupts and preemption must be disabled when accessing a run queue.
@@ -77,6 +96,8 @@ struct thread_list {
*/
struct thread_runq {
struct thread *current;
+ bool yield;
+ unsigned int preempt_level;
unsigned int nr_threads;
struct thread_list lists[THREAD_NR_PRIORITIES];
struct thread *idle;
@@ -100,31 +121,13 @@ enum thread_state {
* When the context is saved, the stack pointer is updated to the value
* of the stack register at the time the thread context is saved.
*
- * A thread may be be "voluntarily" preempted, when it yields the processor
- * itself, or "involuntarily" preempted when an interrupt occurs. Since it's
- * not possible to immediately yield the processor when preemption is
- * disabled (including in the middle of an interrupt handler), preemption
- * may only occur on specific events :
- * - Calling thread_yield() with preemption enabled.
- * - Preemption is reenabled, i.e. the preemption level goes back to 0.
- * - Return from an interrupt.
- *
- * Calling thread_yield() is completely voluntary and is ignored in this
- * discussion. Returning from an interrupt actually reenables preemption,
- * so the only case discussed is reenabling preemption. The mechanism used
- * to trigger scheduling when preemption is reenabled is the yield flag.
- * When the scheduler determines that the current thread should yield the
- * processor, e.g. because an interrupt handler has awaken a higher priority
- * thread, it marks the current thread for yielding. When preemption is
- * reenabled, the flag is checked, and if true, the current thread yields
- * the processor.
+ * Accessing threads is subject to the same synchronization rules as a
+ * run queue.
*/
struct thread {
void *sp;
enum thread_state state;
- bool yield;
struct list node;
- unsigned int preempt_level;
unsigned int priority;
struct thread *joiner;
char name[THREAD_NAME_MAX_SIZE];
@@ -165,215 +168,129 @@ thread_idle(void *arg)
}
static bool
-thread_is_running(const struct thread *thread)
+thread_scheduler_locked(void)
{
- return thread->state == THREAD_STATE_RUNNING;
+ return !cpu_intr_enabled() && !thread_preempt_enabled();
}
static void
-thread_set_running(struct thread *thread)
+thread_list_init(struct thread_list *list)
{
- thread->state = THREAD_STATE_RUNNING;
+ list_init(&list->threads);
}
static void
-thread_set_sleeping(struct thread *thread)
+thread_list_remove(struct thread *thread)
{
- thread->state = THREAD_STATE_SLEEPING;
-}
-
-static bool
-thread_is_dead(const struct thread *thread)
-{
- return thread->state == THREAD_STATE_DEAD;
+ list_remove(&thread->node);
}
static void
-thread_set_dead(struct thread *thread)
+thread_list_enqueue(struct thread_list *list, struct thread *thread)
{
- thread->state = THREAD_STATE_DEAD;
+ list_insert_tail(&list->threads, &thread->node);
}
-static bool
-thread_should_yield(const struct thread *thread)
+static struct thread *
+thread_list_dequeue(struct thread_list *list)
{
- return thread->yield;
-}
+ struct thread *thread;
-static void
-thread_set_yield(struct thread *thread)
-{
- thread->yield = true;
+ thread = list_first_entry(&list->threads, typeof(*thread), node);
+ thread_list_remove(thread);
+ return thread;
}
-static void
-thread_clear_yield(struct thread *thread)
+static bool
+thread_list_empty(struct thread_list *list)
{
- thread->yield = false;
+ return list_empty(&list->threads);
}
-static unsigned int
-thread_get_priority(struct thread *thread)
+static bool
+thread_is_running(const struct thread *thread)
{
- return thread->priority;
+ return thread->state == THREAD_STATE_RUNNING;
}
static void
-thread_remove_from_list(struct thread *thread)
+thread_set_running(struct thread *thread)
{
- list_remove(&thread->node);
+ thread->state = THREAD_STATE_RUNNING;
}
static void
-thread_yield_if_needed(void)
+thread_set_sleeping(struct thread *thread)
{
- if (thread_should_yield(thread_self())) {
- thread_yield();
- }
+ thread->state = THREAD_STATE_SLEEPING;
}
-void
-thread_preempt_disable(void)
+static bool
+thread_is_dead(const struct thread *thread)
{
- struct thread *thread;
-
- thread = thread_self();
- thread->preempt_level++;
- assert(thread->preempt_level != 0);
-
- /*
- * This is a compiler barrier. It tells the compiler not to reorder
- * the instructions it emits across this point.
- *
- * Reordering is one of the most effective ways to optimize generated
- * machine code, and the C specification allows compilers to optimize
- * very aggressively, which is why C shouldn't be thought of as a
- * "portable assembler" any more.
- *
- * Sometimes, especially when building critical sections, strong control
- * over ordering is required, and this is when compiler barriers should
- * be used. In this kernel, disabling preemption is one of the primary
- * ways to create critical sections. The following barrier makes sure
- * that no instructions from the critical section leak out before it.
- *
- * When using GCC or a compatible compiler such as Clang, compiler
- * barriers are implemented with an inline assembly expression that
- * includes the "memory" clobber. This clobber tells the compiler that
- * memory may change in unexpected ways. All memory accesses started
- * before the barrier must complete before the barrier, and all memory
- * accesses that complete after the barrier must start after the barrier.
- * See barrier() in macros.h.
- *
- * When calling assembly functions, there is usually no need to add
- * an explicit compiler barrier, because the compiler, not knowing
- * what the external function does, normally assumes memory may change,
- * i.e. that the function has unexpected side effects. In C code however,
- * the compiler has better knowledge about side effects. That knowledge
- * comes from the amount of code in a single compilation unit. The
- * traditional compilation unit when building with optimizations is the
- * source file, but with link-time optimizations (LTO), this can be the
- * whole program. This is why specifying barriers in C code is required
- * for a reliable behaviour.
- *
- * Also note that compiler barriers only affect the machine code
- * generated by the compiler. Processors also internally perform
- * various kinds of reordering. When confined to a single processor,
- * this can safely be ignored because the processor model guarantees
- * that the state seen by software matches program order. This
- * assumption breaks on multiprocessor systems though, where memory
- * barriers are also required to enforce ordering across processors.
- */
- barrier();
+ return thread->state == THREAD_STATE_DEAD;
}
static void
-thread_preempt_enable_no_yield(void)
-{
- struct thread *thread;
-
- /* See thread_preempt_disable() */
- barrier();
-
- thread = thread_self();
- assert(thread->preempt_level != 0);
- thread->preempt_level--;
-}
-
-void
-thread_preempt_enable(void)
+thread_set_dead(struct thread *thread)
{
- thread_preempt_enable_no_yield();
- thread_yield_if_needed();
+ thread->state = THREAD_STATE_DEAD;
}
-bool
-thread_preempt_enabled(void)
+static unsigned int
+thread_get_priority(struct thread *thread)
{
- struct thread *thread;
-
- thread = thread_self();
- return thread->preempt_level == 0;
+ return thread->priority;
}
static bool
-thread_scheduler_locked(void)
+thread_runq_should_yield(const struct thread_runq *runq)
{
- return !cpu_intr_enabled() && !thread_preempt_enabled();
-}
-
-static uint32_t
-thread_lock_scheduler(void)
-{
- /*
- * When disabling both preemption and interrupts, it is best to do it in
- * this order, since, if an interrupt occurs after preemption is disabled
- * but before interrupts are disabled, it may not cause a context switch.
- *
- * Besides, disabling interrupts first would slightly increase interrupt
- * latencies.
- */
- thread_preempt_disable();
- return cpu_intr_save();
+ return runq->yield;
}
static void
-thread_unlock_scheduler(uint32_t eflags, bool yield)
+thread_runq_set_yield(struct thread_runq *runq)
{
- cpu_intr_restore(eflags);
-
- if (yield) {
- thread_preempt_enable();
- } else {
- thread_preempt_enable_no_yield();
- }
+ runq->yield = true;
}
static void
-thread_list_init(struct thread_list *list)
+thread_runq_clear_yield(struct thread_runq *runq)
{
- list_init(&list->threads);
+ runq->yield = false;
}
-static void
-thread_list_enqueue(struct thread_list *list, struct thread *thread)
+static unsigned int
+thread_runq_get_preempt_level(const struct thread_runq *runq)
{
- list_insert_tail(&list->threads, &thread->node);
+ return runq->preempt_level;
}
-static struct thread *
-thread_list_dequeue(struct thread_list *list)
+static void
+thread_runq_inc_preempt_level(struct thread_runq *runq)
{
- struct thread *thread;
-
- thread = list_first_entry(&list->threads, typeof(*thread), node);
- thread_remove_from_list(thread);
- return thread;
+ /*
+ * There is no need to protect this increment by disabling interrupts
+ * or using a single interrupt-safe instruction such as inc, because
+ * the preemption level can only be 0 when other threads access it
+ * concurrently. As a result, all threads racing to disable preemption
+ * would read 0 and write 1. Although the increment is not atomic, the
+ * store is serialized such that the first thread that manages to update
+ * memory has effectively prevented all other threads from running,
+ * at least until it reenables preemption, which turns the preemption
+ * level back to 0, making the read-modify-write increment other threads
+ * may have started correct.
+ */
+ runq->preempt_level++;
+ assert(runq->preempt_level != 0);
}
-static bool
-thread_list_empty(struct thread_list *list)
+static void
+thread_runq_dec_preempt_level(struct thread_runq *runq)
{
- return list_empty(&list->threads);
+ assert(runq->preempt_level != 0);
+ runq->preempt_level--;
}
static struct thread_list *
@@ -455,7 +372,7 @@ thread_runq_add(struct thread_runq *runq, struct thread *thread)
assert(runq->nr_threads != 0);
if (thread_get_priority(thread) > thread_get_priority(runq->current)) {
- thread_set_yield(runq->current);
+ thread_runq_set_yield(runq);
}
}
@@ -466,7 +383,7 @@ thread_runq_remove(struct thread_runq *runq, struct thread *thread)
runq->nr_threads--;
assert(!thread_is_running(thread));
- thread_remove_from_list(thread);
+ thread_list_remove(thread);
}
static void
@@ -477,7 +394,7 @@ thread_runq_schedule(struct thread_runq *runq)
prev = thread_runq_get_current(runq);
assert(thread_scheduler_locked());
- assert(prev->preempt_level == 1);
+ assert(runq->preempt_level == 1);
thread_runq_put_prev(runq, prev);
@@ -515,14 +432,127 @@ thread_runq_schedule(struct thread_runq *runq)
}
}
+static void
+thread_yield_if_needed(void)
+{
+ if (thread_runq_should_yield(&thread_runq)) {
+ thread_yield();
+ }
+}
+
+void
+thread_preempt_disable(void)
+{
+ thread_runq_inc_preempt_level(&thread_runq);
+
+ /*
+ * This is a compiler barrier. It tells the compiler not to reorder
+ * the instructions it emits across this point.
+ *
+ * Reordering is one of the most effective ways to optimize generated
+ * machine code, and the C specification allows compilers to optimize
+ * very aggressively, which is why C shouldn't be thought of as a
+ * "portable assembler" any more.
+ *
+ * Sometimes, especially when building critical sections, strong control
+ * over ordering is required, and this is when compiler barriers should
+ * be used. In this kernel, disabling preemption is one of the primary
+ * ways to create critical sections. The following barrier makes sure
+ * that no instructions from the critical section leak out before it.
+ *
+ * When using GCC or a compatible compiler such as Clang, compiler
+ * barriers are implemented with an inline assembly expression that
+ * includes the "memory" clobber. This clobber tells the compiler that
+ * memory may change in unexpected ways. All memory accesses started
+ * before the barrier must complete before the barrier, and all memory
+ * accesses that complete after the barrier must start after the barrier.
+ * See barrier() in macros.h.
+ *
+ * When calling assembly functions, there is usually no need to add
+ * an explicit compiler barrier, because the compiler, not knowing
+ * what the external function does, normally assumes memory may change,
+ * i.e. that the function has unexpected side effects. In C code however,
+ * the compiler has better knowledge about side effects. That knowledge
+ * comes from the amount of code in a single compilation unit. The
+ * traditional compilation unit when building with optimizations is the
+ * source file, but with link-time optimizations (LTO), this can be the
+ * whole program. This is why specifying barriers in C code is required
+ * for a reliable behaviour.
+ *
+ * Also note that compiler barriers only affect the machine code
+ * generated by the compiler. Processors also internally perform
+ * various kinds of reordering. When confined to a single processor,
+ * this can safely be ignored because the processor model guarantees
+ * that the state seen by software matches program order. This
+ * assumption breaks on multiprocessor systems though, where memory
+ * barriers are also required to enforce ordering across processors.
+ */
+ barrier();
+}
+
+static void
+thread_preempt_enable_no_yield(void)
+{
+ /* See thread_preempt_disable() */
+ barrier();
+
+ thread_runq_dec_preempt_level(&thread_runq);
+}
+
+void
+thread_preempt_enable(void)
+{
+ thread_preempt_enable_no_yield();
+ thread_yield_if_needed();
+}
+
+static unsigned int
+thread_preempt_level(void)
+{
+ return thread_runq_get_preempt_level(&thread_runq);
+}
+
+bool
+thread_preempt_enabled(void)
+{
+ return thread_preempt_level() == 0;
+}
+
+static uint32_t
+thread_lock_scheduler(void)
+{
+ /*
+ * When disabling both preemption and interrupts, it is best to do it in
+ * this order, since, if an interrupt occurs after preemption is disabled
+ * but before interrupts are disabled, it may not cause a context switch.
+ *
+ * Besides, disabling interrupts first would slightly increase interrupt
+ * latencies.
+ */
+ thread_preempt_disable();
+ return cpu_intr_save();
+}
+
+static void
+thread_unlock_scheduler(uint32_t eflags, bool yield)
+{
+ cpu_intr_restore(eflags);
+
+ if (yield) {
+ thread_preempt_enable();
+ } else {
+ thread_preempt_enable_no_yield();
+ }
+}
+
void
thread_enable_scheduler(void)
{
struct thread *thread;
+ assert(thread_preempt_level() == 1);
+
thread = thread_runq_get_next(&thread_runq);
- assert(thread);
- assert(thread->preempt_level == 1);
thread_load_context(thread);
/* Never reached */
@@ -533,8 +563,8 @@ thread_main(thread_fn_t fn, void *arg)
{
assert(fn);
- assert(thread_scheduler_locked());
- assert(thread_self()->preempt_level == 1);
+ assert(!cpu_intr_enabled());
+ assert(thread_preempt_level() == 1);
cpu_intr_enable();
thread_preempt_enable();
@@ -654,8 +684,6 @@ thread_init(struct thread *thread, thread_fn_t fn, void *arg,
}
thread->state = THREAD_STATE_RUNNING;
- thread->yield = false;
- thread->preempt_level = 1;
thread->priority = priority;
thread->joiner = NULL;
thread_set_name(thread, name);
@@ -777,34 +805,40 @@ thread_create_idle(void)
}
static void
-thread_runq_preinit(struct thread_runq *runq)
+thread_runq_init(struct thread_runq *runq)
{
+ /*
+ * Set a dummy thread context with preemption disabled to prevent
+ * scheduling functions called before the scheduler is running from
+ * triggering a context switch.
+ */
thread_init(&thread_dummy, NULL, NULL, "dummy", NULL, 0, 0);
runq->current = &thread_dummy;
-}
-
-static void
-thread_runq_init(struct thread_runq *runq)
-{
+ runq->yield = false;
+ runq->preempt_level = 1;
runq->nr_threads = 0;
for (size_t i = 0; i < ARRAY_SIZE(runq->lists); i++) {
thread_list_init(&runq->lists[i]);
}
+}
+static void
+thread_runq_init_idle(struct thread_runq *runq)
+{
runq->idle = thread_create_idle();
}
void
thread_bootstrap(void)
{
- thread_runq_preinit(&thread_runq);
+ thread_runq_init(&thread_runq);
}
void
thread_setup(void)
{
- thread_runq_init(&thread_runq);
+ thread_runq_init_idle(&thread_runq);
}
void
@@ -817,7 +851,7 @@ thread_yield(void)
}
eflags = thread_lock_scheduler();
- thread_clear_yield(thread_self());
+ thread_runq_clear_yield(&thread_runq);
thread_runq_schedule(&thread_runq);
thread_unlock_scheduler(eflags, false);
}
@@ -863,6 +897,6 @@ thread_report_tick(void)
{
assert(thread_scheduler_locked());
- thread_set_yield(thread_self());
+ thread_runq_set_yield(&thread_runq);
timer_report_tick();
}