diff options
author | Ayke van Laethem <[email protected]> | 2024-08-02 17:10:52 +0200 |
---|---|---|
committer | Ron Evans <[email protected]> | 2024-10-23 12:25:27 +0100 |
commit | b8fe75a9dd4949a08b78b2f3dd06fa3c697573dd (patch) | |
tree | 79350981ea8af9440f86818cd17c49741a948c89 /src/runtime | |
parent | 0f95b4102d83b6977277ea7d5e87ad538eddc5ef (diff) | |
download | tinygo-b8fe75a9dd4949a08b78b2f3dd06fa3c697573dd.tar.gz tinygo-b8fe75a9dd4949a08b78b2f3dd06fa3c697573dd.zip |
runtime: add support for os/signal
This adds support for enabling and listening to signals on Linux and
MacOS.
Diffstat (limited to 'src/runtime')
-rw-r--r-- | src/runtime/runtime_unix.go | 228 | ||||
-rw-r--r-- | src/runtime/signal.c | 89 | ||||
-rw-r--r-- | src/runtime/wait_other.go | 2 |
3 files changed, 316 insertions, 3 deletions
diff --git a/src/runtime/runtime_unix.go b/src/runtime/runtime_unix.go index ba5d5a593..c4fd3285b 100644 --- a/src/runtime/runtime_unix.go +++ b/src/runtime/runtime_unix.go @@ -3,6 +3,8 @@ package runtime import ( + "math/bits" + "sync/atomic" "unsafe" ) @@ -12,6 +14,9 @@ func libc_write(fd int32, buf unsafe.Pointer, count uint) int //export usleep func usleep(usec uint) int +//export pause +func pause() int32 + // void *mmap(void *addr, size_t length, int prot, int flags, int fd, off_t offset); // Note: off_t is defined as int64 because: // - musl (used on Linux) always defines it as int64 @@ -217,8 +222,47 @@ func nanosecondsToTicks(ns int64) timeUnit { } func sleepTicks(d timeUnit) { - // timeUnit is in nanoseconds, so need to convert to microseconds here. - usleep(uint(d) / 1000) + // When there are no signal handlers present, we can simply go to sleep. + if !hasSignals { + // timeUnit is in nanoseconds, so need to convert to microseconds here. + usleep(uint(d) / 1000) + return + } + + if GOOS == "darwin" { + // Check for incoming signals. + if checkSignals() { + // Received a signal, so there's probably at least one goroutine + // that's runnable again. + return + } + + // WARNING: there is a race condition here. If a signal arrives between + // checkSignals() and usleep(), the usleep() call will not exit early so + // the signal is delayed until usleep finishes or another signal + // arrives. + // There doesn't appear to be a simple way to fix this on MacOS. + + // timeUnit is in nanoseconds, so need to convert to microseconds here. + result := usleep(uint(d) / 1000) + if result != 0 { + checkSignals() + } + } else { + // Linux (and various other POSIX systems) implement sigtimedwait so we + // can do this in a non-racy way. + tinygo_wfi_mask(activeSignals) + if checkSignals() { + tinygo_wfi_unmask() + return + } + signal := tinygo_wfi_sleep(activeSignals, uint64(d)) + if signal >= 0 { + tinygo_signal_handler(signal) + checkSignals() + } + tinygo_wfi_unmask() + } } func getTime(clock int32) uint64 { @@ -307,3 +351,183 @@ func growHeap() bool { setHeapEnd(heapStart + heapSize) return true } + +func init() { + // Set up a channel to receive signals into. + signalChan = make(chan uint32, 1) +} + +var signalChan chan uint32 + +// Indicate whether signals have been registered. +var hasSignals bool + +// Mask of signals that have been received. The signal handler atomically ORs +// signals into this value. +var receivedSignals uint32 + +var activeSignals uint32 + +//go:linkname signal_enable os/signal.signal_enable +func signal_enable(s uint32) { + if s >= 32 { + // TODO: to support higher signal numbers, we need to turn + // receivedSignals into a uint32 array. + runtimePanicAt(returnAddress(0), "unsupported signal number") + } + hasSignals = true + activeSignals |= 1 << s + // It's easier to implement this function in C. + tinygo_signal_enable(s) +} + +//go:linkname signal_ignore os/signal.signal_ignore +func signal_ignore(s uint32) { + if s >= 32 { + // TODO: to support higher signal numbers, we need to turn + // receivedSignals into a uint32 array. + runtimePanicAt(returnAddress(0), "unsupported signal number") + } + activeSignals &^= 1 << s + tinygo_signal_ignore(s) +} + +//go:linkname signal_disable os/signal.signal_disable +func signal_disable(s uint32) { + if s >= 32 { + // TODO: to support higher signal numbers, we need to turn + // receivedSignals into a uint32 array. + runtimePanicAt(returnAddress(0), "unsupported signal number") + } + activeSignals &^= 1 << s + tinygo_signal_disable(s) +} + +//go:linkname signal_waitUntilIdle os/signal.signalWaitUntilIdle +func signal_waitUntilIdle() { + // Make sure all signals are sent on the channel. + for atomic.LoadUint32(&receivedSignals) != 0 { + checkSignals() + Gosched() + } + + // Make sure all signals are processed. + for len(signalChan) != 0 { + Gosched() + } +} + +//export tinygo_signal_enable +func tinygo_signal_enable(s uint32) + +//export tinygo_signal_ignore +func tinygo_signal_ignore(s uint32) + +//export tinygo_signal_disable +func tinygo_signal_disable(s uint32) + +// void tinygo_signal_handler(int sig); +// +//export tinygo_signal_handler +func tinygo_signal_handler(s int32) { + // This loop is essentially the atomic equivalent of the following: + // + // receivedSignals |= 1 << s + // + // TODO: use atomic.Uint32.And once we drop support for Go 1.22 instead of + // this loop. + for { + mask := uint32(1) << uint32(s) + val := atomic.LoadUint32(&receivedSignals) + swapped := atomic.CompareAndSwapUint32(&receivedSignals, val, val|mask) + if swapped { + break + } + } +} + +//go:linkname signal_recv os/signal.signal_recv +func signal_recv() uint32 { + // Function called from os/signal to get the next received signal. + val := <-signalChan + checkSignals() + return val +} + +// Atomically find a signal that previously occured and send it into the +// signalChan channel. Return true if at least one signal was delivered this +// way, false otherwise. +func checkSignals() bool { + gotSignals := false + for { + // Extract the lowest numbered signal number from receivedSignals. + val := atomic.LoadUint32(&receivedSignals) + if val == 0 { + // There is no signal ready to be received by the program (common + // case). + return gotSignals + } + num := uint32(bits.TrailingZeros32(val)) + + // Do a non-blocking send on signalChan. + select { + case signalChan <- num: + // There was room free in the channel, so remove the signal number + // from the receivedSignals mask. + gotSignals = true + default: + // Could not send the signal number on the channel. This means + // there's still a signal pending. In that case, let it be received + // at which point checkSignals is called again to put the next one + // in the channel buffer. + return gotSignals + } + + // Atomically clear the signal number from receivedSignals. + // TODO: use atomic.Uint32.Or once we drop support for Go 1.22 instead + // of this loop. + for { + newVal := val &^ (1 << num) + swapped := atomic.CompareAndSwapUint32(&receivedSignals, val, newVal) + if swapped { + break + } + val = atomic.LoadUint32(&receivedSignals) + } + } +} + +//export tinygo_wfi_mask +func tinygo_wfi_mask(active uint32) + +//export tinygo_wfi_sleep +func tinygo_wfi_sleep(active uint32, timeout uint64) int32 + +//export tinygo_wfi_wait +func tinygo_wfi_wait(active uint32) int32 + +//export tinygo_wfi_unmask +func tinygo_wfi_unmask() + +func waitForEvents() { + if hasSignals { + // We could have used pause() here, but that function is impossible to + // use in a race-free way: + // https://www.cipht.net/2023/11/30/perils-of-pause.html + // Therefore we need something better. + // Note: this is unsafe with multithreading, because sigprocmask is only + // defined for single-threaded applictions. + tinygo_wfi_mask(activeSignals) + if checkSignals() { + tinygo_wfi_unmask() + return + } + signal := tinygo_wfi_wait(activeSignals) + tinygo_signal_handler(signal) + checkSignals() + tinygo_wfi_unmask() + } else { + // The program doesn't use signals, so this is a deadlock. + runtimePanic("deadlocked: no event source") + } +} diff --git a/src/runtime/signal.c b/src/runtime/signal.c new file mode 100644 index 000000000..a462518c1 --- /dev/null +++ b/src/runtime/signal.c @@ -0,0 +1,89 @@ +//go:build none + +// Ignore the //go:build above. This file is manually included on Linux and +// MacOS to provide os/signal support. + +#include <stdint.h> +#include <signal.h> +#include <time.h> +#include <unistd.h> + +// Signal handler in the runtime. +void tinygo_signal_handler(int sig); + +// Enable a signal from the runtime. +void tinygo_signal_enable(uint32_t sig) { + struct sigaction act = { 0 }; + act.sa_handler = &tinygo_signal_handler; + sigaction(sig, &act, NULL); +} + +void tinygo_signal_ignore(uint32_t sig) { + struct sigaction act = { 0 }; + act.sa_handler = SIG_IGN; + sigaction(sig, &act, NULL); +} + +void tinygo_signal_disable(uint32_t sig) { + struct sigaction act = { 0 }; + act.sa_handler = SIG_DFL; + sigaction(sig, &act, NULL); +} + +// Implement waitForEvents and sleep with signals. +// Warning: sigprocmask is not defined in a multithreaded program so will need +// to be replaced with something else once we implement threading on POSIX. + +// Signals active before a call to tinygo_wfi_mask. +static sigset_t active_signals; + +static void tinygo_set_signals(sigset_t *mask, uint32_t signals) { + sigemptyset(mask); + for (int i=0; i<32; i++) { + if ((signals & (1<<i)) != 0) { + sigaddset(mask, i); + } + } +} + +// Mask the given signals. +// This function must always restore the previous signals using +// tinygo_wfi_unmask, to create a critical section. +void tinygo_wfi_mask(uint32_t active) { + sigset_t mask; + tinygo_set_signals(&mask, active); + + sigprocmask(SIG_BLOCK, &mask, &active_signals); +} + +// Wait until a signal becomes pending (or is already pending), and return the +// signal. +#if !defined(__APPLE__) +int tinygo_wfi_sleep(uint32_t active, uint64_t timeout) { + sigset_t active_set; + tinygo_set_signals(&active_set, active); + + struct timespec ts = {0}; + ts.tv_sec = timeout / 1000000000; + ts.tv_nsec = timeout % 1000000000; + + int result = sigtimedwait(&active_set, NULL, &ts); + return result; +} +#endif + +// Wait until any of the active signals becomes pending (or returns immediately +// if one is already pending). +int tinygo_wfi_wait(uint32_t active) { + sigset_t active_set; + tinygo_set_signals(&active_set, active); + + int sig = 0; + sigwait(&active_set, &sig); + return sig; +} + +// Restore previous signal mask. +void tinygo_wfi_unmask(void) { + sigprocmask(SIG_SETMASK, &active_signals, NULL); +} diff --git a/src/runtime/wait_other.go b/src/runtime/wait_other.go index b51d4b64b..f1487e396 100644 --- a/src/runtime/wait_other.go +++ b/src/runtime/wait_other.go @@ -1,4 +1,4 @@ -//go:build !tinygo.riscv && !cortexm +//go:build !tinygo.riscv && !cortexm && !(linux && !baremetal && !tinygo.wasm && !nintendoswitch) && !darwin package runtime |