diff options
author | Jaden Weiss <[email protected]> | 2020-01-03 00:23:37 -0500 |
---|---|---|
committer | Ayke <[email protected]> | 2020-03-17 12:16:10 +0100 |
commit | 6a50f25a48385fc78397b6f0f1c83650130d8d16 (patch) | |
tree | 16745709bcec53d785bd1b17aca718926cd410dd /src | |
parent | 2521cacb513bd59e95dc61746e7e0d0d761dcdc4 (diff) | |
download | tinygo-6a50f25a48385fc78397b6f0f1c83650130d8d16.tar.gz tinygo-6a50f25a48385fc78397b6f0f1c83650130d8d16.zip |
refactor coroutine lowering and tasks
Diffstat (limited to 'src')
24 files changed, 513 insertions, 417 deletions
diff --git a/src/internal/task/queue.go b/src/internal/task/queue.go new file mode 100644 index 000000000..c86bc596c --- /dev/null +++ b/src/internal/task/queue.go @@ -0,0 +1,98 @@ +package task + +const asserts = false + +// Queue is a FIFO container of tasks. +// The zero value is an empty queue. +type Queue struct { + head, tail *Task +} + +// Push a task onto the queue. +func (q *Queue) Push(t *Task) { + if asserts && t.Next != nil { + panic("runtime: pushing a task to a queue with a non-nil Next pointer") + } + if q.tail != nil { + q.tail.Next = t + } + q.tail = t + t.Next = nil + if q.head == nil { + q.head = t + } +} + +// Pop a task off of the queue. +func (q *Queue) Pop() *Task { + t := q.head + if t == nil { + return nil + } + q.head = t.Next + if q.tail == t { + q.tail = nil + } + t.Next = nil + return t +} + +// Append pops the contents of another queue and pushes them onto the end of this queue. +func (q *Queue) Append(other *Queue) { + if q.head == nil { + q.head = other.head + } else { + q.tail.Next = other.head + } + q.tail = other.tail + other.head, other.tail = nil, nil +} + +// Stack is a LIFO container of tasks. +// The zero value is an empty stack. +// This is slightly cheaper than a queue, so it can be preferable when strict ordering is not necessary. +type Stack struct { + top *Task +} + +// Push a task onto the stack. +func (s *Stack) Push(t *Task) { + if asserts && t.Next != nil { + panic("runtime: pushing a task to a stack with a non-nil Next pointer") + } + s.top, t.Next = t, s.top +} + +// Pop a task off of the stack. +func (s *Stack) Pop() *Task { + t := s.top + if t != nil { + s.top = t.Next + } + t.Next = nil + return t +} + +// tail follows the chain of tasks. +// If t is nil, returns nil. +// Otherwise, returns the task in the chain where the Next field is nil. +func (t *Task) tail() *Task { + if t == nil { + return nil + } + for t.Next != nil { + t = t.Next + } + return t +} + +// Queue moves the contents of the stack into a queue. +// Elements can be popped from the queue in the same order that they would be popped from the stack. +func (s *Stack) Queue() Queue { + head := s.top + s.top = nil + return Queue{ + head: head, + tail: head.tail(), + } +} diff --git a/src/internal/task/task.go b/src/internal/task/task.go new file mode 100644 index 000000000..57b29eb39 --- /dev/null +++ b/src/internal/task/task.go @@ -0,0 +1,20 @@ +package task + +import ( + "unsafe" +) + +// Task is a state of goroutine for scheduling purposes. +type Task struct { + // Next is a field which can be used to make a linked list of tasks. + Next *Task + + // Ptr is a field which can be used for storing a pointer. + Ptr unsafe.Pointer + + // Data is a field which can be used for storing state information. + Data uint + + // state is the underlying running state of the task. + state state +} diff --git a/src/internal/task/task_coroutine.go b/src/internal/task/task_coroutine.go new file mode 100644 index 000000000..374bfe11f --- /dev/null +++ b/src/internal/task/task_coroutine.go @@ -0,0 +1,97 @@ +// +build scheduler.coroutines + +package task + +import ( + "unsafe" +) + +// rawState is an underlying coroutine state exposed by llvm.coro. +// This matches *i8 in LLVM. +type rawState uint8 + +//go:export llvm.coro.resume +func (s *rawState) resume() + +type state struct{ *rawState } + +//go:export llvm.coro.noop +func noopState() *rawState + +// Resume the task until it pauses or completes. +func (t *Task) Resume() { + t.state.resume() +} + +// setState is used by the compiler to set the state of the function at the beginning of a function call. +// Returns the state of the caller. +func (t *Task) setState(s *rawState) *rawState { + caller := t.state + t.state = state{s} + return caller.rawState +} + +// returnTo is used by the compiler to return to the state of the caller. +func (t *Task) returnTo(parent *rawState) { + t.state = state{parent} + t.returnCurrent() +} + +// returnCurrent is used by the compiler to return to the state of the caller in a case where the state is not replaced. +func (t *Task) returnCurrent() { + scheduleTask(t) +} + +//go:linkname scheduleTask runtime.runqueuePushBack +func scheduleTask(*Task) + +// setReturnPtr is used by the compiler to store the return buffer into the task. +// This buffer is where the return value of a function that is about to be called will be stored. +func (t *Task) setReturnPtr(buf unsafe.Pointer) { + t.Ptr = buf +} + +// getReturnPtr is used by the compiler to get the return buffer stored into the task. +// This is called at the beginning of an async function, and the return is stored into this buffer immediately before resuming the caller. +func (t *Task) getReturnPtr() unsafe.Pointer { + return t.Ptr +} + +// createTask returns a new task struct initialized with a no-op state. +func createTask() *Task { + return &Task{ + state: state{noopState()}, + } +} + +// start invokes a function in a new goroutine. Calls to this are inserted by the compiler. +// The created goroutine starts running immediately. +// This is implemented inside the compiler. +func start(fn uintptr, args unsafe.Pointer) + +// Current returns the current active task. +// This is implemented inside the compiler. +func Current() *Task + +// Pause suspends the current running task. +// This is implemented inside the compiler. +func Pause() + +type taskHolder interface { + setState(*rawState) *rawState + returnTo(*rawState) + returnCurrent() + setReturnPtr(unsafe.Pointer) + getReturnPtr() unsafe.Pointer +} + +// If there are no direct references to the task methods, they will not be discovered by the compiler, and this will trigger a compiler error. +// Instantiating this interface forces discovery of these methods. +var _ = taskHolder((*Task)(nil)) + +func fake() { + // Hack to ensure intrinsics are discovered. + Current() + go func() {}() + Pause() +} diff --git a/src/internal/task/task_none.go b/src/internal/task/task_none.go new file mode 100644 index 000000000..31835892c --- /dev/null +++ b/src/internal/task/task_none.go @@ -0,0 +1,29 @@ +// +build scheduler.none + +package task + +import "unsafe" + +//go:linkname runtimePanic runtime.runtimePanic +func runtimePanic(str string) + +func Pause() { + runtimePanic("scheduler is disabled") +} + +func Current() *Task { + runtimePanic("scheduler is disabled") + return nil +} + +//go:noinline +func start(fn uintptr, args unsafe.Pointer) { + // The compiler will error if this is reachable. + runtimePanic("scheduler is disabled") +} + +type state struct{} + +func (t *Task) Resume() { + runtimePanic("scheduler is disabled") +} diff --git a/src/internal/task/task_stack.go b/src/internal/task/task_stack.go new file mode 100644 index 000000000..0bd2f4c29 --- /dev/null +++ b/src/internal/task/task_stack.go @@ -0,0 +1,74 @@ +// +build scheduler.tasks + +package task + +import "unsafe" + +//go:linkname runtimePanic runtime.runtimePanic +func runtimePanic(str string) + +// Stack canary, to detect a stack overflow. The number is a random number +// generated by random.org. The bit fiddling dance is necessary because +// otherwise Go wouldn't allow the cast to a smaller integer size. +const stackCanary = uintptr(uint64(0x670c1333b83bf575) & uint64(^uintptr(0))) + +// state is a structure which holds a reference to the state of the task. +// When the task is suspended, the registers are stored onto the stack and the stack pointer is stored into sp. +type state struct { + // sp is the stack pointer of the saved state. + // When the task is inactive, the saved registers are stored at the top of the stack. + sp uintptr + + // canaryPtr points to the top word of the stack (the lowest address). + // This is used to detect stack overflows. + // When initializing the goroutine, the stackCanary constant is stored there. + // If the stack overflowed, the word will likely no longer equal stackCanary. + canaryPtr *uintptr +} + +// currentTask is the current running task, or nil if currently in the scheduler. +var currentTask *Task + +// Current returns the current active task. +func Current() *Task { + return currentTask +} + +// Pause suspends the current task and returns to the scheduler. +// This function may only be called when running on a goroutine stack, not when running on the system stack or in an interrupt. +func Pause() { + // Check whether the canary (the lowest address of the stack) is still + // valid. If it is not, a stack overflow has occured. + if *currentTask.state.canaryPtr != stackCanary { + runtimePanic("goroutine stack overflow") + } + currentTask.state.pause() +} + +// Resume the task until it pauses or completes. +// This may only be called from the scheduler. +func (t *Task) Resume() { + currentTask = t + t.state.resume() + currentTask = nil +} + +// initialize the state and prepare to call the specified function with the specified argument bundle. +func (s *state) initialize(fn uintptr, args unsafe.Pointer) { + // Create a stack. + stack := make([]uintptr, stackSize/unsafe.Sizeof(uintptr(0))) + + // Invoke architecture-specific initialization. + s.archInit(stack, fn, args) +} + +//go:linkname runqueuePushBack runtime.runqueuePushBack +func runqueuePushBack(*Task) + +// start creates and starts a new goroutine with the given function and arguments. +// The new goroutine is scheduled to run later. +func start(fn uintptr, args unsafe.Pointer) { + t := &Task{} + t.state.initialize(fn, args) + runqueuePushBack(t) +} diff --git a/src/internal/task/task_stack_cortexm.go b/src/internal/task/task_stack_cortexm.go new file mode 100644 index 000000000..b91e5f041 --- /dev/null +++ b/src/internal/task/task_stack_cortexm.go @@ -0,0 +1,83 @@ +// +build scheduler.tasks, cortexm + +package task + +import "unsafe" + +const stackSize = 1024 + +// calleeSavedRegs is the list of registers that must be saved and restored when +// switching between tasks. Also see scheduler_cortexm.S that relies on the +// exact layout of this struct. +type calleeSavedRegs struct { + r4 uintptr + r5 uintptr + r6 uintptr + r7 uintptr + r8 uintptr + r9 uintptr + r10 uintptr + r11 uintptr + + pc uintptr +} + +// registers gets a pointer to the registers stored at the top of the stack. +func (s *state) registers() *calleeSavedRegs { + return (*calleeSavedRegs)(unsafe.Pointer(s.sp)) +} + +// startTask is a small wrapper function that sets up the first (and only) +// argument to the new goroutine and makes sure it is exited when the goroutine +// finishes. +//go:extern tinygo_startTask +var startTask [0]uint8 + +// archInit runs architecture-specific setup for the goroutine startup. +func (s *state) archInit(stack []uintptr, fn uintptr, args unsafe.Pointer) { + // Set up the stack canary, a random number that should be checked when + // switching from the task back to the scheduler. The stack canary pointer + // points to the first word of the stack. If it has changed between now and + // the next stack switch, there was a stack overflow. + s.canaryPtr = &stack[0] + *s.canaryPtr = stackCanary + + // Store the initial sp for the startTask function (implemented in assembly). + s.sp = uintptr(unsafe.Pointer(&stack[uintptr(len(stack))-(unsafe.Sizeof(calleeSavedRegs{})/unsafe.Sizeof(uintptr(0)))])) + + // Initialize the registers. + // These will be popped off of the stack on the first resume of the goroutine. + r := s.registers() + + // Start the function at tinygo_startTask (defined in src/runtime/scheduler_cortexm.S). + // This assembly code calls a function (passed in r4) with a single argument (passed in r5). + // After the function returns, it calls Pause(). + r.pc = uintptr(unsafe.Pointer(&startTask)) + + // Pass the function to call in r4. + // This function is a compiler-generated wrapper which loads arguments out of a struct pointer. + // See createGoroutineStartWrapper (defined in compiler/goroutine.go) for more information. + r.r4 = fn + + // Pass the pointer to the arguments struct in r5. + r.r5 = uintptr(args) +} + +func (s *state) resume() { + switchToTask(s.sp) +} + +//export tinygo_switchToTask +func switchToTask(uintptr) + +//export tinygo_switchToScheduler +func switchToScheduler(*uintptr) + +func (s *state) pause() { + switchToScheduler(&s.sp) +} + +//export tinygo_pause +func pause() { + Pause() +} diff --git a/src/runtime/chan.go b/src/runtime/chan.go index a4cb47484..95243136d 100644 --- a/src/runtime/chan.go +++ b/src/runtime/chan.go @@ -24,6 +24,7 @@ package runtime // element of the receiving coroutine and setting the 'comma-ok' value to false. import ( + "internal/task" "unsafe" ) @@ -46,7 +47,7 @@ type channelBlockedList struct { // If this channel operation is not part of a select, then the pointer field of the state holds the data buffer. // If this channel operation is part of a select, then the pointer field of the state holds the recieve buffer. // If this channel operation is a receive, then the data field should be set to zero when resuming due to channel closure. - t *task + t *task.Task // s is a pointer to the channel select state corresponding to this operation. // This will be nil if and only if this channel operation is not part of a select statement. @@ -141,24 +142,24 @@ func (ch *channel) resumeRX(ok bool) unsafe.Pointer { b, ch.blocked = ch.blocked, ch.blocked.next // get destination pointer - dst := b.t.state().ptr + dst := b.t.Ptr if !ok { // the result value is zero memzero(dst, ch.elementSize) - b.t.state().data = 0 + b.t.Data = 0 } if b.s != nil { // tell the select op which case resumed - b.t.state().ptr = unsafe.Pointer(b.s) + b.t.Ptr = unsafe.Pointer(b.s) // detach associated operations b.detach() } // push task onto runqueue - runqueuePushBack(b.t) + runqueue.Push(b.t) return dst } @@ -171,21 +172,21 @@ func (ch *channel) resumeTX() unsafe.Pointer { b, ch.blocked = ch.blocked, ch.blocked.next // get source pointer - src := b.t.state().ptr + src := b.t.Ptr if b.s != nil { // use state's source pointer src = b.s.value // tell the select op which case resumed - b.t.state().ptr = unsafe.Pointer(b.s) + b.t.Ptr = unsafe.Pointer(b.s) // detach associated operations b.detach() } // push task onto runqueue - runqueuePushBack(b.t) + runqueue.Push(b.t) return src } @@ -424,17 +425,16 @@ func chanSend(ch *channel, value unsafe.Pointer) { } // wait for reciever - sender := getCoroutine() + sender := task.Current() ch.state = chanStateSend - senderState := sender.state() - senderState.ptr = value + sender.Ptr = value ch.blocked = &channelBlockedList{ next: ch.blocked, t: sender, } chanDebug(ch) - yield() - senderState.ptr = nil + task.Pause() + sender.Ptr = nil } // chanRecv receives a single value over a channel. @@ -454,18 +454,17 @@ func chanRecv(ch *channel, value unsafe.Pointer) bool { } // wait for a value - receiver := getCoroutine() + receiver := task.Current() ch.state = chanStateRecv - receiverState := receiver.state() - receiverState.ptr, receiverState.data = value, 1 + receiver.Ptr, receiver.Data = value, 1 ch.blocked = &channelBlockedList{ next: ch.blocked, t: receiver, } chanDebug(ch) - yield() - ok := receiverState.data == 1 - receiverState.ptr, receiverState.data = nil, 0 + task.Pause() + ok := receiver.Data == 1 + receiver.Ptr, receiver.Data = nil, 0 return ok } @@ -515,7 +514,7 @@ func chanSelect(recvbuf unsafe.Pointer, states []chanSelectState, ops []channelB for i, v := range states { ops[i] = channelBlockedList{ next: v.ch.blocked, - t: getCoroutine(), + t: task.Current(), s: &states[i], allSelectOps: ops, } @@ -547,14 +546,15 @@ func chanSelect(recvbuf unsafe.Pointer, states []chanSelectState, ops []channelB } // expose rx buffer - getCoroutine().state().ptr = recvbuf - getCoroutine().state().data = 1 + t := task.Current() + t.Ptr = recvbuf + t.Data = 1 // wait for one case to fire - yield() + task.Pause() // figure out which one fired and return the ok value - return (uintptr(getCoroutine().state().ptr) - uintptr(unsafe.Pointer(&states[0]))) / unsafe.Sizeof(chanSelectState{}), getCoroutine().state().data != 0 + return (uintptr(t.Ptr) - uintptr(unsafe.Pointer(&states[0]))) / unsafe.Sizeof(chanSelectState{}), t.Data != 0 } // tryChanSelect is like chanSelect, but it does a non-blocking select operation. diff --git a/src/runtime/runtime.go b/src/runtime/runtime.go index 13f3ab3c9..ae9ab665f 100644 --- a/src/runtime/runtime.go +++ b/src/runtime/runtime.go @@ -10,17 +10,8 @@ const Compiler = "tinygo" // package. func initAll() -// A function call to this function is replaced with one of the following, -// depending on whether the scheduler is necessary: -// -// Without scheduler: -// -// main.main() -// -// With scheduler: -// -// main.main() -// scheduler() +// callMain is a placeholder for the program main function. +// All references to this are replaced with references to the program main function by the compiler. func callMain() func GOMAXPROCS(n int) int { diff --git a/src/runtime/runtime_arm7tdmi.go b/src/runtime/runtime_arm7tdmi.go index cff5bca36..cf2ff6818 100644 --- a/src/runtime/runtime_arm7tdmi.go +++ b/src/runtime/runtime_arm7tdmi.go @@ -40,7 +40,10 @@ func main() { initAll() // Compiler-generated call to main.main(). - callMain() + go callMain() + + // Run the scheduler. + scheduler() } func preinit() { diff --git a/src/runtime/runtime_atsamd21.go b/src/runtime/runtime_atsamd21.go index 440cd148e..a3aa6c973 100644 --- a/src/runtime/runtime_atsamd21.go +++ b/src/runtime/runtime_atsamd21.go @@ -17,7 +17,8 @@ type timeUnit int64 func main() { preinit() initAll() - callMain() + go callMain() + scheduler() abort() } diff --git a/src/runtime/runtime_atsamd51.go b/src/runtime/runtime_atsamd51.go index 7cbcad044..23d91109c 100644 --- a/src/runtime/runtime_atsamd51.go +++ b/src/runtime/runtime_atsamd51.go @@ -16,7 +16,8 @@ type timeUnit int64 func main() { preinit() initAll() - callMain() + go callMain() + scheduler() abort() } diff --git a/src/runtime/runtime_cortexm.go b/src/runtime/runtime_cortexm.go index 147a3b39f..04b34039a 100644 --- a/src/runtime/runtime_cortexm.go +++ b/src/runtime/runtime_cortexm.go @@ -40,29 +40,6 @@ func preinit() { } } -// calleeSavedRegs is the list of registers that must be saved and restored when -// switching between tasks. Also see scheduler_cortexm.S that relies on the -// exact layout of this struct. -type calleeSavedRegs struct { - r4 uintptr - r5 uintptr - r6 uintptr - r7 uintptr - r8 uintptr - r9 uintptr - r10 uintptr - r11 uintptr -} - -// prepareStartTask stores fn and args in some callee-saved registers that can -// then be used by the startTask function (implemented in assembly) to set up -// the initial stack pointer and initial argument with the pointer to the object -// with the goroutine start arguments. -func (r *calleeSavedRegs) prepareStartTask(fn, args uintptr) { - r.r4 = fn - r.r5 = args -} - func abort() { // disable all interrupts arm.DisableInterrupts() diff --git a/src/runtime/runtime_cortexm_qemu.go b/src/runtime/runtime_cortexm_qemu.go index f1f95caad..973c20ac2 100644 --- a/src/runtime/runtime_cortexm_qemu.go +++ b/src/runtime/runtime_cortexm_qemu.go @@ -21,7 +21,8 @@ var timestamp timeUnit func main() { preinit() initAll() - callMain() + go callMain() + scheduler() arm.SemihostingCall(arm.SemihostingReportException, arm.SemihostingApplicationExit) abort() } diff --git a/src/runtime/runtime_fe310.go b/src/runtime/runtime_fe310.go index 92cb9ad17..88295c9c0 100644 --- a/src/runtime/runtime_fe310.go +++ b/src/runtime/runtime_fe310.go @@ -52,7 +52,8 @@ func main() { preinit() initPeripherals() initAll() - callMain() + go callMain() + scheduler() abort() } diff --git a/src/runtime/runtime_nrf.go b/src/runtime/runtime_nrf.go index 50d3e5f2b..9b01ded70 100644 --- a/src/runtime/runtime_nrf.go +++ b/src/runtime/runtime_nrf.go @@ -22,7 +22,8 @@ func main() { systemInit() preinit() initAll() - callMain() + go callMain() + scheduler() abort() } diff --git a/src/runtime/runtime_stm32.go b/src/runtime/runtime_stm32.go index 0717e7cff..11e686f06 100644 --- a/src/runtime/runtime_stm32.go +++ b/src/runtime/runtime_stm32.go @@ -8,6 +8,7 @@ type timeUnit int64 func main() { preinit() initAll() - callMain() + go callMain() + scheduler() abort() } diff --git a/src/runtime/runtime_unix.go b/src/runtime/runtime_unix.go index 63d78917e..a88f243f7 100644 --- a/src/runtime/runtime_unix.go +++ b/src/runtime/runtime_unix.go @@ -52,7 +52,10 @@ func main() int { initAll() // Compiler-generated call to main.main(). - callMain() + go callMain() + + // Run scheduler. + scheduler() // For libc compatibility. return 0 diff --git a/src/runtime/runtime_wasm.go b/src/runtime/runtime_wasm.go index 09216cc3f..c01fa9326 100644 --- a/src/runtime/runtime_wasm.go +++ b/src/runtime/runtime_wasm.go @@ -21,7 +21,8 @@ func fd_write(id uint32, iovs *wasiIOVec, iovs_len uint, nwritten *uint) (errno //export _start func _start() { initAll() - callMain() + go callMain() + scheduler() } // Using global variables to avoid heap allocation. @@ -50,7 +51,9 @@ func setEventHandler(fn func()) { //go:export resume func resume() { - handleEvent() + go func() { + handleEvent() + }() } //go:export go_scheduler diff --git a/src/runtime/scheduler.go b/src/runtime/scheduler.go index 7dc2b7906..49cf8e402 100644 --- a/src/runtime/scheduler.go +++ b/src/runtime/scheduler.go @@ -14,27 +14,16 @@ package runtime // of the coroutine-based scheduler, it is the coroutine pointer (a *i8 in // LLVM). -import "unsafe" +import ( + "internal/task" +) const schedulerDebug = false -// State of a task. Internally represented as: -// -// {i8* next, i8* ptr, i32/i64 data} -type taskState struct { - next *task - ptr unsafe.Pointer - data uint -} - // Queues used by the scheduler. -// -// TODO: runqueueFront can be removed by making the run queue a circular linked -// list. The runqueueBack will simply refer to the front in the 'next' pointer. var ( - runqueueFront *task - runqueueBack *task - sleepQueue *task + runqueue task.Queue + sleepQueue *task.Task sleepQueueBaseTime timeUnit ) @@ -46,14 +35,14 @@ func scheduleLog(msg string) { } // Simple logging with a task pointer, for debugging. -func scheduleLogTask(msg string, t *task) { +func scheduleLogTask(msg string, t *task.Task) { if schedulerDebug { println("---", msg, t) } } // Simple logging with a channel and task pointer. -func scheduleLogChan(msg string, ch *channel, t *task) { +func scheduleLogChan(msg string, ch *channel, t *task.Task) { if schedulerDebug { println("---", msg, ch, t) } @@ -67,7 +56,7 @@ func scheduleLogChan(msg string, ch *channel, t *task) { //go:noinline func deadlock() { // call yield without requesting a wakeup - yield() + task.Pause() panic("unreachable") } @@ -80,122 +69,20 @@ func Goexit() { deadlock() } -// unblock unblocks a task and returns the next value -func unblock(t *task) *task { - state := t.state() - next := state.next - state.next = nil - activateTask(t) - return next -} - -// unblockChain unblocks the next task on the stack/queue, returning it -// also updates the chain, putting the next element into the chain pointer -// if the chain is used as a queue, tail is used as a pointer to the final insertion point -// if the chain is used as a stack, tail should be nil -func unblockChain(chain **task, tail ***task) *task { - t := *chain - if t == nil { - return nil - } - *chain = unblock(t) - if tail != nil && *chain == nil { - *tail = chain - } - return t -} - -// dropChain drops a task from the given stack or queue -// if the chain is used as a queue, tail is used as a pointer to the field containing a pointer to the next insertion point -// if the chain is used as a stack, tail should be nil -func dropChain(t *task, chain **task, tail ***task) { - for c := chain; *c != nil; c = &((*c).state().next) { - if *c == t { - next := (*c).state().next - if next == nil && tail != nil { - *tail = c - } - *c = next - return - } - } - panic("runtime: task not in chain") -} - -// Pause the current task for a given time. -//go:linkname sleep time.Sleep -func sleep(duration int64) { - addSleepTask(getCoroutine(), duration) - yield() -} - -func avrSleep(duration int64) { - sleepTicks(timeUnit(duration / tickMicros)) -} - -// Add a non-queued task to the run queue. -// -// This is a compiler intrinsic, and is called from a callee to reactivate the -// caller. -func activateTask(t *task) { - if t == nil { - return - } - scheduleLogTask(" set runnable:", t) - runqueuePushBack(t) -} - -// getTaskStateData is a helper function to get the current .data field of the -// goroutine state. -//go:inline -func getTaskStateData(t *task) uint { - return t.state().data -} - -// Add this task to the end of the run queue. May also destroy the task if it's -// done. -func runqueuePushBack(t *task) { - if schedulerDebug { - scheduleLogTask(" pushing back:", t) - if t.state().next != nil { - panic("runtime: runqueuePushBack: expected next task to be nil") - } - } - if runqueueBack == nil { // empty runqueue - runqueueBack = t - runqueueFront = t - } else { - lastTaskState := runqueueBack.state() - lastTaskState.next = t - runqueueBack = t - } -} - -// Get a task from the front of the run queue. Returns nil if there is none. -func runqueuePopFront() *task { - t := runqueueFront - if t == nil { - return nil - } - state := t.state() - runqueueFront = state.next - if runqueueFront == nil { - // Runqueue is empty now. - runqueueBack = nil - } - state.next = nil - return t +// Add this task to the end of the run queue. +func runqueuePushBack(t *task.Task) { + runqueue.Push(t) } // Add this task to the sleep queue, assuming its state is set to sleeping. -func addSleepTask(t *task, duration int64) { +func addSleepTask(t *task.Task, duration int64) { if schedulerDebug { println(" set sleep:", t, uint(duration/tickMicros)) - if t.state().next != nil { + if t.Next != nil { panic("runtime: addSleepTask: expected next task to be nil") } } - t.state().data = uint(duration / tickMicros) // TODO: longer durations + t.Data = uint(duration / tickMicros) // TODO: longer durations now := ticks() if sleepQueue == nil { scheduleLog(" -> sleep new queue") @@ -206,20 +93,20 @@ func addSleepTask(t *task, duration int64) { // Add to sleep queue. q := &sleepQueue - for ; *q != nil; q = &((*q).state()).next { - if t.state().data < (*q).state().data { + for ; *q != nil; q = &(*q).Next { + if t.Data < (*q).Data { // this will finish earlier than the next - insert here break } else { // this will finish later - adjust delay - t.state().data -= (*q).state().data + t.Data -= (*q).Data } } if *q != nil { // cut delay time between this sleep task and the next - (*q).state().data -= t.state().data + (*q).Data -= t.Data } - t.state().next = *q + t.Next = *q *q = t } @@ -236,17 +123,16 @@ func scheduler() { // Add tasks that are done sleeping to the end of the runqueue so they // will be executed soon. - if sleepQueue != nil && now-sleepQueueBaseTime >= timeUnit(sleepQueue.state().data) { + if sleepQueue != nil && now-sleepQueueBaseTime >= timeUnit(sleepQueue.Data) { t := sleepQueue scheduleLogTask(" awake:", t) - state := t.state() - sleepQueueBaseTime += timeUnit(state.data) - sleepQueue = state.next - state.next = nil - runqueuePushBack(t) + sleepQueueBaseTime += timeUnit(t.Data) + sleepQueue = t.Next + t.Next = nil + runqueue.Push(t) } - t := runqueuePopFront() + t := runqueue.Pop() if t == nil { if sleepQueue == nil { // No more tasks to execute. @@ -256,11 +142,11 @@ func scheduler() { scheduleLog(" no tasks left!") return } - timeLeft := timeUnit(sleepQueue.state().data) - (now - sleepQueueBaseTime) + timeLeft := timeUnit(sleepQueue.Data) - (now - sleepQueueBaseTime) if schedulerDebug { println(" sleeping...", sleepQueue, uint(timeLeft)) - for t := sleepQueue; t != nil; t = t.state().next { - println(" task sleeping:", t, timeUnit(t.state().data)) + for t := sleepQueue; t != nil; t = t.Next { + println(" task sleeping:", t, timeUnit(t.Data)) } } sleepTicks(timeLeft) @@ -275,11 +161,11 @@ func scheduler() { // Run the given task. scheduleLogTask(" run:", t) - t.resume() + t.Resume() } } func Gosched() { - runqueuePushBack(getCoroutine()) - yield() + runqueue.Push(task.Current()) + task.Pause() } diff --git a/src/runtime/scheduler_any.go b/src/runtime/scheduler_any.go new file mode 100644 index 000000000..d541dc6a3 --- /dev/null +++ b/src/runtime/scheduler_any.go @@ -0,0 +1,12 @@ +// +build !scheduler.none + +package runtime + +import "internal/task" + +// Pause the current task for a given time. +//go:linkname sleep time.Sleep +func sleep(duration int64) { + addSleepTask(task.Current(), duration) + task.Pause() +} diff --git a/src/runtime/scheduler_coroutines.go b/src/runtime/scheduler_coroutines.go index 1d0996d94..5e871a64a 100644 --- a/src/runtime/scheduler_coroutines.go +++ b/src/runtime/scheduler_coroutines.go @@ -2,103 +2,8 @@ package runtime -// This file implements the Go scheduler using coroutines. -// A goroutine contains a whole stack. A coroutine is just a single function. -// How do we use coroutines for goroutines, then? -// * Every function that contains a blocking call (like sleep) is marked -// blocking, and all it's parents (callers) are marked blocking as well -// transitively until the root (main.main or a go statement). -// * A blocking function that calls a non-blocking function is called as -// usual. -// * A blocking function that calls a blocking function passes its own -// coroutine handle as a parameter to the subroutine. When the subroutine -// returns, it will re-insert the parent into the scheduler. -// Note that we use the type 'task' to refer to a coroutine, for compatibility -// with the task-based scheduler. A task type here does not represent the whole -// task, but just the topmost coroutine. For most of the scheduler, this -// difference doesn't matter. -// -// For more background on coroutines in LLVM: -// https://llvm.org/docs/Coroutines.html - -import "unsafe" - -// A coroutine instance, wrapped here to provide some type safety. The value -// must not be used directly, it is meant to be used as an opaque *i8 in LLVM. -type task uint8 - -//go:export llvm.coro.resume -func (t *task) resume() - -//go:export llvm.coro.destroy -func (t *task) destroy() - -//go:export llvm.coro.done -func (t *task) done() bool - -//go:export llvm.coro.promise -func (t *task) _promise(alignment int32, from bool) unsafe.Pointer - -// Get the state belonging to a task. -func (t *task) state() *taskState { - return (*taskState)(t._promise(int32(unsafe.Alignof(taskState{})), false)) -} - -func makeGoroutine(uintptr) uintptr - -// Compiler stub to get the current goroutine. Calls to this function are -// removed in the goroutine lowering pass. -func getCoroutine() *task - -// setTaskStatePtr is a helper function to set the current .ptr field of a -// coroutine promise. -func setTaskStatePtr(t *task, value unsafe.Pointer) { - t.state().ptr = value -} - -// getTaskStatePtr is a helper function to get the current .ptr field from a -// coroutine promise. -func getTaskStatePtr(t *task) unsafe.Pointer { - if t == nil { - blockingPanic() - } - return t.state().ptr -} - -// yield suspends execution of the current goroutine -// any wakeups must be configured before calling yield -func yield() - // getSystemStackPointer returns the current stack pointer of the system stack. // This is always the current stack pointer. func getSystemStackPointer() uintptr { return getCurrentStackPointer() } - -func fakeCoroutine(dst **task) { - *dst = getCoroutine() - for { - yield() - } -} - -func getFakeCoroutine() *task { - // this isnt defined behavior, but this is what our implementation does - // this is really a horrible hack - var t *task - go fakeCoroutine(&t) - - // the first line of fakeCoroutine will have completed by now - return t -} - -// noret is a placeholder that can be used to indicate that an async function is not going to directly return here -func noret() - -func getParentHandle() *task - -func llvmCoroRefHolder() { - noret() - getParentHandle() - getCoroutine() -} diff --git a/src/runtime/scheduler_cortexm.S b/src/runtime/scheduler_cortexm.S index 6d83ba4f3..121992e7d 100644 --- a/src/runtime/scheduler_cortexm.S +++ b/src/runtime/scheduler_cortexm.S @@ -17,7 +17,7 @@ tinygo_startTask: blx r4 // After return, exit this goroutine. This is a tail call. - bl runtime.yield + bl tinygo_pause .section .text.tinygo_getSystemStackPointer .global tinygo_getSystemStackPointer @@ -35,24 +35,23 @@ tinygo_getSystemStackPointer: .global tinygo_switchToScheduler .type tinygo_switchToScheduler, %function tinygo_switchToScheduler: - // r0 = oldTask *task + // r0 = sp *uintptr // Currently on the task stack (SP=PSP). We need to store the position on // the stack where the in-use registers will be stored. mov r1, sp subs r1, #36 - str r1, [r0, #36] + str r1, [r0] b tinygo_swapTask .global tinygo_switchToTask .type tinygo_switchToTask, %function tinygo_switchToTask: - // r0 = newTask *task + // r0 = sp uintptr // Currently on the scheduler stack (SP=MSP). We'll have to update the PSP, // and then we can invoke swapTask. - ldr r0, [r0, #36] msr PSP, r0 // Continue executing in the swapTask function, which swaps the stack diff --git a/src/runtime/scheduler_none.go b/src/runtime/scheduler_none.go new file mode 100644 index 000000000..222867dd0 --- /dev/null +++ b/src/runtime/scheduler_none.go @@ -0,0 +1,14 @@ +// +build scheduler.none + +package runtime + +//go:linkname sleep time.Sleep +func sleep(duration int64) { + sleepTicks(timeUnit(duration / tickMicros)) +} + +// getSystemStackPointer returns the current stack pointer of the system stack. +// This is always the current stack pointer. +func getSystemStackPointer() uintptr { + return getCurrentStackPointer() +} diff --git a/src/runtime/scheduler_tasks.go b/src/runtime/scheduler_tasks.go index b2142892e..9be63d5d5 100644 --- a/src/runtime/scheduler_tasks.go +++ b/src/runtime/scheduler_tasks.go @@ -2,110 +2,6 @@ package runtime -import "unsafe" - -const stackSize = 1024 - -// Stack canary, to detect a stack overflow. The number is a random number -// generated by random.org. The bit fiddling dance is necessary because -// otherwise Go wouldn't allow the cast to a smaller integer size. -const stackCanary = uintptr(uint64(0x670c1333b83bf575) & uint64(^uintptr(0))) - -var ( - currentTask *task // currently running goroutine, or nil -) - -// This type points to the bottom of the goroutine stack and contains some state -// that must be kept with the task. The last field is a canary, which is -// necessary to make sure that no stack overflow occured when switching tasks. -type task struct { - // The order of fields in this structs must be kept in sync with assembly! - calleeSavedRegs - pc uintptr - sp uintptr - taskState - canaryPtr *uintptr // used to detect stack overflows -} - -// getCoroutine returns the currently executing goroutine. It is used as an -// intrinsic when compiling channel operations, but is not necessary with the -// task-based scheduler. -//go:inline -func getCoroutine() *task { - return currentTask -} - -// state is a small helper that returns the task state, and is provided for -// compatibility with the coroutine implementation. -//go:inline -func (t *task) state() *taskState { - return &t.taskState -} - -// resume is a small helper that resumes this task until this task switches back -// to the scheduler. -func (t *task) resume() { - currentTask = t - switchToTask(t) - currentTask = nil -} - -// switchToScheduler saves the current state on the stack, saves the current -// stack pointer in the task, and switches to the scheduler. It must only be -// called when actually running on this task. -// When it returns, the scheduler has switched back to this task (for example, -// after a blocking operation completed). -//export tinygo_switchToScheduler -func switchToScheduler(t *task) - -// switchToTask switches from the scheduler to the task. It must only be called -// from the scheduler. -// When this function returns, the task just yielded control back to the -// scheduler. -//export tinygo_switchToTask -func switchToTask(t *task) - -// startTask is a small wrapper function that sets up the first (and only) -// argument to the new goroutine and makes sure it is exited when the goroutine -// finishes. -//go:extern tinygo_startTask -var startTask [0]uint8 - -// startGoroutine starts a new goroutine with the given function pointer and -// argument. It creates a new goroutine stack, prepares it for execution, and -// adds it to the runqueue. -func startGoroutine(fn, args uintptr) { - stack := alloc(stackSize) - t := (*task)(unsafe.Pointer(uintptr(stack) + stackSize - unsafe.Sizeof(task{}))) - - // Set up the stack canary, a random number that should be checked when - // switching from the task back to the scheduler. The stack canary pointer - // points to the first word of the stack. If it has changed between now and - // the next stack switch, there was a stack overflow. - t.canaryPtr = (*uintptr)(unsafe.Pointer(stack)) - *t.canaryPtr = stackCanary - - // Store the initial sp/pc for the startTask function (implemented in - // assembly). - t.sp = uintptr(stack) + stackSize - unsafe.Sizeof(task{}) - t.pc = uintptr(unsafe.Pointer(&startTask)) - t.prepareStartTask(fn, args) - scheduleLogTask(" start goroutine:", t) - runqueuePushBack(t) -} - -// yield suspends execution of the current goroutine -// any wakeups must be configured before calling yield -//export runtime.yield -func yield() { - // Check whether the canary (the lowest address of the stack) is still - // valid. If it is not, a stack overflow has occured. - if *currentTask.canaryPtr != stackCanary { - runtimePanic("goroutine stack overflow") - } - switchToScheduler(currentTask) -} - // getSystemStackPointer returns the current stack pointer of the system stack. // This is not necessarily the same as the current stack pointer. //export tinygo_getSystemStackPointer |