Skip to content

Commit b8fe75a

Browse files
aykevldeadprogram
authored andcommitted
runtime: add support for os/signal
This adds support for enabling and listening to signals on Linux and MacOS.
1 parent 0f95b41 commit b8fe75a

File tree

9 files changed

+374
-19
lines changed

9 files changed

+374
-19
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
@@ -391,7 +391,8 @@ func defaultTarget(options *Options) (*TargetSpec, error) {
391391
)
392392
spec.ExtraFiles = append(spec.ExtraFiles,
393393
"src/runtime/os_darwin.c",
394-
"src/runtime/runtime_unix.c")
394+
"src/runtime/runtime_unix.c",
395+
"src/runtime/signal.c")
395396
case "linux":
396397
spec.Linker = "ld.lld"
397398
spec.RTLib = "compiler-rt"
@@ -412,7 +413,8 @@ func defaultTarget(options *Options) (*TargetSpec, error) {
412413
spec.CFlags = append(spec.CFlags, "-mno-outline-atomics")
413414
}
414415
spec.ExtraFiles = append(spec.ExtraFiles,
415-
"src/runtime/runtime_unix.c")
416+
"src/runtime/runtime_unix.c",
417+
"src/runtime/signal.c")
416418
case "windows":
417419
spec.Linker = "ld.lld"
418420
spec.Libc = "mingw-w64"

