Skip to content

Commit 7ab8d89

Browse files
committed
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.
1 parent befd1fb commit 7ab8d89

35 files changed

+1238
-161
lines changed

builder/sizes.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -490,7 +490,7 @@ func loadProgramSize(path string, packagePathMap map[string]string) (*programSiz
490490
continue
491491
}
492492
if section.Type == elf.SHT_NOBITS {
493-
if section.Name == ".stack" {
493+
if strings.HasPrefix(section.Name, ".stack") {
494494
// TinyGo emits stack sections on microcontroller using the
495495
// ".stack" name.
496496
// This is a bit ugly, but I don't think there is a way to

builder/sizes_test.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -42,7 +42,7 @@ func TestBinarySize(t *testing.T) {
4242
// This is a small number of very diverse targets that we want to test.
4343
tests := []sizeTest{
4444
// microcontrollers
45-
{"hifive1b", "examples/echo", 4556, 280, 0, 2268},
45+
{"hifive1b", "examples/echo", 4556, 280, 0, 2264},
4646
{"microbit", "examples/serial", 2920, 388, 8, 2272},
4747
{"wioterminal", "examples/pininterrupt", 7379, 1489, 116, 6912},
4848

compileopts/config.go

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -110,6 +110,11 @@ func (c *Config) BuildTags() []string {
110110
"math_big_pure_go", // to get math/big to work
111111
"gc." + c.GC(), "scheduler." + c.Scheduler(), // used inside the runtime package
112112
"serial." + c.Serial()}...) // used inside the machine package
113+
switch c.Scheduler() {
114+
case "threads", "cores":
115+
default:
116+
tags = append(tags, "tinygo.unicore")
117+
}
113118
for i := 1; i <= c.GoMinorVersion; i++ {
114119
tags = append(tags, fmt.Sprintf("go1.%d", i))
115120
}

compileopts/options.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,7 @@ import (
1010
var (
1111
validBuildModeOptions = []string{"default", "c-shared", "wasi-legacy"}
1212
validGCOptions = []string{"none", "leaking", "conservative", "custom", "precise", "boehm"}
13-
validSchedulerOptions = []string{"none", "tasks", "asyncify", "threads"}
13+
validSchedulerOptions = []string{"none", "tasks", "asyncify", "threads", "cores"}
1414
validSerialOptions = []string{"none", "uart", "usb", "rtt"}
1515
validPrintSizeOptions = []string{"none", "short", "full", "html"}
1616
validPanicStrategyOptions = []string{"print", "trap"}

compileopts/options_test.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,7 @@ import (
1010
func TestVerifyOptions(t *testing.T) {
1111

1212
expectedGCError := errors.New(`invalid gc option 'incorrect': valid values are none, leaking, conservative, custom, precise, boehm`)
13-
expectedSchedulerError := errors.New(`invalid scheduler option 'incorrect': valid values are none, tasks, asyncify, threads`)
13+
expectedSchedulerError := errors.New(`invalid scheduler option 'incorrect': valid values are none, tasks, asyncify, threads, cores`)
1414
expectedPrintSizeError := errors.New(`invalid size option 'incorrect': valid values are none, short, full, html`)
1515
expectedPanicStrategyError := errors.New(`invalid panic option 'incorrect': valid values are print, trap`)
1616

src/device/riscv/start.S

Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,8 +3,47 @@
33
.type _start,@function
44

55
_start:
6+
// If we're on a multicore system, we need to wait for hart 0 to wake us up.
7+
#if TINYGO_CORES > 1
8+
csrr a0, mhartid
9+
10+
// Hart 0 stack
11+
bnez a0, 1f
12+
la sp, _stack0_top
13+
14+
1:
15+
// Hart 1 stack
16+
li a1, 1
17+
bne a0, a1, 2f
18+
la sp, _stack1_top
19+
20+
2:
21+
// Hart 2 stack
22+
#if TINYGO_CORES >= 3
23+
li a1, 2
24+
bne a0, a1, 3f
25+
la sp, _stack2_top
26+
#endif
27+
28+
3:
29+
// Hart 3 stack
30+
#if TINYGO_CORES >= 4
31+
li a1, 3
32+
bne a0, a1, 4f
33+
la sp, _stack3_top
34+
#endif
35+
36+
4:
37+
// done
38+
39+
#if TINYGO_CORES > 4
40+
#error only up to 4 cores are supported at the moment!
41+
#endif
42+
43+
#else
644
// Load the stack pointer.
745
la sp, _stack_top
46+
#endif
847

948
// Load the globals pointer. The program will load pointers relative to this
1049
// register, so it must be set to the right value on startup.

src/internal/task/atomic-cooperative.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
//go:build !scheduler.threads
1+
//go:build tinygo.unicore
22

33
package task
44

src/internal/task/atomic-preemptive.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
//go:build scheduler.threads
1+
//go:build !tinygo.unicore
22

33
package task
44

src/internal/task/futex-cooperative.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
//go:build !scheduler.threads
1+
//go:build tinygo.unicore
22

33
package task
44

src/internal/task/futex-cores.go

Lines changed: 64 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,64 @@
1+
//go:build scheduler.cores
2+
3+
package task
4+
5+
import "runtime/interrupt"
6+
7+
// A futex is a way for userspace to wait with the pointer as the key, and for
8+
// another thread to wake one or all waiting threads keyed on the same pointer.
9+
//
10+
// A futex does not change the underlying value, it only reads it before to prevent
11+
// lost wake-ups.
12+
type Futex struct {
13+
Uint32
14+
15+
waiters Stack
16+
}
17+
18+
// Atomically check for cmp to still be equal to the futex value and if so, go
19+
// to sleep. Return true if we were definitely awoken by a call to Wake or
20+
// WakeAll, and false if we can't be sure of that.
21+
func (f *Futex) Wait(cmp uint32) (awoken bool) {
22+
mask := lockFutex()
23+
24+
if f.Uint32.Load() != cmp {
25+
unlockFutex(mask)
26+
return false
27+
}
28+
29+
// Push the current goroutine onto the waiter stack.
30+
f.waiters.Push(Current())
31+
32+
unlockFutex(mask)
33+
34+
// Pause until this task is awoken by Wake/WakeAll.
35+
Pause()
36+
37+
// We were awoken by a call to Wake or WakeAll. There is no chance for
38+
// spurious wakeups.
39+
return true
40+
}
41+
42+
// Wake a single waiter.
43+
func (f *Futex) Wake() {
44+
mask := lockFutex()
45+
if t := f.waiters.Pop(); t != nil {
46+
scheduleTask(t)
47+
}
48+
unlockFutex(mask)
49+
}
50+
51+
// Wake all waiters.
52+
func (f *Futex) WakeAll() {
53+
mask := lockFutex()
54+
for t := f.waiters.Pop(); t != nil; t = f.waiters.Pop() {
55+
scheduleTask(t)
56+
}
57+
unlockFutex(mask)
58+
}
59+
60+
//go:linkname lockFutex runtime.lockFutex
61+
func lockFutex() interrupt.State
62+
63+
//go:linkname unlockFutex runtime.unlockFutex
64+
func unlockFutex(interrupt.State)

src/internal/task/mutex-cooperative.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
//go:build !scheduler.threads
1+
//go:build tinygo.unicore
22

33
package task
44

src/internal/task/mutex-preemptive.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
//go:build scheduler.threads
1+
//go:build !tinygo.unicore
22

33
package task
44

src/internal/task/pmutex-cooperative.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
//go:build !scheduler.threads
1+
//go:build tinygo.unicore
22

33
package task
44

src/internal/task/pmutex-preemptive.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
//go:build scheduler.threads
1+
//go:build !tinygo.unicore
22

33
package task
44

src/internal/task/queue.go

Lines changed: 30 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -12,9 +12,9 @@ type Queue struct {
1212

1313
// Push a task onto the queue.
1414
func (q *Queue) Push(t *Task) {
15-
i := interrupt.Disable()
15+
mask := lockAtomics()
1616
if asserts && t.Next != nil {
17-
interrupt.Restore(i)
17+
unlockAtomics(mask)
1818
panic("runtime: pushing a task to a queue with a non-nil Next pointer")
1919
}
2020
if q.tail != nil {
@@ -25,44 +25,44 @@ func (q *Queue) Push(t *Task) {
2525
if q.head == nil {
2626
q.head = t
2727
}
28-
interrupt.Restore(i)
28+
unlockAtomics(mask)
2929
}
3030

3131
// Pop a task off of the queue.
3232
func (q *Queue) Pop() *Task {
33-
i := interrupt.Disable()
33+
mask := lockAtomics()
3434
t := q.head
3535
if t == nil {
36-
interrupt.Restore(i)
36+
unlockAtomics(mask)
3737
return nil
3838
}
3939
q.head = t.Next
4040
if q.tail == t {
4141
q.tail = nil
4242
}
4343
t.Next = nil
44-
interrupt.Restore(i)
44+
unlockAtomics(mask)
4545
return t
4646
}
4747

4848
// Append pops the contents of another queue and pushes them onto the end of this queue.
4949
func (q *Queue) Append(other *Queue) {
50-
i := interrupt.Disable()
50+
mask := lockAtomics()
5151
if q.head == nil {
5252
q.head = other.head
5353
} else {
5454
q.tail.Next = other.head
5555
}
5656
q.tail = other.tail
5757
other.head, other.tail = nil, nil
58-
interrupt.Restore(i)
58+
unlockAtomics(mask)
5959
}
6060

6161
// Empty checks if the queue is empty.
6262
func (q *Queue) Empty() bool {
63-
i := interrupt.Disable()
63+
mask := lockAtomics()
6464
empty := q.head == nil
65-
interrupt.Restore(i)
65+
unlockAtomics(mask)
6666
return empty
6767
}
6868

@@ -75,24 +75,24 @@ type Stack struct {
7575

7676
// Push a task onto the stack.
7777
func (s *Stack) Push(t *Task) {
78-
i := interrupt.Disable()
78+
mask := lockAtomics()
7979
if asserts && t.Next != nil {
80-
interrupt.Restore(i)
80+
unlockAtomics(mask)
8181
panic("runtime: pushing a task to a stack with a non-nil Next pointer")
8282
}
8383
s.top, t.Next = t, s.top
84-
interrupt.Restore(i)
84+
unlockAtomics(mask)
8585
}
8686

8787
// Pop a task off of the stack.
8888
func (s *Stack) Pop() *Task {
89-
i := interrupt.Disable()
89+
mask := lockAtomics()
9090
t := s.top
9191
if t != nil {
9292
s.top = t.Next
9393
t.Next = nil
9494
}
95-
interrupt.Restore(i)
95+
unlockAtomics(mask)
9696
return t
9797
}
9898

@@ -112,13 +112,26 @@ func (t *Task) tail() *Task {
112112
// Queue moves the contents of the stack into a queue.
113113
// Elements can be popped from the queue in the same order that they would be popped from the stack.
114114
func (s *Stack) Queue() Queue {
115-
i := interrupt.Disable()
115+
mask := lockAtomics()
116116
head := s.top
117117
s.top = nil
118118
q := Queue{
119119
head: head,
120120
tail: head.tail(),
121121
}
122-
interrupt.Restore(i)
122+
unlockAtomics(mask)
123123
return q
124124
}
125+
126+
// Use runtime.lockAtomics and runtime.unlockAtomics so that Queue and Stack
127+
// work correctly even on multicore systems. These functions are normally used
128+
// to implement atomic operations, but the same spinlock can also be used for
129+
// Queue/Stack operations which are very fast.
130+
// These functions are just plain old interrupt disable/restore on non-multicore
131+
// systems.
132+
133+
//go:linkname lockAtomics runtime.lockAtomics
134+
func lockAtomics() interrupt.State
135+
136+
//go:linkname unlockAtomics runtime.unlockAtomics
137+
func unlockAtomics(mask interrupt.State)

src/internal/task/task.go

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,11 +24,28 @@ type Task struct {
2424
// This is needed for some crypto packages.
2525
FipsIndicator uint8
2626

27+
// State of the goroutine: running, paused, or must-resume-next-pause.
28+
// This extra field doesn't increase memory usage on 32-bit CPUs and above,
29+
// since it falls into the padding of the FipsIndicator bit above.
30+
RunState uint8
31+
2732
// DeferFrame stores a pointer to the (stack allocated) defer frame of the
2833
// goroutine that is used for the recover builtin.
2934
DeferFrame unsafe.Pointer
3035
}
3136

37+
const (
38+
// Initial state: the goroutine state is saved on the stack.
39+
RunStatePaused = iota
40+
41+
// The goroutine is running right now.
42+
RunStateRunning
43+
44+
// The goroutine is running, but already marked as "can resume".
45+
// The next call to Pause() won't actually pause the goroutine.
46+
RunStateResuming
47+
)
48+
3249
// DataUint32 returns the Data field as a uint32. The value is only valid after
3350
// setting it through SetDataUint32 or by storing to it using DataAtomicUint32.
3451
func (t *Task) DataUint32() uint32 {

0 commit comments

Comments
 (0)