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:
parent
538e1c48bd
commit
f4b8a1a272
4 changed files with 476 additions and 68 deletions
|
@ -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.
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
||||
|
|
|
@ -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(¤tFiber->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(¤tFiber->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(¤tFiber->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, ¤tFiber->tcb, oldFiber->stack_top, currentFiber->stack_top);
|
||||
|
|
Loading…
Reference in a new issue