Description
The runtime registers all of its signal handlers with the SA_RESTART
and SA_ONSTACK
flags. It enforces that other handlers are registered with SA_ONSTACK
, but does not enforce SA_RESTART
.
Since the Go runtime does not register very many handlers when using -buildmode=c-archive
and -buildmode=c-shared
, other libraries that register handlers will not see an existing handler using SA_RESTART
and thus will not know to propagate it. So in those programs, it is fairly likely that some handler will be registered without SA_RESTART
.
Unfortunately, it appears that the Go standard library is written based on the assumption that all handlers use SA_RESTART
. System calls in the standard library do not consistently check for the EINTR
error and do not document its possibility. For example, many users would be surprised to learn that (*os.Cmd).CombinedOutput
can return an os.SyscallError
wrapping EINTR
, as illustrated by the program below:
bcmills:~/src$ go version
go version devel +b53acd89db Tue May 16 17:15:11 2017 +0000 linux/amd64
eintr/eintr.go:
package main
import "C"
import (
"fmt"
"os"
"os/exec"
"runtime"
"syscall"
"time"
)
//export go_annoy
func go_annoy(sig, seconds C.int) {
annoy(syscall.Signal(sig), time.Duration(seconds)*time.Second)
}
func annoy(sig syscall.Signal, d time.Duration) {
runtime.LockOSThread()
pid := syscall.Getpid()
tid := syscall.Gettid()
exit := make(chan bool)
go func() {
for {
select {
case <-exit:
return
default:
}
if err := syscall.Tgkill(pid, tid, sig); err != nil {
panic(err)
}
}
}()
started := time.Now()
for time.Since(started) < d {
cmd := exec.Command("/bin/echo", "Are we there yet?")
_, err := cmd.CombinedOutput()
if err != nil {
fmt.Fprintln(os.Stderr, "exec.Cmd error: ", err)
os.Exit(1)
}
}
exit <- true
}
func main() {}
eintr/main/eintr.c:
#include <signal.h>
#include <stddef.h>
#include <string.h>
#include "eintr.h"
static void ignore_signal(int signo, siginfo_t *info, void *context) {
}
int main() {
struct sigaction sa;
memset(&sa, 0, sizeof(sa));
sigemptyset(&sa.sa_mask);
sa.sa_flags = SA_SIGINFO | SA_ONSTACK;
sa.sa_sigaction = ignore_signal;
sigaction(SIGUSR1, &sa, NULL);
go_annoy(SIGUSR1, 10);
return 0;
}
bcmills:~/src$ go build -buildmode=c-archive eintr
bcmills:~/src$ $(go env CC) -pthread -static -I. eintr/main/eintr.c ./eintr.a -o eintr_main
bcmills:~/src$ ./eintr_main
exec.Cmd error: fork/exec /bin/echo: interrupted system call
bcmills:~/src$ ./eintr_main
exec.Cmd error: waitid: interrupted system call
In typical usage, the Go portion of the program would have only the exec.Command
portion of the annoy
function; the signal would originate externally (either from some other process, or perhaps from some other language runtime within the process).
I can think of three ways we could deal with this issue:
- Document that all signal handlers in a program containing a Go runtime must be registered with
SA_RESTART
and treat it the same way we do forSA_ONSTACK
. - Consistently check for
EINTR
throughout the standard library and transparently retry. - Document that many Go functions can return
EINTR
and require end-users to check for it explicitly.
I believe that (3) is strictly worse than (2): if we can't handle EINTR
correctly and consistently within the runtime, we can't reasonably expect users to do so.
I am not sure whether (1) is feasible. In particular, it might prevent the use of the Go runtime with many other languages (such languages that execute on a JVM).
That leaves us with (2). I am not familiar enough with syscall usage in the standard library to evaluate whether it is feasible.
(CC: @ianlancetaylor @mdempsky )