Skip to content

Commit 246de9c

Browse files
committed
runtime: add support for os/signal
This adds support for enabling and listening to signals on Linux and MacOS. TODO: also support disabling signals.
1 parent b5626e7 commit 246de9c

File tree

9 files changed

+210
-18
lines changed

9 files changed

+210
-18
lines changed

builder/musl.go

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -128,6 +128,7 @@ var libMusl = Library{
128128
"mman/*.c",
129129
"math/*.c",
130130
"multibyte/*.c",
131+
"signal/" + arch + "/*.s",
131132
"signal/*.c",
132133
"stdio/*.c",
133134
"string/*.c",

compileopts/target.go

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -390,7 +390,8 @@ func defaultTarget(options *Options) (*TargetSpec, error) {
390390
)
391391
spec.ExtraFiles = append(spec.ExtraFiles,
392392
"src/runtime/os_darwin.c",
393-
"src/runtime/runtime_unix.c")
393+
"src/runtime/runtime_unix.c",
394+
"src/runtime/signal.c")
394395
case "linux":
395396
spec.Linker = "ld.lld"
396397
spec.RTLib = "compiler-rt"
@@ -411,7 +412,8 @@ func defaultTarget(options *Options) (*TargetSpec, error) {
411412
spec.CFlags = append(spec.CFlags, "-mno-outline-atomics")
412413
}
413414
spec.ExtraFiles = append(spec.ExtraFiles,
414-
"src/runtime/runtime_unix.c")
415+
"src/runtime/runtime_unix.c",
416+
"src/runtime/signal.c")
415417
case "windows":
416418
spec.Linker = "ld.lld"
417419
spec.Libc = "mingw-w64"

