microbit: Added fork_on_block functionality to fiber scheduler

The microbit fiber scheduler is often used to service event handlers by the microbit message bus.
This provides a very elegant decoupling of user code from system code, interrupt handlers and
also allows users to make blocking calls within event handlers. However, many event handlers are
non-blocking, so launching a dedicated fiber is wasteful in terms of time and SRAM.

This patch adds fork_on_block() to the scheduler. Inspired by the UNIX copy_on_write
technique, this optimisation is semantically equivalent to a create_fiber() call, but will first attempt
to execute the given function in the context of the currently running fiber, and *only* create a fiber
if the given code attempts a blocking operation.

More specifically, this update:

 - adds fork_on_block() functions for parameterised and non-parameterised functions.
 - adds fields to the fiber context to record the status of parent/child fibers.
 - adds optimised ASM functions to store and restore Cortex M0 register context.
 - adds a utility function to determine if the processor is executing in interrupt context.
 - updates to sleep() and wait_for_event() to handle fork_on_block semantics.
 - minor code optimsations within the scheduler.
This commit is contained in:
Joe Finney 2015-08-19 23:27:26 +01:00
parent 538e1c48bd
commit f4b8a1a272
4 changed files with 476 additions and 68 deletions

View file

@ -14,9 +14,9 @@
#include "MicroBitMessageBus.h"
// Typical stack size of each fiber.
// A physical stack of anything less than 512 will likely hit overflow issues during ISR/mBed calls.
// A physical stack of anything less than 1024 will likely hit overflow issues during ISR/mBed calls.
// However, as we're running a co=operative fiber scheduler, the size of the stack at the point of
// context switching is normally *very* small (circa 12 bytes!). Also, as we're likely to have many short lived threads
// context switching is normally *very* small (circa 64 bytes!). Also, as we're likely to have many short lived threads
// being used, be actually perform a stack duplication on context switch, which keeps the RAM footprint of a fiber
// down to a minimum, without constraining what can be done insode a fiber context.
//
@ -27,7 +27,11 @@
#define FIBER_TICK_PERIOD_MS 6
#define CORTEX_M0_STACK_BASE (0x20004000 - 4)
#define MICROBIT_FLAG_DATA_READ 0x01
#define MICROBIT_FLAG_DATA_READ 0x01
#define MICROBIT_FIBER_FLAG_FOB 0x01
#define MICROBIT_FIBER_FLAG_PARENT 0x02
#define MICROBIT_FIBER_FLAG_CHILD 0x04
/**
* Thread Context for an ARM Cortex M0 core.
@ -65,6 +69,7 @@ struct Fiber
uint32_t stack_top, stack_bottom; // Address of this Fiber's stack. Stack is heap allocated, and full descending.
Cortex_M0_TCB tcb; // Thread context when last scheduled out.
uint32_t context; // Context specific information.
uint32_t flags; // Information about this fiber.
Fiber **queue; // The queue this fiber is stored on.
Fiber *next, *prev; // Position of this Fiber on the run queues.
};
@ -167,6 +172,41 @@ void scheduler_tick();
*/
void fiber_wait_for_event(uint16_t id, uint16_t value);
/**
* Executes the given function asynchronously.
*
* Fibers are often used to run event handlers, however many of these event handlers are very simple functions
* that complete very quickly, bringing unecessary RAM overhead.
*
* This function takes a snapshot of the current processor context, then attempts to optimistically call the given function directly.
* We only create an additional fiber if that function performs a block operation.
*
* @param entry_fn The function to execute.
*/
void fork_on_block(void (*entry_fn)(void));
/**
* Executes the given function asynchronously.
*
* Fibers are often used to run event handlers, however many of these event handlers are very simple functions
* that complete very quickly, bringing unecessary RAM. overhead
*
* This function takes a snapshot of the current fiber context, then attempt to optimistically call the given function directly.
* We only create an additional fiber if that function performs a block operation.
*
* @param entry_fn The function to execute.
* @param param an untyped parameter passed into the entry_fn anf completion_fn.
*/
void fork_on_block(void (*entry_fn)(void *), void *param);
/**
* Resizes the stack allocation of the current fiber if necessary to hold the system stack.
*
* If the stack allocaiton is large enough to hold the current system stack, then this function does nothing.
* Otherwise, the the current allocation of the fiber is freed, and a larger block is allocated.
*/
inline void verify_stack_size(Fiber *f);
/**
* Event callback. Called from the message bus whenever an event is raised.
@ -201,11 +241,22 @@ void dequeue_fiber(Fiber *f);
void idle_task();
/**
* Assembler Ccontext switch routing.
* Determines if the processor is executing in interrupt context.
* @return true if any the processor is currently executing any interrupt service routine. False otherwise.
*/
inline int inInterruptContext()
{
return (((int)__get_IPSR()) & 0x003F) > 0;
}
/**
* Assembler Context switch routing.
* Defined in CortexContextSwitch.s
*/
extern "C" void swap_context(Cortex_M0_TCB *from, Cortex_M0_TCB *to, uint32_t from_stack, uint32_t to_stack);
extern "C" void save_context(Cortex_M0_TCB *tcb, uint32_t stack);
extern "C" void save_register_context(Cortex_M0_TCB *tcb);
extern "C" void restore_register_context(Cortex_M0_TCB *tcb);
/**
* Time since power on. Measured in milliseconds.

View file

@ -1,8 +1,10 @@
AREA asm_func, CODE, READONLY
; Export our context switching subroutine as a C function for use in mBed
; Export our context switching subroutine as a C function for use in mbed
EXPORT swap_context
EXPORT save_context
EXPORT save_register_context
EXPORT restore_register_context
ALIGN
@ -183,5 +185,85 @@ store_stack1
; Return to caller (scheduler).
BX LR
; R0 Contains a pointer to the TCB of the fibre to snapshot
save_register_context
; Write our core registers into the TCB
; First, store the general registers
STR R0, [R0,#0]
STR R1, [R0,#4]
STR R2, [R0,#8]
STR R3, [R0,#12]
STR R4, [R0,#16]
STR R5, [R0,#20]
STR R6, [R0,#24]
STR R7, [R0,#28]
; Now the high general purpose registers
MOV R4, R8
STR R4, [R0,#32]
MOV R4, R9
STR R4, [R0,#36]
MOV R4, R10
STR R4, [R0,#40]
MOV R4, R11
STR R4, [R0,#44]
MOV R4, R12
STR R4, [R0,#48]
; Now the Stack Pointer and Link Register.
; As this context is only intended for use with a fiber scheduler,
; we don't need the PC.
MOV R4, SP
STR R4, [R0,#52]
MOV R4, LR
STR R4, [R0,#56]
; Restore scratch registers.
LDR R4, [R0, #16]
; Return to caller (scheduler).
BX LR
restore_register_context
;
; Now page in the new context.
; Update all registers except the PC. We can also safely ignore the STATUS register, as we're just a fiber scheduler.
;
LDR R4, [R0, #56]
MOV LR, R4
LDR R4, [R0, #52]
MOV SP, R4
; High registers...
LDR R4, [R0, #48]
MOV R12, R4
LDR R4, [R0, #44]
MOV R11, R4
LDR R4, [R0, #40]
MOV R10, R4
LDR R4, [R0, #36]
MOV R9, R4
LDR R4, [R0, #32]
MOV R8, R4
; Low registers...
LDR R7, [R0, #28]
LDR R6, [R0, #24]
LDR R5, [R0, #20]
LDR R4, [R0, #16]
LDR R3, [R0, #12]
LDR R2, [R0, #8]
LDR R0, [R0, #0]
LDR R1, [R0, #4]
; Return to caller (normally the scheduler).
BX LR
ALIGN
END
END

View file

@ -4,9 +4,11 @@
.text
.align 2
@ Export our context switching subroutine as a C function for use in mBed
@ Export our context switching subroutine as a C function for use in mbed
.global swap_context
.global save_context
.global save_register_context
.global restore_register_context
@ R0 Contains a pointer to the TCB of the fibre being scheduled out.
@ R1 Contains a pointer to the TCB of the fibre being scheduled in.
@ -185,3 +187,82 @@ store_stack1:
@ Return to caller (scheduler).
BX LR
@ R0 Contains a pointer to the TCB of the fibre to snapshot
save_register_context:
@ Write our core registers into the TCB
@ First, store the general registers
STR R0, [R0,#0]
STR R1, [R0,#4]
STR R2, [R0,#8]
STR R3, [R0,#12]
STR R4, [R0,#16]
STR R5, [R0,#20]
STR R6, [R0,#24]
STR R7, [R0,#28]
@ Now the high general purpose registers
MOV R4, R8
STR R4, [R0,#32]
MOV R4, R9
STR R4, [R0,#36]
MOV R4, R10
STR R4, [R0,#40]
MOV R4, R11
STR R4, [R0,#44]
MOV R4, R12
STR R4, [R0,#48]
@ Now the Stack Pointer and Link Register.
@ As this context is only intended for use with a fiber scheduler,
@ we don't need the PC.
MOV R4, SP
STR R4, [R0,#52]
MOV R4, LR
STR R4, [R0,#56]
@ Restore scratch registers.
LDR R4, [R0, #16]
@ Return to caller (scheduler).
BX LR
restore_register_context:
@
@ Now page in the new context.
@ Update all registers except the PC. We can also safely ignore the STATUS register, as we're just a fiber scheduler.
@
LDR R4, [R0, #56]
MOV LR, R4
LDR R4, [R0, #52]
MOV SP, R4
@ High registers...
LDR R4, [R0, #48]
MOV R12, R4
LDR R4, [R0, #44]
MOV R11, R4
LDR R4, [R0, #40]
MOV R10, R4
LDR R4, [R0, #36]
MOV R9, R4
LDR R4, [R0, #32]
MOV R8, R4
@ Low registers...
LDR R7, [R0, #28]
LDR R6, [R0, #24]
LDR R5, [R0, #20]
LDR R4, [R0, #16]
LDR R3, [R0, #12]
LDR R2, [R0, #8]
LDR R0, [R0, #0]
LDR R1, [R0, #4]
@ Return to caller (normally the scheduler).
BX LR

View file

@ -1,5 +1,5 @@
/**
* The MicroBit Fiber scheduler.
* The MicroBit Fiber scheduler.
*
* This lightweight, non-preemptive scheduler provides a simple threading mechanism for two main purposes:
*
@ -19,6 +19,7 @@
*/
Fiber *currentFiber = NULL; // The context in which the current fiber is executing.
Fiber *forkedFiber = NULL; // The context in which a newly created child fiber is executing.
Fiber *runQueue = NULL; // The list of runnable fibers.
Fiber *sleepQueue = NULL; // The list of blocked fibers waiting on a fiber_sleep() operation.
Fiber *waitQueue = NULL; // The list of blocked fibers waiting on an event.
@ -66,6 +67,12 @@ void queue_fiber(Fiber *f, Fiber **queue)
*/
void dequeue_fiber(Fiber *f)
{
// If this fiber is already dequeued, nothing the there's nothing to do.
if (f->queue == NULL)
return;
// Remove this fiber fromm whichever queue it is on.
__disable_irq();
if (f->prev != NULL)
@ -84,6 +91,46 @@ void dequeue_fiber(Fiber *f)
}
/**
* Allocates a fiber from the fiber pool if availiable. Otherwise, allocates a new one from the heap.
*/
Fiber *getFiberContext()
{
Fiber *f;
__disable_irq();
if (fiberPool != NULL)
{
f = fiberPool;
dequeue_fiber(f);
// dequeue_fiber() exits with irqs enabled, so no need to do this again!
}
else
{
__enable_irq();
f = new Fiber();
if (f == NULL)
return NULL;
f->stack_bottom = (uint32_t) malloc(FIBER_STACK_SIZE);
f->stack_top = f->stack_bottom + FIBER_STACK_SIZE;
if (f->stack_bottom == NULL)
{
delete f;
return NULL;
}
}
f->flags = 0;
return f;
}
/**
* Initialises the Fiber scheduler.
@ -97,6 +144,7 @@ void scheduler_init()
currentFiber = new Fiber();
currentFiber->stack_bottom = (uint32_t) malloc(FIBER_STACK_SIZE);
currentFiber->stack_top = ((uint32_t) currentFiber->stack_bottom) + FIBER_STACK_SIZE;
currentFiber->flags = 0;
// Add ourselves to the run queue.
queue_fiber(currentFiber, &runQueue);
@ -185,14 +233,31 @@ void scheduler_event(MicroBitEvent evt)
*/
void fiber_sleep(unsigned long t)
{
// Calculate and store the time we want to wake up.
currentFiber->context = ticks + t;
Fiber *f = currentFiber;
// Sleep is a blocking call, so if we're in a fork on block context,
// it's time to spawn a new fiber...
if (currentFiber->flags & MICROBIT_FIBER_FLAG_FOB)
{
// Allocate a TCB from the new fiber. This will come from the tread pool if availiable,
// else a new one will be allocated on the heap.
forkedFiber = getFiberContext();
// Remove ourselve from the run queue
dequeue_fiber(currentFiber);
// If we're out of memory, there's nothing we can do.
// keep running in the context of the current thread as a best effort.
if (forkedFiber != NULL)
f = forkedFiber;
}
// Calculate and store the time we want to wake up.
f->context = ticks + t;
// Remove fiber from the run queue
dequeue_fiber(f);
// Add ourselves to the sleep queue. We maintain strict ordering here to reduce lookup times.
queue_fiber(currentFiber, &sleepQueue);
// Add fiber to the sleep queue. We maintain strict ordering here to reduce lookup times.
queue_fiber(f, &sleepQueue);
// Finally, enter the scheduler.
schedule();
@ -211,53 +276,143 @@ void fiber_sleep(unsigned long t)
*/
void fiber_wait_for_event(uint16_t id, uint16_t value)
{
Fiber *f = currentFiber;
// Sleep is a blocking call, so if we'r ein a fork on block context,
// it's time to spawn a new fiber...
if (currentFiber->flags & MICROBIT_FIBER_FLAG_FOB)
{
// Allocate a TCB from the new fiber. This will come from the tread pool if availiable,
// else a new one will be allocated on the heap.
forkedFiber = getFiberContext();
// If we're out of memory, there's nothing we can do.
// keep running in the context of the current thread as a best effort.
if (forkedFiber != NULL)
f = forkedFiber;
}
// Encode the event data in the context field. It's handy having a 32 bit core. :-)
currentFiber->context = value << 16 | id;
f->context = value << 16 | id;
// Remove ourselve from the run queue
dequeue_fiber(currentFiber);
dequeue_fiber(f);
// Add ourselves to the sleep queue. We maintain strict ordering here to reduce lookup times.
queue_fiber(currentFiber, &waitQueue);
queue_fiber(f, &waitQueue);
// Finally, enter the scheduler.
schedule();
}
Fiber *getFiberContext()
/**
* Executes the given function asynchronously.
*
* Fibers are often used to run event handlers, however many of these event handlers are very simple functions
* that complete very quickly, bringing unecessary RAM overhead.
*
* This function takes a snapshot of the current processor context, then attempts to optimistically call the given function directly.
* We only create an additional fiber if that function performs a block operation.
*
* @param entry_fn The function to execute.
*/
void fork_on_block(void (*entry_fn)(void))
{
Fiber *f;
__disable_irq();
if (fiberPool != NULL)
{
f = fiberPool;
dequeue_fiber(f);
// dequeue_fiber() exits with irqs enablesd, so no need to do this again!
}
else
{
__enable_irq();
f = new Fiber();
if (f == NULL)
return NULL;
// Validate our parameters.
if (entry_fn == NULL)
return;
if (currentFiber->flags & MICROBIT_FIBER_FLAG_FOB)
{
// If we attempt a fork on block whilst already in fork n block context,
// simply launch a fiber to deal with the request and we're done.
create_fiber(entry_fn);
return;
}
// Snapshot current context, but also update the Link Register to
// refer to our calling function.
save_register_context(&currentFiber->tcb);
// If we're here, there are three possibilities:
// 1) We're about to attempt to execute the user code
// 2) We've already tried to execute the code, it blocked, and we've backtracked.
// If we're returning from the user function and we forked another fiber then cleanup and exit.
if (currentFiber->flags & MICROBIT_FIBER_FLAG_PARENT)
{
currentFiber->flags &= ~MICROBIT_FIBER_FLAG_FOB;
currentFiber->flags &= ~MICROBIT_FIBER_FLAG_PARENT;
return;
}
// Otherwise, we're here for the first time. Enter FORK ON BLOCK mode, and
// execute the function directly. If the code tries to block, we detect this and
// spawn a thread to deal with it.
currentFiber->flags |= MICROBIT_FIBER_FLAG_FOB;
entry_fn();
currentFiber->flags &= ~MICROBIT_FIBER_FLAG_FOB;
// If this is is an exiting fiber that for spawned to handle a blocking call, recycle it.
// The fiber will then re-enter the scheduler, so no need for further cleanup.
if (currentFiber->flags & MICROBIT_FIBER_FLAG_CHILD)
release_fiber();
}
/**
* Executes the given parameterized function asynchronously.
*
* Fibers are often used to run event handlers, however many of these event handlers are very simple functions
* that complete very quickly, bringing unecessary RAM overhead.
*
* This function takes a snapshot of the current processor context, then attempt to optimistically call the given function directly.
* We only create an additional fiber if that function performs a block operation.
*
* @param entry_fn The function to execute.
* @param param an untyped parameter passed into the entry_fn anf completion_fn.
*/
void fork_on_block(void (*entry_fn)(void *), void *param)
{
// Validate our parameters.
if (entry_fn == NULL)
return;
if (currentFiber->flags & MICROBIT_FIBER_FLAG_FOB)
{
// If we attempt a fork on block whilst already in fork n block context,
// simply launch a fiber to deal with the request and we're done.
create_fiber(entry_fn, param);
return;
}
// Snapshot current context, but also update the Link Register to
// refer to our calling function.
save_register_context(&currentFiber->tcb);
// If we're here, there are three possibilities:
// 1) We're about to attempt to execute the user code
// 2) We've already tried to execute the code, it blocked, and we've backtracked.
// If we're returning from the user function and we forked another fiber then cleanup and exit.
if (currentFiber->flags & MICROBIT_FIBER_FLAG_PARENT)
{
currentFiber->flags &= ~MICROBIT_FIBER_FLAG_FOB;
currentFiber->flags &= ~MICROBIT_FIBER_FLAG_PARENT;
return;
}
// Otherwise, we're here for the first time. Enter FORK ON BLOCK mode, and
// execute the function directly. If the code tries to block, we detect this and
// spawn a thread to deal with it.
currentFiber->flags |= MICROBIT_FIBER_FLAG_FOB;
entry_fn(param);
currentFiber->flags &= ~MICROBIT_FIBER_FLAG_FOB;
// If this is is an exiting fiber that for spawned to handle a blocking call, recycle it.
// The fiber will then re-enter the scheduler, so no need for further cleanup.
if (currentFiber->flags & MICROBIT_FIBER_FLAG_CHILD)
release_fiber();
f->stack_bottom = (uint32_t) malloc(FIBER_STACK_SIZE);
f->stack_top = f->stack_bottom + FIBER_STACK_SIZE;
if (f->stack_bottom == NULL)
{
delete f;
return NULL;
}
}
return f;
}
void launch_new_fiber()
@ -411,6 +566,34 @@ void release_fiber(void)
schedule();
}
/**
* Resizes the stack allocation of the current fiber if necessary to hold the system stack.
*
* If the stack allocaiton is large enough to hold the current system stack, then this function does nothing.
* Otherwise, the the current allocation of the fiber is freed, and a larger block is allocated.
*/
void verify_stack_size(Fiber *f)
{
// Ensure the stack buffer is large enough to hold the stack Reallocate if necessary.
uint32_t stackDepth;
uint32_t bufferSize;
// Calculate the stack depth.
stackDepth = CORTEX_M0_STACK_BASE - ((uint32_t) __get_MSP());
bufferSize = f->stack_top - f->stack_bottom;
// If we're too small, increase our buffer exponentially.
if (bufferSize < stackDepth)
{
while (bufferSize < stackDepth)
bufferSize = bufferSize << 1;
free((void *)f->stack_bottom);
f->stack_bottom = (uint32_t) malloc(bufferSize);
f->stack_top = f->stack_bottom + bufferSize;
}
}
/**
* Calls the Fiber scheduler.
* The calling Fiber will likely be blocked, and control given to another waiting fiber.
@ -418,9 +601,36 @@ void release_fiber(void)
*/
void schedule()
{
// Just round robin for now!
// First, take a reference to the currently running fiber;
Fiber *oldFiber = currentFiber;
// First, see if we're in Fork on Block context. If so, we simply want to store the full context
// of the currently running thread in a nrely created fiber, and restore the context of the
// currently running fiber.
if (currentFiber->flags & MICROBIT_FIBER_FLAG_FOB)
{
// Ensure the stack allocation of the new fiber is large enough
verify_stack_size(forkedFiber);
// Record that the fibers have a parent/child relationship
currentFiber->flags |= MICROBIT_FIBER_FLAG_PARENT;
forkedFiber->flags |= MICROBIT_FIBER_FLAG_CHILD;
// Store the full context of this fiber.
save_context(&forkedFiber->tcb, forkedFiber->stack_top);
// We may now be either the newly created thread, or the one that created it.
// if the FORK_ON_BLOCK flag is still set, we're the old thread, so
// restore the current fiber to its stored context, and we're done.
// if we're the new thread, we must have been unblocked by the scheduler, so simply return.
if (currentFiber->flags & MICROBIT_FIBER_FLAG_PARENT)
restore_register_context(&currentFiber->tcb);
else
return;
}
// We're in a normal scheduling context, so perform a round robin algorithm across runnable fibers.
// OK - if we've nothing to do, then run the IDLE task (power saving sleep)
if (runQueue == NULL || fiber_flags & MICROBIT_FLAG_DATA_READ)
currentFiber = idle;
@ -437,24 +647,8 @@ void schedule()
// Don't bother with the overhead of switching if there's only one fiber on the runqueue!
if (currentFiber != oldFiber)
{
// Ensure the stack buffer is large enough to hold the stack Reallocate if necessary.
uint32_t stackDepth;
uint32_t bufferSize;
// Calculate the stack depth.
stackDepth = CORTEX_M0_STACK_BASE - ((uint32_t) __get_MSP());
bufferSize = oldFiber->stack_top - oldFiber->stack_bottom;
// If we're too small, increase our buffer exponentially.
if (bufferSize < stackDepth)
{
while (bufferSize < stackDepth)
bufferSize = bufferSize << 1;
free((void *)oldFiber->stack_bottom);
oldFiber->stack_bottom = (uint32_t) malloc(bufferSize);
oldFiber->stack_top = oldFiber->stack_bottom + bufferSize;
}
// Ensure the stack allocation of the fiber being scheduled out is large enough
verify_stack_size(oldFiber);
// Schedule in the new fiber.
swap_context(&oldFiber->tcb, &currentFiber->tcb, oldFiber->stack_top, currentFiber->stack_top);