Skip to content

Commit

Permalink
runtime: implement race-free signals using futexes
Browse files Browse the repository at this point in the history
  • Loading branch information
aykevl committed Nov 7, 2024
1 parent ca23845 commit eb6f2da
Show file tree
Hide file tree
Showing 10 changed files with 291 additions and 177 deletions.
1 change: 1 addition & 0 deletions GNUmakefile
Original file line number Diff line number Diff line change
Expand Up @@ -934,6 +934,7 @@ endif
@cp -rp lib/musl/src/malloc build/release/tinygo/lib/musl/src
@cp -rp lib/musl/src/mman build/release/tinygo/lib/musl/src
@cp -rp lib/musl/src/math build/release/tinygo/lib/musl/src
@cp -rp lib/musl/src/misc build/release/tinygo/lib/musl/src
@cp -rp lib/musl/src/multibyte build/release/tinygo/lib/musl/src
@cp -rp lib/musl/src/signal build/release/tinygo/lib/musl/src
@cp -rp lib/musl/src/stdio build/release/tinygo/lib/musl/src
Expand Down
1 change: 1 addition & 0 deletions builder/musl.go
Original file line number Diff line number Diff line change
Expand Up @@ -127,6 +127,7 @@ var libMusl = Library{
"malloc/mallocng/*.c",
"mman/*.c",
"math/*.c",
"misc/*.c",
"multibyte/*.c",
"signal/" + arch + "/*.s",
"signal/*.c",
Expand Down
2 changes: 2 additions & 0 deletions compileopts/target.go
Original file line number Diff line number Diff line change
Expand Up @@ -390,6 +390,7 @@ func defaultTarget(options *Options) (*TargetSpec, error) {
"-platform_version", "macos", platformVersion, platformVersion,
)
spec.ExtraFiles = append(spec.ExtraFiles,
"src/internal/futex/futex_darwin.c",
"src/runtime/os_darwin.c",
"src/runtime/runtime_unix.c",
"src/runtime/signal.c")
Expand All @@ -413,6 +414,7 @@ func defaultTarget(options *Options) (*TargetSpec, error) {
spec.CFlags = append(spec.CFlags, "-mno-outline-atomics")
}
spec.ExtraFiles = append(spec.ExtraFiles,
"src/internal/futex/futex_linux.c",
"src/runtime/runtime_unix.c",
"src/runtime/signal.c")
case "windows":
Expand Down
1 change: 1 addition & 0 deletions loader/goroot.go
Original file line number Diff line number Diff line change
Expand Up @@ -243,6 +243,7 @@ func pathsToOverride(goMinor int, needsSyscallPackage bool) map[string]bool {
"internal/binary/": false,
"internal/bytealg/": false,
"internal/cm/": false,
"internal/futex/": false,
"internal/fuzz/": false,
"internal/reflectlite/": false,
"internal/gclayout": false,
Expand Down
74 changes: 74 additions & 0 deletions src/internal/futex/futex.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,74 @@
package futex

// Cross platform futex implementation.
// Futexes are supported on all major operating systems and on WebAssembly.
//
// For more information, see: https://outerproduct.net/futex-dictionary.html

import (
"sync/atomic"
"unsafe"
)

// A futex is a way for userspace to wait with the pointer as the key, and for
// another thread to wake one or all waiting threads keyed on the same pointer.
//
// A futex does not change the underlying value, it only reads it before going
// to sleep (atomically) to prevent lost wake-ups.
type Futex struct {
atomic.Uint32
}

// Atomically check for cmp to still be equal to the futex value and if so, go
// to sleep. Return true if we were definitely awoken by a call to Wake or
// WakeAll, and false if we can't be sure of that.
func (f *Futex) Wait(cmp uint32) bool {
tinygo_futex_wait((*uint32)(unsafe.Pointer(&f.Uint32)), cmp)

// We *could* detect a zero return value from the futex system call which
// would indicate we got awoken by a Wake or WakeAll call. However, this is
// what the manual page has to say:
//
// > Note that a wake-up can also be caused by common futex usage patterns
// > in unrelated code that happened to have previously used the futex
// > word's memory location (e.g., typical futex-based implementations of
// > Pthreads mutexes can cause this under some conditions). Therefore,
// > callers should always conservatively assume that a return value of 0
// > can mean a spurious wake-up, and use the futex word's value (i.e., the
// > user-space synchronization scheme) to decide whether to continue to
// > block or not.
//
// I'm not sure whether we do anything like pthread does, so to be on the
// safe side we say we don't know whether the wakeup was spurious or not and
// return false.
return false
}

// Like Wait, but times out after the number of nanoseconds in timeout.
// If timeout is 0, it may or may not be treated as Wait with infinite timeout.
// Therefore, make sure the timeout value is non-zero.
func (f *Futex) WaitUntil(cmp uint32, timeout uint64) {
tinygo_futex_wait_timeout((*uint32)(unsafe.Pointer(&f.Uint32)), cmp, timeout)
}

// Wake a single waiter.
func (f *Futex) Wake() {
tinygo_futex_wake((*uint32)(unsafe.Pointer(&f.Uint32)))
}

// Wake all waiters.
func (f *Futex) WakeAll() {
tinygo_futex_wake_all((*uint32)(unsafe.Pointer(&f.Uint32)))
}

//export tinygo_futex_wait
func tinygo_futex_wait(addr *uint32, cmp uint32)

//export tinygo_futex_wait_timeout
func tinygo_futex_wait_timeout(addr *uint32, cmp uint32, timeout uint64)

//export tinygo_futex_wake
func tinygo_futex_wake(addr *uint32)

//export tinygo_futex_wake_all
func tinygo_futex_wake_all(addr *uint32)
38 changes: 38 additions & 0 deletions src/internal/futex/futex_darwin.c
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
//go:build none

// This file is manually included, to avoid CGo which would cause a circular
// import.

#include <stdint.h>

int __ulock_wait(uint32_t operation, void *addr, uint64_t value, uint32_t timeout_us);
int __ulock_wait2(uint32_t operation, void *addr, uint64_t value, uint64_t timeout_ns, uint64_t value2);
int __ulock_wake(uint32_t operation, void *addr, uint64_t wake_value);

// Operation code.
#define UL_COMPARE_AND_WAIT 1

// Flags to the operation value.
#define ULF_WAKE_ALL 0x00000100
#define ULF_WAKE_THREAD 0x00000200
#define ULF_NO_ERRNO 0x01000000

void tinygo_futex_wait(uint32_t *addr, uint32_t cmp) {
__ulock_wait(UL_COMPARE_AND_WAIT|ULF_NO_ERRNO, addr, (uint64_t)cmp, 0);
}

void tinygo_futex_wait_timeout(uint32_t *addr, uint32_t cmp, uint64_t timeout) {
// Note: __ulock_wait2 is available since MacOS 11.
// I think that's fine, since the version before that (MacOS 10.15) is EOL
// since 2022. Though if needed, we could certainly use __ulock_wait instead
// and deal with the smaller timeout value.
__ulock_wait2(UL_COMPARE_AND_WAIT|ULF_NO_ERRNO, addr, (uint64_t)cmp, timeout, 0);
}

void tinygo_futex_wake(uint32_t *addr) {
__ulock_wake(UL_COMPARE_AND_WAIT|ULF_NO_ERRNO, addr, 0);
}

void tinygo_futex_wake_all(uint32_t *addr) {
__ulock_wake(UL_COMPARE_AND_WAIT|ULF_NO_ERRNO|ULF_WAKE_ALL, addr, 0);
}
32 changes: 32 additions & 0 deletions src/internal/futex/futex_linux.c
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
//go:build none

// This file is manually included, to avoid CGo which would cause a circular
// import.

#include <stdint.h>
#include <sys/syscall.h>
#include <time.h>
#include <unistd.h>

#define FUTEX_WAIT 0
#define FUTEX_WAKE 1
#define FUTEX_PRIVATE_FLAG 128

void tinygo_futex_wait(uint32_t *addr, uint32_t cmp) {
syscall(SYS_futex, addr, FUTEX_WAIT|FUTEX_PRIVATE_FLAG, cmp, NULL, NULL, 0);
}

void tinygo_futex_wait_timeout(uint32_t *addr, uint32_t cmp, uint64_t timeout) {
struct timespec ts = {0};
ts.tv_sec = timeout / 1000000000;
ts.tv_nsec = timeout % 1000000000;
syscall(SYS_futex, addr, FUTEX_WAIT|FUTEX_PRIVATE_FLAG, cmp, &ts, NULL, 0);
}

void tinygo_futex_wake(uint32_t *addr) {
syscall(SYS_futex, addr, FUTEX_WAKE|FUTEX_PRIVATE_FLAG, 1, NULL, NULL, 0);
}

void tinygo_futex_wake_all(uint32_t *addr) {
syscall(SYS_futex, addr, FUTEX_WAKE|FUTEX_PRIVATE_FLAG, INT_MAX, NULL, NULL, 0);
}
55 changes: 55 additions & 0 deletions src/internal/futex/futex_linux.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
package futex

import (
"unsafe"
)

// Atomically check for cmp to still be equal to the futex value and if so, go
// to sleep. Return true if we were definitely awoken by a call to Wake or
// WakeAll, and false if we can't be sure of that.
func (f *Futex) Wait(cmp uint32) bool {
tinygo_futex_wait((*uint32)(unsafe.Pointer(&f.Uint32)), cmp)

// We *could* detect a zero return value from the futex system call which
// would indicate we got awoken by a Wake or WakeAll call. However, this is
// what the manual page has to say:
//
// > Note that a wake-up can also be caused by common futex usage patterns
// > in unrelated code that happened to have previously used the futex
// > word's memory location (e.g., typical futex-based implementations of
// > Pthreads mutexes can cause this under some conditions). Therefore,
// > callers should always conservatively assume that a return value of 0
// > can mean a spurious wake-up, and use the futex word's value (i.e., the
// > user-space synchronization scheme) to decide whether to continue to
// > block or not.
//
// I'm not sure whether we do anything like pthread does, so to be on the
// safe side we say we don't know whether the wakeup was spurious or not and
// return false.
return false
}

// Like Wait, but times out after the number of nanoseconds in timeout.
func (f *Futex) WaitUntil(cmp uint32, timeout uint64) {
tinygo_futex_wait_timeout((*uint32)(unsafe.Pointer(&f.Uint32)), cmp, timeout)
}

// Wake a single waiter.
func (f *Futex) Wake() {
tinygo_futex_wake((*uint32)(unsafe.Pointer(&f.Uint32)), 1)
}

// Wake all waiters.
func (f *Futex) WakeAll() {
const maxInt32 = 0x7fff_ffff
tinygo_futex_wake((*uint32)(unsafe.Pointer(&f.Uint32)), maxInt32)
}

//export tinygo_futex_wait
func tinygo_futex_wait(addr *uint32, cmp uint32)

//export tinygo_futex_wait_timeout
func tinygo_futex_wait_timeout(addr *uint32, cmp uint32, timeout uint64)

//export tinygo_futex_wake
func tinygo_futex_wake(addr *uint32, num uint32)
Loading

0 comments on commit eb6f2da

Please sign in to comment.