aboutsummaryrefslogtreecommitdiffhomepage
path: root/src/runtime
diff options
context:
space:
mode:
authorAyke van Laethem <[email protected]>2024-08-02 17:10:52 +0200
committerRon Evans <[email protected]>2024-10-23 12:25:27 +0100
commitb8fe75a9dd4949a08b78b2f3dd06fa3c697573dd (patch)
tree79350981ea8af9440f86818cd17c49741a948c89 /src/runtime
parent0f95b4102d83b6977277ea7d5e87ad538eddc5ef (diff)
downloadtinygo-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.go228
-rw-r--r--src/runtime/signal.c89
-rw-r--r--src/runtime/wait_other.go2
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