all: add support for multicore scheduler

This commit adds support for a scheduler that runs a scheduler on all
available cores. It is meant to be used on baremetal systems with a
fixed number of cores, such as the RP2040.

The initial implementation adds support for multicore scheduling to the
riscv-qemu target as a convenient testing target. This means that this
new multicore scheduler is tested in CI, including a bunch of standard
library tests (`make tinygo-test-baremetal`). This should ensure the new
scheduler is reasonably well tested before trying to use it on
harder-to-debug targets like the RP2040.
This commit is contained in:
Ayke van Laethem
2025-04-03 13:29:55 +02:00
committed by Ron Evans
parent 0c7c2926f9
commit 60f8a62978
35 changed files with 1237 additions and 160 deletions
+101
View File
@@ -0,0 +1,101 @@
//go:build scheduler.cores
package runtime
import (
"internal/task"
"sync/atomic"
)
// Normally 0. During a GC scan it has various purposes for signalling between
// the core running the GC and the other cores in the system.
var gcScanState atomic.Uint32
// Start GC scan by pausing the world (all other cores) and scanning their
// stacks. It doesn't resume the world.
func gcMarkReachable() {
core := currentCPU()
// Interrupt all other cores.
gcScanState.Store(1)
for i := uint32(0); i < numCPU; i++ {
if i == core {
continue
}
gcPauseCore(i)
}
// Scan the stack(s) of the current core.
scanCurrentStack()
if !task.OnSystemStack() {
// Mark system stack.
markRoots(task.SystemStack(), coreStackTop(core))
}
// Scan globals.
findGlobals(markRoots)
// Busy-wait until all the other cores are ready. They certainly should be,
// after the scanning we did above.
for gcScanState.Load() != numCPU {
spinLoopHint()
}
gcScanState.Store(0)
// Signal each core in turn that they can scan the stack.
for i := uint32(0); i < numCPU; i++ {
if i == core {
continue
}
// Wake up the core to scan the stack.
gcSignalCore(i)
// Busy-wait until this core finished scanning.
for gcScanState.Load() == 0 {
spinLoopHint()
}
gcScanState.Store(0)
}
// All the stack are now scanned.
}
//go:export tinygo_scanCurrentStack
func scanCurrentStack()
//go:export tinygo_scanstack
func scanstack(sp uintptr) {
// Mark the current stack.
// This function is called by scanCurrentStack, after pushing all registers
// onto the stack.
if task.OnSystemStack() {
// This is the system stack.
// Scan all words on the stack.
markRoots(sp, coreStackTop(currentCPU()))
} else {
// This is a goroutine stack.
markCurrentGoroutineStack(sp)
}
}
// Resume the world after a call to gcMarkReachable.
func gcResumeWorld() {
// Signal each core that they can resume.
hartID := currentCPU()
for i := uint32(0); i < numCPU; i++ {
if i == hartID {
continue
}
// Signal the core.
gcSignalCore(i)
}
// Busy-wait until the core acknowledges the signal (and is going to return
// from the interrupt handler).
for gcScanState.Load() != numCPU-1 {
spinLoopHint()
}
gcScanState.Store(0)
}