diff --git a/src/runtime/runtime_unix.go b/src/runtime/runtime_unix.go index 24b208c387..ce548d0ef4 100644 --- a/src/runtime/runtime_unix.go +++ b/src/runtime/runtime_unix.go @@ -4,7 +4,6 @@ package runtime import ( "internal/futex" - "internal/task" "math/bits" "sync/atomic" "tinygo" @@ -414,17 +413,6 @@ func signal_disable(s uint32) { tinygo_signal_disable(s) } -//go:linkname signal_waitUntilIdle os/signal.signalWaitUntilIdle -func signal_waitUntilIdle() { - // Wait until signal_recv has processed all signals. - for receivedSignals.Load() != 0 { - // TODO: this becomes a busy loop when using threads. - // We might want to pause until signal_recv has no more incoming signals - // to process. - Gosched() - } -} - //export tinygo_signal_enable func tinygo_signal_enable(s uint32) @@ -448,48 +436,28 @@ func tinygo_signal_handler(s int32) { // goroutines. signalFutex.WakeAll() } -} -// Task waiting for a signal to arrive, or nil if it is running or there are no -// signals. -var signalRecvWaiter atomic.Pointer[task.Task] - -//go:linkname signal_recv os/signal.signal_recv -func signal_recv() uint32 { - // Function called from os/signal to get the next received signal. - for { - val := receivedSignals.Load() - if val == 0 { - // There are no signals to receive. Sleep until there are. - if signalRecvWaiter.Swap(task.Current()) != nil { - // We expect only a single goroutine to call signal_recv. - runtimeFatal("signal_recv called concurrently") - } - task.Pause() - continue - } + // Wake the goroutine inside os/signal that gives signals to the channels + // of signal.Notify. This is a no-op with the cooperative scheduler, which + // resumes that goroutine from checkSignals instead. + signalRecvWake() +} - // Extract the lowest numbered signal number from receivedSignals. - num := uint32(bits.TrailingZeros32(val)) +// nextReceivedSignal takes the lowest numbered signal out of receivedSignals, +// or returns 0, false when there are none pending. +func nextReceivedSignal() (uint32, bool) { + val := receivedSignals.Load() + if val == 0 { + return 0, false + } - // Atomically clear the signal number from receivedSignals. - receivedSignals.And(^(uint32(1) << num)) + // Extract the lowest numbered signal number from receivedSignals. + num := uint32(bits.TrailingZeros32(val)) - return num - } -} + // Atomically clear the signal number from receivedSignals. + receivedSignals.And(^(uint32(1) << num)) -// Reactivate the goroutine waiting for signals, if there are any. -// Return true if it was reactivated (and therefore the scheduler should run -// again), and false otherwise. -func checkSignals() bool { - if receivedSignals.Load() != 0 { - if waiter := signalRecvWaiter.Swap(nil); waiter != nil { - scheduleTask(waiter) - return true - } - } - return false + return num, true } func waitForEvents() { diff --git a/src/runtime/signal_cooperative.go b/src/runtime/signal_cooperative.go new file mode 100644 index 0000000000..f334979dd5 --- /dev/null +++ b/src/runtime/signal_cooperative.go @@ -0,0 +1,60 @@ +//go:build (darwin || (linux && !baremetal && !wasip1 && !wasm_unknown && !wasip2 && !nintendoswitch)) && !scheduler.threads + +package runtime + +import ( + "internal/task" + "sync/atomic" +) + +// The goroutine inside os/signal that reads signals is an ordinary task here. +// It is parked with task.Pause and resumed from checkSignals below. + +// Task waiting for a signal to arrive, or nil if it is running or there are no +// signals. +var signalRecvWaiter atomic.Pointer[task.Task] + +//go:linkname signal_recv os/signal.signal_recv +func signal_recv() uint32 { + // Function called from os/signal to get the next received signal. + for { + if num, ok := nextReceivedSignal(); ok { + return num + } + + // There are no signals to receive. Sleep until there are. + if signalRecvWaiter.Swap(task.Current()) != nil { + // We expect only a single goroutine to call signal_recv. + runtimeFatal("signal_recv called concurrently") + } + task.Pause() + } +} + +//go:linkname signal_waitUntilIdle os/signal.signalWaitUntilIdle +func signal_waitUntilIdle() { + // Wait until signal_recv has processed all signals. Yielding is enough: + // the scheduler runs signal_recv, which is the only thing that empties + // receivedSignals. + for receivedSignals.Load() != 0 { + Gosched() + } +} + +// Called from the signal handler. The waiting task is resumed by checkSignals +// instead, from the scheduler, so there is nothing to do here. +func signalRecvWake() { +} + +// Reactivate the goroutine waiting for signals, if there are any. +// Return true if it was reactivated (and therefore the scheduler should run +// again), and false otherwise. +func checkSignals() bool { + if receivedSignals.Load() != 0 { + if waiter := signalRecvWaiter.Swap(nil); waiter != nil { + scheduleTask(waiter) + return true + } + } + return false +} diff --git a/src/runtime/signal_threads.go b/src/runtime/signal_threads.go new file mode 100644 index 0000000000..a215b037d0 --- /dev/null +++ b/src/runtime/signal_threads.go @@ -0,0 +1,69 @@ +//go:build (darwin || (linux && !baremetal && !wasip1 && !wasm_unknown && !wasip2 && !nintendoswitch)) && scheduler.threads + +package runtime + +import ( + "internal/futex" +) + +// Futex the receiver in os/signal waits on. Its value is 1 when the handler +// has stored a signal that the receiver did not read yet. +// +// It cannot wait on signalFutex, because sleepTicks waits on that one too and +// swaps it back to zero, which would lose the wakeup of the receiver. +var signalRecvFutex futex.Futex + +// Futex signalWaitUntilIdle waits on. Its value is always zero, so it is only +// a wakeup address. The wait has a timeout because a wake that arrives before +// the wait starts is not remembered. +var signalIdleFutex futex.Futex + +// How long signalWaitUntilIdle blocks before rechecking on its own. +const signalIdlePoll = 1e6 // 1ms, in nanoseconds + +//go:linkname signal_recv os/signal.signal_recv +func signal_recv() uint32 { + // Function called from os/signal to get the next received signal. + for { + if num, ok := nextReceivedSignal(); ok { + if receivedSignals.Load() == 0 { + // That was the last pending signal, so signalWaitUntilIdle can + // return now. + signalIdleFutex.WakeAll() + } + return num + } + + // Clear the flag and then read receivedSignals again. The handler + // stores the signal before the flag, so no wakeup is lost. + signalRecvFutex.Store(0) + if receivedSignals.Load() != 0 { + continue + } + signalRecvFutex.Wait(0) + } +} + +//go:linkname signal_waitUntilIdle os/signal.signalWaitUntilIdle +func signal_waitUntilIdle() { + // Wait until signal_recv has processed all signals. Gosched is a no-op + // with threads, so this must block. + for receivedSignals.Load() != 0 { + signalIdleFutex.WaitUntil(0, signalIdlePoll) + } +} + +// Called from the signal handler to wake signal_recv. An atomic store and a +// futex wake syscall are both safe in a signal handler. +func signalRecvWake() { + if signalRecvFutex.Swap(1) == 0 { + // Changed from 0 to 1, so signal_recv may be waiting on it. + signalRecvFutex.WakeAll() + } +} + +// Reactivate the goroutine waiting for signals, if there are any. There is no +// such goroutine here, because the handler wakes the receiver directly. +func checkSignals() bool { + return false +} diff --git a/testdata/signal.go b/testdata/signal.go index a82991f086..164561fd7c 100644 --- a/testdata/signal.go +++ b/testdata/signal.go @@ -12,17 +12,24 @@ import ( ) func main() { + // A signal must reach the channel while the program does nothing else. The + // receive below is the only thing that runs, so no timer and no sleep can + // carry the delivery. c := make(chan os.Signal, 1) signal.Notify(c, syscall.SIGUSR1) + syscall.Kill(syscall.Getpid(), syscall.SIGUSR1) + report(<-c) + signal.Stop(c) + + // The same again, with a goroutine that reads the channel while the main + // goroutine sleeps. + c2 := make(chan os.Signal, 1) + signal.Notify(c2, 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()) - } + for sig := range c2 { + report(sig) } }() @@ -36,7 +43,15 @@ func main() { // in a unit test). signal.Ignore(syscall.SIGUSR1) - signal.Stop(c) + signal.Stop(c2) println("exiting signal program") } + +func report(sig os.Signal) { + if sig == syscall.SIGUSR1 { + println("got expected signal") + } else { + println("got signal:", sig.String()) + } +} diff --git a/testdata/signal.txt b/testdata/signal.txt index c4726d7174..294eba06e3 100644 --- a/testdata/signal.txt +++ b/testdata/signal.txt @@ -1,2 +1,3 @@ got expected signal +got expected signal exiting signal program