Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
66 changes: 17 additions & 49 deletions src/runtime/runtime_unix.go
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,6 @@ package runtime

import (
"internal/futex"
"internal/task"
"math/bits"
"sync/atomic"
"tinygo"
Expand Down Expand Up @@ -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)

Expand All @@ -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() {
Expand Down
60 changes: 60 additions & 0 deletions src/runtime/signal_cooperative.go
Original file line number Diff line number Diff line change
@@ -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
}
69 changes: 69 additions & 0 deletions src/runtime/signal_threads.go
Original file line number Diff line number Diff line change
@@ -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
}
29 changes: 22 additions & 7 deletions testdata/signal.go
Original file line number Diff line number Diff line change
Expand Up @@ -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)
}
}()

Expand All @@ -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())
}
}
1 change: 1 addition & 0 deletions testdata/signal.txt
Original file line number Diff line number Diff line change
@@ -1,2 +1,3 @@
got expected signal
got expected signal
exiting signal program