Skip to content

Commit 2811f2d

Browse files
committed
runtime: add support for os/signal
This adds support for enabling and listening to signals on Linux and MacOS.
1 parent b5626e7 commit 2811f2d

File tree

9 files changed

+270
-18
lines changed

9 files changed

+270
-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.go":
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: 181 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,164 @@ 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+
//go:linkname signal_ignore os/signal.signal_ignore
357+
func signal_ignore(s uint32) {
358+
if s >= 32 {
359+
// TODO: to support higher signal numbers, we need to turn
360+
// receivedSignals into a uint32 array.
361+
runtimePanicAt(returnAddress(0), "unsupported signal number")
362+
}
363+
tinygo_signal_ignore(s)
364+
}
365+
366+
//go:linkname signal_disable os/signal.signal_disable
367+
func signal_disable(s uint32) {
368+
if s >= 32 {
369+
// TODO: to support higher signal numbers, we need to turn
370+
// receivedSignals into a uint32 array.
371+
runtimePanicAt(returnAddress(0), "unsupported signal number")
372+
}
373+
tinygo_signal_disable(s)
374+
}
375+
376+
//go:linkname signal_waitUntilIdle os/signal.signalWaitUntilIdle
377+
func signal_waitUntilIdle() {
378+
// Make sure all signals are sent on the channel.
379+
for atomic.LoadUint32(&receivedSignals) != 0 {
380+
checkSignals()
381+
Gosched()
382+
}
383+
384+
// Make sure all signals are processed.
385+
for len(signalChan) != 0 {
386+
Gosched()
387+
}
388+
}
389+
390+
//export tinygo_signal_enable
391+
func tinygo_signal_enable(s uint32)
392+
393+
//export tinygo_signal_ignore
394+
func tinygo_signal_ignore(s uint32)
395+
396+
//export tinygo_signal_disable
397+
func tinygo_signal_disable(s uint32)
398+
399+
// void tinygo_signal_handler(int sig);
400+
//
401+
//export tinygo_signal_handler
402+
func tinygo_signal_handler(s int32) {
403+
// This loop is essentially the atomic equivalent of the following:
404+
//
405+
// receivedSignals |= 1 << s
406+
//
407+
// TODO: use atomic.Uint32.And once we drop support for Go 1.22 instead of
408+
// this loop.
409+
for {
410+
mask := uint32(1) << uint32(s)
411+
val := atomic.LoadUint32(&receivedSignals)
412+
swapped := atomic.CompareAndSwapUint32(&receivedSignals, val, val|mask)
413+
if swapped {
414+
break
415+
}
416+
}
417+
}
418+
419+
//go:linkname signal_recv os/signal.signal_recv
420+
func signal_recv() uint32 {
421+
// Function called from os/signal to get the next received signal.
422+
val := <-signalChan
423+
checkSignals()
424+
return val
425+
}
426+
427+
// Atomically find a signal that previously occured and send it into the
428+
// signalChan channel. Return true if at least one signal was delivered this
429+
// way, false otherwise.
430+
func checkSignals() bool {
431+
gotSignals := false
432+
for {
433+
// Extract the lowest numbered signal number from receivedSignals.
434+
val := atomic.LoadUint32(&receivedSignals)
435+
if val == 0 {
436+
// There is no signal ready to be received by the program (common
437+
// case).
438+
return gotSignals
439+
}
440+
num := uint32(bits.TrailingZeros32(val))
441+
442+
// Do a non-blocking send on signalChan.
443+
select {
444+
case signalChan <- num:
445+
// There was room free in the channel, so remove the signal number
446+
// from the receivedSignals mask.
447+
gotSignals = true
448+
default:
449+
// Could not send the signal number on the channel. This means
450+
// there's still a signal pending. In that case, let it be received
451+
// at which point checkSignals is called again to put the next one
452+
// in the channel buffer.
453+
return gotSignals
454+
}
455+
456+
// Atomically clear the signal number from receivedSignals.
457+
// TODO: use atomic.Uint32.Or once we drop support for Go 1.22 instead
458+
// of this loop.
459+
for {
460+
newVal := val &^ (1 << num)
461+
swapped := atomic.CompareAndSwapUint32(&receivedSignals, val, newVal)
462+
if swapped {
463+
break
464+
}
465+
val = atomic.LoadUint32(&receivedSignals)
466+
}
467+
}
468+
}
469+
470+
func waitForEvents() {
471+
if atomic.LoadUint32(&hasSignals) != 0 {
472+
// TODO: there is a race condition here. If a signal arrives between
473+
// checkSignals() and pause(), pause() will not exit early but instead
474+
// be delayed until the next signal arrives.
475+
// We should use something like this instead to avoid it:
476+
// - mask all active signals
477+
// - run checkSignals()
478+
// - run sigsuspend() with all active signals
479+
// - unmask all active signals
480+
// For a longer explanation of the problem, see:
481+
// https://www.cipht.net/2023/11/30/perils-of-pause.html
482+
checkSignals()
483+
pause()
484+
checkSignals()
485+
} else {
486+
// The program doesn't use signals, so this is a deadlock.
487+
runtimePanic("deadlocked: no event source")
488+
}
489+
}

src/runtime/signal.c

Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,30 @@
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+
}
19+
20+
void tinygo_signal_ignore(uint32_t sig) {
21+
struct sigaction act = { 0 };
22+
act.sa_handler = SIG_IGN;
23+
sigaction(sig, &act, NULL);
24+
}
25+
26+
void tinygo_signal_disable(uint32_t sig) {
27+
struct sigaction act = { 0 };
28+
act.sa_handler = SIG_DFL;
29+
sigaction(sig, &act, NULL);
30+
}

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 && !baremetal) && !darwin
22

33
package runtime
44

testdata/signal.go

Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,42 @@
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+
34+
// Stop notifying.
35+
// (This is just a smoke test, it's difficult to test the default behavior
36+
// in a unit test).
37+
signal.Ignore(syscall.SIGUSR1)
38+
39+
signal.Stop(c)
40+
41+
println("exiting signal program")
42+
}

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)