From 2811f2d60b5e0df8502b01f9bdfa32adc56eedb7 Mon Sep 17 00:00:00 2001 From: Ayke van Laethem Date: Fri, 2 Aug 2024 17:10:52 +0200 Subject: [PATCH] runtime: add support for os/signal This adds support for enabling and listening to signals on Linux and MacOS. --- builder/musl.go | 1 + compileopts/target.go | 6 +- main_test.go | 9 ++ src/os/signal/signal.go | 14 --- src/runtime/runtime_unix.go | 182 +++++++++++++++++++++++++++++++++++- src/runtime/signal.c | 30 ++++++ src/runtime/wait_other.go | 2 +- testdata/signal.go | 42 +++++++++ testdata/signal.txt | 2 + 9 files changed, 270 insertions(+), 18 deletions(-) delete mode 100644 src/os/signal/signal.go create mode 100644 src/runtime/signal.c create mode 100644 testdata/signal.go create mode 100644 testdata/signal.txt diff --git a/builder/musl.go b/builder/musl.go index 8130981e6c..ecae118e47 100644 --- a/builder/musl.go +++ b/builder/musl.go @@ -128,6 +128,7 @@ var libMusl = Library{ "mman/*.c", "math/*.c", "multibyte/*.c", + "signal/" + arch + "/*.s", "signal/*.c", "stdio/*.c", "string/*.c", diff --git a/compileopts/target.go b/compileopts/target.go index 41a7babd91..7c0592368e 100644 --- a/compileopts/target.go +++ b/compileopts/target.go @@ -390,7 +390,8 @@ func defaultTarget(options *Options) (*TargetSpec, error) { ) spec.ExtraFiles = append(spec.ExtraFiles, "src/runtime/os_darwin.c", - "src/runtime/runtime_unix.c") + "src/runtime/runtime_unix.c", + "src/runtime/signal.c") case "linux": spec.Linker = "ld.lld" spec.RTLib = "compiler-rt" @@ -411,7 +412,8 @@ func defaultTarget(options *Options) (*TargetSpec, error) { spec.CFlags = append(spec.CFlags, "-mno-outline-atomics") } spec.ExtraFiles = append(spec.ExtraFiles, - "src/runtime/runtime_unix.c") + "src/runtime/runtime_unix.c", + "src/runtime/signal.c") case "windows": spec.Linker = "ld.lld" spec.Libc = "mingw-w64" diff --git a/main_test.go b/main_test.go index 62eb5c51b9..532ddf30c6 100644 --- a/main_test.go +++ b/main_test.go @@ -75,6 +75,7 @@ func TestBuild(t *testing.T) { "oldgo/", "print.go", "reflect.go", + "signal.go", "slice.go", "sort.go", "stdlib.go", @@ -213,6 +214,7 @@ func runPlatTests(options compileopts.Options, tests []string, t *testing.T) { // isWebAssembly := strings.HasPrefix(spec.Triple, "wasm") isWASI := strings.HasPrefix(options.Target, "wasi") isWebAssembly := isWASI || strings.HasPrefix(options.Target, "wasm") || (options.Target == "" && strings.HasPrefix(options.GOARCH, "wasm")) + isBaremetal := options.Target == "simavr" || options.Target == "cortex-m-qemu" || options.Target == "riscv-qemu" for _, name := range tests { if options.GOOS == "linux" && (options.GOARCH == "arm" || options.GOARCH == "386") { @@ -277,6 +279,13 @@ func runPlatTests(options compileopts.Options, tests []string, t *testing.T) { continue } } + if isWebAssembly || isBaremetal || options.GOOS == "windows" { + switch name { + case "signal.go": + // Signals only work on POSIX-like systems. + continue + } + } name := name // redefine to avoid race condition t.Run(name, func(t *testing.T) { diff --git a/src/os/signal/signal.go b/src/os/signal/signal.go deleted file mode 100644 index 41ceaf4853..0000000000 --- a/src/os/signal/signal.go +++ /dev/null @@ -1,14 +0,0 @@ -// Copyright 2012 The Go Authors. All rights reserved. -// Use of this source code is governed by a BSD-style -// license that can be found in the LICENSE file. - -package signal - -import ( - "os" -) - -// Just stubbing the functions for now since signal handling is not yet implemented in tinygo -func Reset(sig ...os.Signal) {} -func Ignore(sig ...os.Signal) {} -func Notify(c chan<- os.Signal, sig ...os.Signal) {} diff --git a/src/runtime/runtime_unix.go b/src/runtime/runtime_unix.go index ba5d5a5938..7b7a51f2ad 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,22 @@ func nanosecondsToTicks(ns int64) timeUnit { } func sleepTicks(d timeUnit) { + // Check for incoming signals. + if checkSignals() { + // Received a signal, so there's probably at least one goroutine that's + // runnable again. + return + } + + // TODO: 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. + // timeUnit is in nanoseconds, so need to convert to microseconds here. - usleep(uint(d) / 1000) + result := usleep(uint(d) / 1000) + if result != 0 { + checkSignals() + } } func getTime(clock int32) uint64 { @@ -307,3 +326,164 @@ 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 + +// Simple boolean that's true when any signals have been registered. +var hasSignals uint32 + +// Mask of signals that have been received. The signal handler atomically ORs +// signals into this value. +var receivedSignals 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") + } + atomic.StoreUint32(&hasSignals, 1) + // 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") + } + 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") + } + 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) + } + } +} + +func waitForEvents() { + if atomic.LoadUint32(&hasSignals) != 0 { + // TODO: there is a race condition here. If a signal arrives between + // checkSignals() and pause(), pause() will not exit early but instead + // be delayed until the next signal arrives. + // We should use something like this instead to avoid it: + // - mask all active signals + // - run checkSignals() + // - run sigsuspend() with all active signals + // - unmask all active signals + // For a longer explanation of the problem, see: + // https://www.cipht.net/2023/11/30/perils-of-pause.html + checkSignals() + pause() + checkSignals() + } 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 0000000000..bf1f452b1c --- /dev/null +++ b/src/runtime/signal.c @@ -0,0 +1,30 @@ +//go:build none + +// Ignore the //go:build above. This file is manually included on Linux and +// MacOS to provide os/signal support. + +#include +#include +#include + +// 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); +} diff --git a/src/runtime/wait_other.go b/src/runtime/wait_other.go index b51d4b64b6..1a056dd267 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) && !darwin package runtime diff --git a/testdata/signal.go b/testdata/signal.go new file mode 100644 index 0000000000..a82991f086 --- /dev/null +++ b/testdata/signal.go @@ -0,0 +1,42 @@ +package main + +// Test POSIX signals. +// TODO: run `tinygo test os/signal` instead, once CGo errno return values are +// supported. + +import ( + "os" + "os/signal" + "syscall" + "time" +) + +func main() { + c := make(chan os.Signal, 1) + signal.Notify(c, syscall.SIGUSR1) + + // Wait for signals to arrive. + go func() { + for sig := range c { + if sig == syscall.SIGUSR1 { + println("got expected signal") + } else { + println("got signal:", sig.String()) + } + } + }() + + // Send the signal. + syscall.Kill(syscall.Getpid(), syscall.SIGUSR1) + + time.Sleep(time.Millisecond * 100) + + // Stop notifying. + // (This is just a smoke test, it's difficult to test the default behavior + // in a unit test). + signal.Ignore(syscall.SIGUSR1) + + signal.Stop(c) + + println("exiting signal program") +} diff --git a/testdata/signal.txt b/testdata/signal.txt new file mode 100644 index 0000000000..c4726d7174 --- /dev/null +++ b/testdata/signal.txt @@ -0,0 +1,2 @@ +got expected signal +exiting signal program