main_test.go

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -75,6 +75,7 @@ func TestBuild(t *testing.T) {
7575
"oldgo/",
7676
"print.go",
7777
"reflect.go",
78+
"signal.go",
7879
"slice.go",
7980
"sort.go",
8081
"stdlib.go",
@@ -213,6 +214,7 @@ func runPlatTests(options compileopts.Options, tests []string, t *testing.T) {
213214
// isWebAssembly := strings.HasPrefix(spec.Triple, "wasm")
214215
isWASI := strings.HasPrefix(options.Target, "wasi")
215216
isWebAssembly := isWASI || strings.HasPrefix(options.Target, "wasm") || (options.Target == "" && strings.HasPrefix(options.GOARCH, "wasm"))
217+
isBaremetal := options.Target == "simavr" || options.Target == "cortex-m-qemu" || options.Target == "riscv-qemu"
216218

217219
for _, name := range tests {
218220
if options.GOOS == "linux" && (options.GOARCH == "arm" || options.GOARCH == "386") {
@@ -277,6 +279,13 @@ func runPlatTests(options compileopts.Options, tests []string, t *testing.T) {
277279
continue
278280
}
279281
}
282+
if isWebAssembly || isBaremetal || options.GOOS == "windows" {
283+
switch name {
284+
case "signal/":
285+
// Signals only work on POSIX-like systems.
286+
continue
287+
}
288+
}
280289

281290
name := name // redefine to avoid race condition
282291
t.Run(name, func(t *testing.T) {

src/os/signal/signal.go

Lines changed: 0 additions & 14 deletions
This file was deleted.

src/runtime/runtime_unix.go

Lines changed: 141 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,8 @@
33
package runtime
44

55
import (
6+
"math/bits"
7+
"sync/atomic"
68
"unsafe"
79
)
810

@@ -12,6 +14,9 @@ func libc_write(fd int32, buf unsafe.Pointer, count uint) int
1214
//export usleep
1315
func usleep(usec uint) int
1416

17+
//export pause
18+
func pause() int32
19+
1520
// void *mmap(void *addr, size_t length, int prot, int flags, int fd, off_t offset);
1621
// Note: off_t is defined as int64 because:
1722
// - musl (used on Linux) always defines it as int64
@@ -217,8 +222,22 @@ func nanosecondsToTicks(ns int64) timeUnit {
217222
}
218223

219224
func sleepTicks(d timeUnit) {
225+
// Check for incoming signals.
226+
if checkSignals() {
227+
// Received a signal, so there's probably at least one goroutine that's
228+
// runnable again.
229+
return
230+
}
231+
232+
// TODO: there is a race condition here. If a signal arrives between
233+
// checkSignals() and usleep(), the usleep() call will not exit early so the
234+
// signal is delayed until usleep finishes or another signal arrives.
235+
220236
// timeUnit is in nanoseconds, so need to convert to microseconds here.
221-
usleep(uint(d) / 1000)
237+
result := usleep(uint(d) / 1000)
238+
if result != 0 {
239+
checkSignals()
240+
}
222241
}
223242

224243
func getTime(clock int32) uint64 {
@@ -307,3 +326,124 @@ func growHeap() bool {
307326
setHeapEnd(heapStart + heapSize)
308327
return true
309328
}
329+
330+
func init() {
331+
// Set up a channel to receive signals into.
332+
signalChan = make(chan uint32, 1)
333+
}
334+
335+
var signalChan chan uint32
336+
337+
// Simple boolean that's true when any signals have been registered.
338+
var hasSignals uint32
339+
340+
// Mask of signals that have been received. The signal handler atomically ORs
341+
// signals into this value.
342+
var receivedSignals uint32
343+
344+
//go:linkname signal_enable os/signal.signal_enable
345+
func signal_enable(s uint32) {
346+
if s >= 32 {
347+
// TODO: to support higher signal numbers, we need to turn
348+
// receivedSignals into a uint32 array.
349+
runtimePanicAt(returnAddress(0), "unsupported signal number")
350+
}
351+
atomic.StoreUint32(&hasSignals, 1)
352+
// It's easier to implement this function in C.
353+
tinygo_signal_enable(s)
354+
}
355+
356+
//export tinygo_signal_enable
357+
func tinygo_signal_enable(s uint32)
358+
359+
// void tinygo_signal_handler(int sig);
360+
//
361+
//export tinygo_signal_handler
362+
func tinygo_signal_handler(s int32) {
363+
// This loop is essentially the atomic equivalent of the following:
364+
//
365+
// receivedSignals |= 1 << s
366+
//
367+
// TODO: use atomic.Uint32.And once we drop support for Go 1.22 instead of
368+
// this loop.
369+
for {
370+
mask := uint32(1) << uint32(s)
371+
val := atomic.LoadUint32(&receivedSignals)
372+
swapped := atomic.CompareAndSwapUint32(&receivedSignals, val, val|mask)
373+
if swapped {
374+
break
375+
}
376+
}
377+
}
378+
379+
//go:linkname signal_recv os/signal.signal_recv
380+
func signal_recv() uint32 {
381+
// Function called from os/signal to get the next received signal.
382+
val := <-signalChan
383+
checkSignals()
384+
return val
385+
}
386+
387+
// Atomically find a signal that previously occured and send it into the
388+
// signalChan channel. Return true if at least one signal was delivered this
389+
// way, false otherwise.
390+
func checkSignals() bool {
391+
gotSignals := false
392+
for {
393+
// Extract the lowest numbered signal number from receivedSignals.
394+
val := atomic.LoadUint32(&receivedSignals)
395+
if val == 0 {
396+
// There is no signal ready to be received by the program (common
397+
// case).
398+
return gotSignals
399+
}
400+
num := uint32(bits.TrailingZeros32(val))
401+
402+
// Do a non-blocking send on signalChan.
403+
select {
404+
case signalChan <- num:
405+
// There was room free in the channel, so remove the signal number
406+
// from the receivedSignals mask.
407+
gotSignals = true
408+
default:
409+
// Could not send the signal number on the channel. This means
410+
// there's still a signal pending. In that case, let it be received
411+
// at which point checkSignals is called again to put the next one
412+
// in the channel buffer.
413+
return gotSignals
414+
}
415+
416+
// Atomically clear the signal number from receivedSignals.
417+
// TODO: use atomic.Uint32.Or once we drop support for Go 1.22 instead
418+
// of this loop.
419+
for {
420+
newVal := val &^ (1 << num)
421+
swapped := atomic.CompareAndSwapUint32(&receivedSignals, val, newVal)
422+
if swapped {
423+
break
424+
}
425+
val = atomic.LoadUint32(&receivedSignals)
426+
}
427+
}
428+
}
429+
430+
func waitForEvents() {
431+
if atomic.LoadUint32(&hasSignals) != 0 {
432+
// TODO: there is a race condition here. If a signal arrives between
433+
// checkSignals() and pause(), pause() will not exit early but instead
434+
// be delayed until the next signal arrives.
435+
// We should use something like this instead to avoid it:
436+
// - mask all active signals
437+
// - run checkSignals()
438+
// - run sigsuspend() with all active signals
439+
// - unmask all active signals
440+
// For a longer explanation of the problem, see:
441+
// https://www.cipht.net/2023/11/30/perils-of-pause.html
442+
checkSignals()
443+
pause()
444+
checkSignals()
445+
} else {
446+
// The program doesn't use signals, so this is a deadlock.
447+
runtimePanic("deadlocked: no event source")
448+
}
449+
}

src/runtime/signal.c

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
//go:build none
2+
3+
// Ignore the //go:build above. This file is manually included on Linux and
4+
// MacOS to provide os/signal support.
5+
6+
#include <stdint.h>
7+
#include <signal.h>
8+
#include <unistd.h>
9+
10+
// Signal handler in the runtime.
11+
void tinygo_signal_handler(int sig);
12+
13+
// Enable a signal from the runtime.
14+
void tinygo_signal_enable(uint32_t sig) {
15+
struct sigaction act = { 0 };
16+
act.sa_handler = &tinygo_signal_handler;
17+
sigaction(sig, &act, NULL);
18+
}

src/runtime/wait_other.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
//go:build !tinygo.riscv && !cortexm
1+
//go:build !tinygo.riscv && !cortexm && !linux && !darwin
22

33
package runtime
44

testdata/signal.go

Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,34 @@
1+
package main
2+
3+
// Test POSIX signals.
4+
// TODO: run `tinygo test os/signal` instead, once CGo errno return values are
5+
// supported.
6+
7+
import (
8+
"os"
9+
"os/signal"
10+
"syscall"
11+
"time"
12+
)
13+
14+
func main() {
15+
c := make(chan os.Signal, 1)
16+
signal.Notify(c, syscall.SIGUSR1)
17+
18+
// Wait for signals to arrive.
19+
go func() {
20+
for sig := range c {
21+
if sig == syscall.SIGUSR1 {
22+
println("got expected signal")
23+
} else {
24+
println("got signal:", sig.String())
25+
}
26+
}
27+
}()
28+
29+
// Send the signal.
30+
syscall.Kill(syscall.Getpid(), syscall.SIGUSR1)
31+
32+
time.Sleep(time.Millisecond * 100)
33+
println("exiting signal program")
34+
}

testdata/signal.txt

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
got expected signal
2+
exiting signal program

0 commit comments

Comments
 (0)