main_test.go

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -79,6 +79,7 @@ func TestBuild(t *testing.T) {
7979
"oldgo/",
8080
"print.go",
8181
"reflect.go",
82+
"signal.go",
8283
"slice.go",
8384
"sort.go",
8485
"stdlib.go",
@@ -217,6 +218,7 @@ func runPlatTests(options compileopts.Options, tests []string, t *testing.T) {
217218
// isWebAssembly := strings.HasPrefix(spec.Triple, "wasm")
218219
isWASI := strings.HasPrefix(options.Target, "wasi")
219220
isWebAssembly := isWASI || strings.HasPrefix(options.Target, "wasm") || (options.Target == "" && strings.HasPrefix(options.GOARCH, "wasm"))
221+
isBaremetal := options.Target == "simavr" || options.Target == "cortex-m-qemu" || options.Target == "riscv-qemu"
220222

221223
for _, name := range tests {
222224
if options.GOOS == "linux" && (options.GOARCH == "arm" || options.GOARCH == "386") {
@@ -281,6 +283,13 @@ func runPlatTests(options compileopts.Options, tests []string, t *testing.T) {
281283
continue
282284
}
283285
}
286+
if isWebAssembly || isBaremetal || options.GOOS == "windows" {
287+
switch name {
288+
case "signal.go":
289+
// Signals only work on POSIX-like systems.
290+
continue
291+
}
292+
}
284293

285294
name := name // redefine to avoid race condition
286295
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: 226 additions & 2 deletions
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,47 @@ func nanosecondsToTicks(ns int64) timeUnit {
217222
}
218223

219224
func sleepTicks(d timeUnit) {
220-
// timeUnit is in nanoseconds, so need to convert to microseconds here.
221-
usleep(uint(d) / 1000)
225+
// When there are no signal handlers present, we can simply go to sleep.
226+
if !hasSignals {
227+
// timeUnit is in nanoseconds, so need to convert to microseconds here.
228+
usleep(uint(d) / 1000)
229+
return
230+
}
231+
232+
if GOOS == "darwin" {
233+
// Check for incoming signals.
234+
if checkSignals() {
235+
// Received a signal, so there's probably at least one goroutine
236+
// that's runnable again.
237+
return
238+
}
239+
240+
// WARNING: there is a race condition here. If a signal arrives between
241+
// checkSignals() and usleep(), the usleep() call will not exit early so
242+
// the signal is delayed until usleep finishes or another signal
243+
// arrives.
244+
// There doesn't appear to be a simple way to fix this on MacOS.
245+
246+
// timeUnit is in nanoseconds, so need to convert to microseconds here.
247+
result := usleep(uint(d) / 1000)
248+
if result != 0 {
249+
checkSignals()
250+
}
251+
} else {
252+
// Linux (and various other POSIX systems) implement sigtimedwait so we
253+
// can do this in a non-racy way.
254+
tinygo_wfi_mask(activeSignals)
255+
if checkSignals() {
256+
tinygo_wfi_unmask()
257+
return
258+
}
259+
signal := tinygo_wfi_sleep(activeSignals, uint64(d))
260+
if signal >= 0 {
261+
tinygo_signal_handler(signal)
262+
checkSignals()
263+
}
264+
tinygo_wfi_unmask()
265+
}
222266
}
223267

224268
func getTime(clock int32) uint64 {
@@ -307,3 +351,183 @@ func growHeap() bool {
307351
setHeapEnd(heapStart + heapSize)
308352
return true
309353
}
354+
355+
func init() {
356+
// Set up a channel to receive signals into.
357+
signalChan = make(chan uint32, 1)
358+
}
359+
360+
var signalChan chan uint32
361+
362+
// Indicate whether signals have been registered.
363+
var hasSignals bool
364+
365+
// Mask of signals that have been received. The signal handler atomically ORs
366+
// signals into this value.
367+
var receivedSignals uint32
368+
369+
var activeSignals uint32
370+
371+
//go:linkname signal_enable os/signal.signal_enable
372+
func signal_enable(s uint32) {
373+
if s >= 32 {
374+
// TODO: to support higher signal numbers, we need to turn
375+
// receivedSignals into a uint32 array.
376+
runtimePanicAt(returnAddress(0), "unsupported signal number")
377+
}
378+
hasSignals = true
379+
activeSignals |= 1 << s
380+
// It's easier to implement this function in C.
381+
tinygo_signal_enable(s)
382+
}
383+
384+
//go:linkname signal_ignore os/signal.signal_ignore
385+
func signal_ignore(s uint32) {
386+
if s >= 32 {
387+
// TODO: to support higher signal numbers, we need to turn
388+
// receivedSignals into a uint32 array.
389+
runtimePanicAt(returnAddress(0), "unsupported signal number")
390+
}
391+
activeSignals &^= 1 << s
392+
tinygo_signal_ignore(s)
393+
}
394+
395+
//go:linkname signal_disable os/signal.signal_disable
396+
func signal_disable(s uint32) {
397+
if s >= 32 {
398+
// TODO: to support higher signal numbers, we need to turn
399+
// receivedSignals into a uint32 array.
400+
runtimePanicAt(returnAddress(0), "unsupported signal number")
401+
}
402+
activeSignals &^= 1 << s
403+
tinygo_signal_disable(s)
404+
}
405+
406+
//go:linkname signal_waitUntilIdle os/signal.signalWaitUntilIdle
407+
func signal_waitUntilIdle() {
408+
// Make sure all signals are sent on the channel.
409+
for atomic.LoadUint32(&receivedSignals) != 0 {
410+
checkSignals()
411+
Gosched()
412+
}
413+
414+
// Make sure all signals are processed.
415+
for len(signalChan) != 0 {
416+
Gosched()
417+
}
418+
}
419+
420+
//export tinygo_signal_enable
421+
func tinygo_signal_enable(s uint32)
422+
423+
//export tinygo_signal_ignore
424+
func tinygo_signal_ignore(s uint32)
425+
426+
//export tinygo_signal_disable
427+
func tinygo_signal_disable(s uint32)
428+
429+
// void tinygo_signal_handler(int sig);
430+
//
431+
//export tinygo_signal_handler
432+
func tinygo_signal_handler(s int32) {
433+
// This loop is essentially the atomic equivalent of the following:
434+
//
435+
// receivedSignals |= 1 << s
436+
//
437+
// TODO: use atomic.Uint32.And once we drop support for Go 1.22 instead of
438+
// this loop.
439+
for {
440+
mask := uint32(1) << uint32(s)
441+
val := atomic.LoadUint32(&receivedSignals)
442+
swapped := atomic.CompareAndSwapUint32(&receivedSignals, val, val|mask)
443+
if swapped {
444+
break
445+
}
446+
}
447+
}
448+
449+
//go:linkname signal_recv os/signal.signal_recv
450+
func signal_recv() uint32 {
451+
// Function called from os/signal to get the next received signal.
452+
val := <-signalChan
453+
checkSignals()
454+
return val
455+
}
456+
457+
// Atomically find a signal that previously occured and send it into the
458+
// signalChan channel. Return true if at least one signal was delivered this
459+
// way, false otherwise.
460+
func checkSignals() bool {
461+
gotSignals := false
462+
for {
463+
// Extract the lowest numbered signal number from receivedSignals.
464+
val := atomic.LoadUint32(&receivedSignals)
465+
if val == 0 {
466+
// There is no signal ready to be received by the program (common
467+
// case).
468+
return gotSignals
469+
}
470+
num := uint32(bits.TrailingZeros32(val))
471+
472+
// Do a non-blocking send on signalChan.
473+
select {
474+
case signalChan <- num:
475+
// There was room free in the channel, so remove the signal number
476+
// from the receivedSignals mask.
477+
gotSignals = true
478+
default:
479+
// Could not send the signal number on the channel. This means
480+
// there's still a signal pending. In that case, let it be received
481+
// at which point checkSignals is called again to put the next one
482+
// in the channel buffer.
483+
return gotSignals
484+
}
485+
486+
// Atomically clear the signal number from receivedSignals.
487+
// TODO: use atomic.Uint32.Or once we drop support for Go 1.22 instead
488+
// of this loop.
489+
for {
490+
newVal := val &^ (1 << num)
491+
swapped := atomic.CompareAndSwapUint32(&receivedSignals, val, newVal)
492+
if swapped {
493+
break
494+
}
495+
val = atomic.LoadUint32(&receivedSignals)
496+
}
497+
}
498+
}
499+
500+
//export tinygo_wfi_mask
501+
func tinygo_wfi_mask(active uint32)
502+
503+
//export tinygo_wfi_sleep
504+
func tinygo_wfi_sleep(active uint32, timeout uint64) int32
505+
506+
//export tinygo_wfi_wait
507+
func tinygo_wfi_wait(active uint32) int32
508+
509+
//export tinygo_wfi_unmask
510+
func tinygo_wfi_unmask()
511+
512+
func waitForEvents() {
513+
if hasSignals {
514+
// We could have used pause() here, but that function is impossible to
515+
// use in a race-free way:
516+
// https://www.cipht.net/2023/11/30/perils-of-pause.html
517+
// Therefore we need something better.
518+
// Note: this is unsafe with multithreading, because sigprocmask is only
519+
// defined for single-threaded applictions.
520+
tinygo_wfi_mask(activeSignals)
521+
if checkSignals() {
522+
tinygo_wfi_unmask()
523+
return
524+
}
525+
signal := tinygo_wfi_wait(activeSignals)
526+
tinygo_signal_handler(signal)
527+
checkSignals()
528+
tinygo_wfi_unmask()
529+
} else {
530+
// The program doesn't use signals, so this is a deadlock.
531+
runtimePanic("deadlocked: no event source")
532+
}
533+
}

0 commit comments

Comments
 (0)