Skip to content

Commit

Permalink
runtime: add support for os/signal
Browse files Browse the repository at this point in the history
This adds support for enabling and listening to signals on Linux and
MacOS.
  • Loading branch information
aykevl committed Sep 11, 2024
1 parent b5626e7 commit 21e7a41
Show file tree
Hide file tree
Showing 9 changed files with 270 additions and 18 deletions.
1 change: 1 addition & 0 deletions builder/musl.go
Original file line number Diff line number Diff line change
Expand Up @@ -128,6 +128,7 @@ var libMusl = Library{
"mman/*.c",
"math/*.c",
"multibyte/*.c",
"signal/" + arch + "/*.s",
"signal/*.c",
"stdio/*.c",
"string/*.c",
Expand Down
6 changes: 4 additions & 2 deletions compileopts/target.go
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand All @@ -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"
Expand Down
9 changes: 9 additions & 0 deletions main_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -75,6 +75,7 @@ func TestBuild(t *testing.T) {
"oldgo/",
"print.go",
"reflect.go",
"signal.go",
"slice.go",
"sort.go",
"stdlib.go",
Expand Down Expand Up @@ -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") {
Expand Down Expand Up @@ -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) {
Expand Down
14 changes: 0 additions & 14 deletions src/os/signal/signal.go

This file was deleted.

182 changes: 181 additions & 1 deletion src/runtime/runtime_unix.go
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,8 @@
package runtime

import (
"math/bits"
"sync/atomic"
"unsafe"
)

Expand All @@ -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
Expand Down Expand Up @@ -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 {
Expand Down Expand Up @@ -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")
}
}
30 changes: 30 additions & 0 deletions src/runtime/signal.c
Original file line number Diff line number Diff line change
@@ -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 <stdint.h>
#include <signal.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);
}
2 changes: 1 addition & 1 deletion src/runtime/wait_other.go
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
//go:build !tinygo.riscv && !cortexm
//go:build !tinygo.riscv && !cortexm && !(linux && !baremetal && !tinygo.wasm) && !darwin

package runtime

Expand Down
42 changes: 42 additions & 0 deletions testdata/signal.go
Original file line number Diff line number Diff line change
@@ -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")
}
2 changes: 2 additions & 0 deletions testdata/signal.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
got expected signal
exiting signal program

0 comments on commit 21e7a41

Please sign in to comment.