mirror of
https://github.com/tinygo-org/tinygo.git
synced 2026-07-26 06:38:42 +00:00
runtime: add support for os/signal
This adds support for enabling and listening to signals on Linux and MacOS.
This commit is contained in:
committed by
Ron Evans
parent
0f95b4102d
commit
b8fe75a9dd
@@ -128,6 +128,7 @@ var libMusl = Library{
|
|||||||
"mman/*.c",
|
"mman/*.c",
|
||||||
"math/*.c",
|
"math/*.c",
|
||||||
"multibyte/*.c",
|
"multibyte/*.c",
|
||||||
|
"signal/" + arch + "/*.s",
|
||||||
"signal/*.c",
|
"signal/*.c",
|
||||||
"stdio/*.c",
|
"stdio/*.c",
|
||||||
"string/*.c",
|
"string/*.c",
|
||||||
|
|||||||
@@ -391,7 +391,8 @@ func defaultTarget(options *Options) (*TargetSpec, error) {
|
|||||||
)
|
)
|
||||||
spec.ExtraFiles = append(spec.ExtraFiles,
|
spec.ExtraFiles = append(spec.ExtraFiles,
|
||||||
"src/runtime/os_darwin.c",
|
"src/runtime/os_darwin.c",
|
||||||
"src/runtime/runtime_unix.c")
|
"src/runtime/runtime_unix.c",
|
||||||
|
"src/runtime/signal.c")
|
||||||
case "linux":
|
case "linux":
|
||||||
spec.Linker = "ld.lld"
|
spec.Linker = "ld.lld"
|
||||||
spec.RTLib = "compiler-rt"
|
spec.RTLib = "compiler-rt"
|
||||||
@@ -412,7 +413,8 @@ func defaultTarget(options *Options) (*TargetSpec, error) {
|
|||||||
spec.CFlags = append(spec.CFlags, "-mno-outline-atomics")
|
spec.CFlags = append(spec.CFlags, "-mno-outline-atomics")
|
||||||
}
|
}
|
||||||
spec.ExtraFiles = append(spec.ExtraFiles,
|
spec.ExtraFiles = append(spec.ExtraFiles,
|
||||||
"src/runtime/runtime_unix.c")
|
"src/runtime/runtime_unix.c",
|
||||||
|
"src/runtime/signal.c")
|
||||||
case "windows":
|
case "windows":
|
||||||
spec.Linker = "ld.lld"
|
spec.Linker = "ld.lld"
|
||||||
spec.Libc = "mingw-w64"
|
spec.Libc = "mingw-w64"
|
||||||
|
|||||||
@@ -79,6 +79,7 @@ func TestBuild(t *testing.T) {
|
|||||||
"oldgo/",
|
"oldgo/",
|
||||||
"print.go",
|
"print.go",
|
||||||
"reflect.go",
|
"reflect.go",
|
||||||
|
"signal.go",
|
||||||
"slice.go",
|
"slice.go",
|
||||||
"sort.go",
|
"sort.go",
|
||||||
"stdlib.go",
|
"stdlib.go",
|
||||||
@@ -217,6 +218,7 @@ func runPlatTests(options compileopts.Options, tests []string, t *testing.T) {
|
|||||||
// isWebAssembly := strings.HasPrefix(spec.Triple, "wasm")
|
// isWebAssembly := strings.HasPrefix(spec.Triple, "wasm")
|
||||||
isWASI := strings.HasPrefix(options.Target, "wasi")
|
isWASI := strings.HasPrefix(options.Target, "wasi")
|
||||||
isWebAssembly := isWASI || strings.HasPrefix(options.Target, "wasm") || (options.Target == "" && strings.HasPrefix(options.GOARCH, "wasm"))
|
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 {
|
for _, name := range tests {
|
||||||
if options.GOOS == "linux" && (options.GOARCH == "arm" || options.GOARCH == "386") {
|
if options.GOOS == "linux" && (options.GOARCH == "arm" || options.GOARCH == "386") {
|
||||||
@@ -281,6 +283,13 @@ func runPlatTests(options compileopts.Options, tests []string, t *testing.T) {
|
|||||||
continue
|
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
|
name := name // redefine to avoid race condition
|
||||||
t.Run(name, func(t *testing.T) {
|
t.Run(name, func(t *testing.T) {
|
||||||
|
|||||||
@@ -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) {}
|
|
||||||
+226
-2
@@ -3,6 +3,8 @@
|
|||||||
package runtime
|
package runtime
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"math/bits"
|
||||||
|
"sync/atomic"
|
||||||
"unsafe"
|
"unsafe"
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -12,6 +14,9 @@ func libc_write(fd int32, buf unsafe.Pointer, count uint) int
|
|||||||
//export usleep
|
//export usleep
|
||||||
func usleep(usec uint) int
|
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);
|
// void *mmap(void *addr, size_t length, int prot, int flags, int fd, off_t offset);
|
||||||
// Note: off_t is defined as int64 because:
|
// Note: off_t is defined as int64 because:
|
||||||
// - musl (used on Linux) always defines it as int64
|
// - musl (used on Linux) always defines it as int64
|
||||||
@@ -217,8 +222,47 @@ func nanosecondsToTicks(ns int64) timeUnit {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func sleepTicks(d timeUnit) {
|
func sleepTicks(d timeUnit) {
|
||||||
// timeUnit is in nanoseconds, so need to convert to microseconds here.
|
// When there are no signal handlers present, we can simply go to sleep.
|
||||||
usleep(uint(d) / 1000)
|
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 {
|
func getTime(clock int32) uint64 {
|
||||||
@@ -307,3 +351,183 @@ func growHeap() bool {
|
|||||||
setHeapEnd(heapStart + heapSize)
|
setHeapEnd(heapStart + heapSize)
|
||||||
return true
|
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")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@@ -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);
|
||||||
|
}
|
||||||
@@ -1,4 +1,4 @@
|
|||||||
//go:build !tinygo.riscv && !cortexm
|
//go:build !tinygo.riscv && !cortexm && !(linux && !baremetal && !tinygo.wasm && !nintendoswitch) && !darwin
|
||||||
|
|
||||||
package runtime
|
package runtime
|
||||||
|
|
||||||
|
|||||||
Vendored
+42
@@ -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")
|
||||||
|
}
|
||||||
Vendored
+2
@@ -0,0 +1,2 @@
|
|||||||
|
got expected signal
|
||||||
|
exiting signal program
|
||||||
Reference in New Issue
Block a user