Skip to content

Commit eb6f2da

Browse files
committed
runtime: implement race-free signals using futexes
1 parent ca23845 commit eb6f2da

File tree

10 files changed

+291
-177
lines changed

10 files changed

+291
-177
lines changed

GNUmakefile

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -934,6 +934,7 @@ endif
934934
@cp -rp lib/musl/src/malloc build/release/tinygo/lib/musl/src
935935
@cp -rp lib/musl/src/mman build/release/tinygo/lib/musl/src
936936
@cp -rp lib/musl/src/math build/release/tinygo/lib/musl/src
937+
@cp -rp lib/musl/src/misc build/release/tinygo/lib/musl/src
937938
@cp -rp lib/musl/src/multibyte build/release/tinygo/lib/musl/src
938939
@cp -rp lib/musl/src/signal build/release/tinygo/lib/musl/src
939940
@cp -rp lib/musl/src/stdio build/release/tinygo/lib/musl/src

builder/musl.go

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -127,6 +127,7 @@ var libMusl = Library{
127127
"malloc/mallocng/*.c",
128128
"mman/*.c",
129129
"math/*.c",
130+
"misc/*.c",
130131
"multibyte/*.c",
131132
"signal/" + arch + "/*.s",
132133
"signal/*.c",

compileopts/target.go

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -390,6 +390,7 @@ func defaultTarget(options *Options) (*TargetSpec, error) {
390390
"-platform_version", "macos", platformVersion, platformVersion,
391391
)
392392
spec.ExtraFiles = append(spec.ExtraFiles,
393+
"src/internal/futex/futex_darwin.c",
393394
"src/runtime/os_darwin.c",
394395
"src/runtime/runtime_unix.c",
395396
"src/runtime/signal.c")
@@ -413,6 +414,7 @@ func defaultTarget(options *Options) (*TargetSpec, error) {
413414
spec.CFlags = append(spec.CFlags, "-mno-outline-atomics")
414415
}
415416
spec.ExtraFiles = append(spec.ExtraFiles,
417+
"src/internal/futex/futex_linux.c",
416418
"src/runtime/runtime_unix.c",
417419
"src/runtime/signal.c")
418420
case "windows":

loader/goroot.go

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -243,6 +243,7 @@ func pathsToOverride(goMinor int, needsSyscallPackage bool) map[string]bool {
243243
"internal/binary/": false,
244244
"internal/bytealg/": false,
245245
"internal/cm/": false,
246+
"internal/futex/": false,
246247
"internal/fuzz/": false,
247248
"internal/reflectlite/": false,
248249
"internal/gclayout": false,

src/internal/futex/futex.go

Lines changed: 74 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,74 @@
1+
package futex
2+
3+
// Cross platform futex implementation.
4+
// Futexes are supported on all major operating systems and on WebAssembly.
5+
//
6+
// For more information, see: https://outerproduct.net/futex-dictionary.html
7+
8+
import (
9+
"sync/atomic"
10+
"unsafe"
11+
)
12+
13+
// A futex is a way for userspace to wait with the pointer as the key, and for
14+
// another thread to wake one or all waiting threads keyed on the same pointer.
15+
//
16+
// A futex does not change the underlying value, it only reads it before going
17+
// to sleep (atomically) to prevent lost wake-ups.
18+
type Futex struct {
19+
atomic.Uint32
20+
}
21+
22+
// Atomically check for cmp to still be equal to the futex value and if so, go
23+
// to sleep. Return true if we were definitely awoken by a call to Wake or
24+
// WakeAll, and false if we can't be sure of that.
25+
func (f *Futex) Wait(cmp uint32) bool {
26+
tinygo_futex_wait((*uint32)(unsafe.Pointer(&f.Uint32)), cmp)
27+
28+
// We *could* detect a zero return value from the futex system call which
29+
// would indicate we got awoken by a Wake or WakeAll call. However, this is
30+
// what the manual page has to say:
31+
//
32+
// > Note that a wake-up can also be caused by common futex usage patterns
33+
// > in unrelated code that happened to have previously used the futex
34+
// > word's memory location (e.g., typical futex-based implementations of
35+
// > Pthreads mutexes can cause this under some conditions). Therefore,
36+
// > callers should always conservatively assume that a return value of 0
37+
// > can mean a spurious wake-up, and use the futex word's value (i.e., the
38+
// > user-space synchronization scheme) to decide whether to continue to
39+
// > block or not.
40+
//
41+
// I'm not sure whether we do anything like pthread does, so to be on the
42+
// safe side we say we don't know whether the wakeup was spurious or not and
43+
// return false.
44+
return false
45+
}
46+
47+
// Like Wait, but times out after the number of nanoseconds in timeout.
48+
// If timeout is 0, it may or may not be treated as Wait with infinite timeout.
49+
// Therefore, make sure the timeout value is non-zero.
50+
func (f *Futex) WaitUntil(cmp uint32, timeout uint64) {
51+
tinygo_futex_wait_timeout((*uint32)(unsafe.Pointer(&f.Uint32)), cmp, timeout)
52+
}
53+
54+
// Wake a single waiter.
55+
func (f *Futex) Wake() {
56+
tinygo_futex_wake((*uint32)(unsafe.Pointer(&f.Uint32)))
57+
}
58+
59+
// Wake all waiters.
60+
func (f *Futex) WakeAll() {
61+
tinygo_futex_wake_all((*uint32)(unsafe.Pointer(&f.Uint32)))
62+
}
63+
64+
//export tinygo_futex_wait
65+
func tinygo_futex_wait(addr *uint32, cmp uint32)
66+
67+
//export tinygo_futex_wait_timeout
68+
func tinygo_futex_wait_timeout(addr *uint32, cmp uint32, timeout uint64)
69+
70+
//export tinygo_futex_wake
71+
func tinygo_futex_wake(addr *uint32)
72+
73+
//export tinygo_futex_wake_all
74+
func tinygo_futex_wake_all(addr *uint32)

src/internal/futex/futex_darwin.c

Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,38 @@
1+
//go:build none
2+
3+
// This file is manually included, to avoid CGo which would cause a circular
4+
// import.
5+
6+
#include <stdint.h>
7+
8+
int __ulock_wait(uint32_t operation, void *addr, uint64_t value, uint32_t timeout_us);
9+
int __ulock_wait2(uint32_t operation, void *addr, uint64_t value, uint64_t timeout_ns, uint64_t value2);
10+
int __ulock_wake(uint32_t operation, void *addr, uint64_t wake_value);
11+
12+
// Operation code.
13+
#define UL_COMPARE_AND_WAIT 1
14+
15+
// Flags to the operation value.
16+
#define ULF_WAKE_ALL 0x00000100
17+
#define ULF_WAKE_THREAD 0x00000200
18+
#define ULF_NO_ERRNO 0x01000000
19+
20+
void tinygo_futex_wait(uint32_t *addr, uint32_t cmp) {
21+
__ulock_wait(UL_COMPARE_AND_WAIT|ULF_NO_ERRNO, addr, (uint64_t)cmp, 0);
22+
}
23+
24+
void tinygo_futex_wait_timeout(uint32_t *addr, uint32_t cmp, uint64_t timeout) {
25+
// Note: __ulock_wait2 is available since MacOS 11.
26+
// I think that's fine, since the version before that (MacOS 10.15) is EOL
27+
// since 2022. Though if needed, we could certainly use __ulock_wait instead
28+
// and deal with the smaller timeout value.
29+
__ulock_wait2(UL_COMPARE_AND_WAIT|ULF_NO_ERRNO, addr, (uint64_t)cmp, timeout, 0);
30+
}
31+
32+
void tinygo_futex_wake(uint32_t *addr) {
33+
__ulock_wake(UL_COMPARE_AND_WAIT|ULF_NO_ERRNO, addr, 0);
34+
}
35+
36+
void tinygo_futex_wake_all(uint32_t *addr) {
37+
__ulock_wake(UL_COMPARE_AND_WAIT|ULF_NO_ERRNO|ULF_WAKE_ALL, addr, 0);
38+
}

src/internal/futex/futex_linux.c

Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,32 @@
1+
//go:build none
2+
3+
// This file is manually included, to avoid CGo which would cause a circular
4+
// import.
5+
6+
#include <stdint.h>
7+
#include <sys/syscall.h>
8+
#include <time.h>
9+
#include <unistd.h>
10+
11+
#define FUTEX_WAIT 0
12+
#define FUTEX_WAKE 1
13+
#define FUTEX_PRIVATE_FLAG 128
14+
15+
void tinygo_futex_wait(uint32_t *addr, uint32_t cmp) {
16+
syscall(SYS_futex, addr, FUTEX_WAIT|FUTEX_PRIVATE_FLAG, cmp, NULL, NULL, 0);
17+
}
18+
19+
void tinygo_futex_wait_timeout(uint32_t *addr, uint32_t cmp, uint64_t timeout) {
20+
struct timespec ts = {0};
21+
ts.tv_sec = timeout / 1000000000;
22+
ts.tv_nsec = timeout % 1000000000;
23+
syscall(SYS_futex, addr, FUTEX_WAIT|FUTEX_PRIVATE_FLAG, cmp, &ts, NULL, 0);
24+
}
25+
26+
void tinygo_futex_wake(uint32_t *addr) {
27+
syscall(SYS_futex, addr, FUTEX_WAKE|FUTEX_PRIVATE_FLAG, 1, NULL, NULL, 0);
28+
}
29+
30+
void tinygo_futex_wake_all(uint32_t *addr) {
31+
syscall(SYS_futex, addr, FUTEX_WAKE|FUTEX_PRIVATE_FLAG, INT_MAX, NULL, NULL, 0);
32+
}

src/internal/futex/futex_linux.go

Lines changed: 55 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,55 @@
1+
package futex
2+
3+
import (
4+
"unsafe"
5+
)
6+
7+
// Atomically check for cmp to still be equal to the futex value and if so, go
8+
// to sleep. Return true if we were definitely awoken by a call to Wake or
9+
// WakeAll, and false if we can't be sure of that.
10+
func (f *Futex) Wait(cmp uint32) bool {
11+
tinygo_futex_wait((*uint32)(unsafe.Pointer(&f.Uint32)), cmp)
12+
13+
// We *could* detect a zero return value from the futex system call which
14+
// would indicate we got awoken by a Wake or WakeAll call. However, this is
15+
// what the manual page has to say:
16+
//
17+
// > Note that a wake-up can also be caused by common futex usage patterns
18+
// > in unrelated code that happened to have previously used the futex
19+
// > word's memory location (e.g., typical futex-based implementations of
20+
// > Pthreads mutexes can cause this under some conditions). Therefore,
21+
// > callers should always conservatively assume that a return value of 0
22+
// > can mean a spurious wake-up, and use the futex word's value (i.e., the
23+
// > user-space synchronization scheme) to decide whether to continue to
24+
// > block or not.
25+
//
26+
// I'm not sure whether we do anything like pthread does, so to be on the
27+
// safe side we say we don't know whether the wakeup was spurious or not and
28+
// return false.
29+
return false
30+
}
31+
32+
// Like Wait, but times out after the number of nanoseconds in timeout.
33+
func (f *Futex) WaitUntil(cmp uint32, timeout uint64) {
34+
tinygo_futex_wait_timeout((*uint32)(unsafe.Pointer(&f.Uint32)), cmp, timeout)
35+
}
36+
37+
// Wake a single waiter.
38+
func (f *Futex) Wake() {
39+
tinygo_futex_wake((*uint32)(unsafe.Pointer(&f.Uint32)), 1)
40+
}
41+
42+
// Wake all waiters.
43+
func (f *Futex) WakeAll() {
44+
const maxInt32 = 0x7fff_ffff
45+
tinygo_futex_wake((*uint32)(unsafe.Pointer(&f.Uint32)), maxInt32)
46+
}
47+
48+
//export tinygo_futex_wait
49+
func tinygo_futex_wait(addr *uint32, cmp uint32)
50+
51+
//export tinygo_futex_wait_timeout
52+
func tinygo_futex_wait_timeout(addr *uint32, cmp uint32, timeout uint64)
53+
54+
//export tinygo_futex_wake
55+
func tinygo_futex_wake(addr *uint32, num uint32)

0 commit comments

Comments
 (0)