Skip to content

Commit

Permalink
os/exec: use pidfd for waiting and signaling of processes
Browse files Browse the repository at this point in the history
Using pidfd allows us to have a handle on the process and
poll the handle to non-blocking wait for the process to
exit.

Fixes golang#34396
Fixes golang#60321
Fixes golang#60320
  • Loading branch information
mitar committed May 26, 2023
1 parent f90b4cd commit 86ae506
Show file tree
Hide file tree
Showing 5 changed files with 142 additions and 63 deletions.
2 changes: 1 addition & 1 deletion src/os/exec.go
Original file line number Diff line number Diff line change
Expand Up @@ -85,7 +85,7 @@ func Getppid() int { return syscall.Getppid() }
// The Process it returns can be used to obtain information
// about the underlying operating system process.
//
// On Unix systems, FindProcess always succeeds and returns a Process
// On non-Linux Unix systems, FindProcess always succeeds and returns a Process
// for the given pid, regardless of whether the process exists.
func FindProcess(pid int) (*Process, error) {
return findProcess(pid)
Expand Down
36 changes: 18 additions & 18 deletions src/os/exec_posix.go
Original file line number Diff line number Diff line change
Expand Up @@ -69,9 +69,9 @@ func (p *Process) kill() error {

// ProcessState stores information about a process, as reported by Wait.
type ProcessState struct {
pid int // The process's id.
status syscall.WaitStatus // System-dependent status info.
rusage *syscall.Rusage
pid int // The process's id.
siginfo Siginfo // System-dependent status info.
rusage *syscall.Rusage
}

// Pid returns the process id of the exited process.
Expand All @@ -80,15 +80,15 @@ func (p *ProcessState) Pid() int {
}

func (p *ProcessState) exited() bool {
return p.status.Exited()
return p.siginfo.Exited()
}

func (p *ProcessState) success() bool {
return p.status.ExitStatus() == 0
return p.siginfo.ExitStatus() == 0
}

func (p *ProcessState) sys() any {
return p.status
return p.siginfo
}

func (p *ProcessState) sysUsage() any {
Expand All @@ -99,27 +99,27 @@ func (p *ProcessState) String() string {
if p == nil {
return "<nil>"
}
status := p.Sys().(syscall.WaitStatus)
siginfo := p.Sys().(Siginfo)
res := ""
switch {
case status.Exited():
code := status.ExitStatus()
case siginfo.Exited():
code := siginfo.ExitStatus()
if runtime.GOOS == "windows" && uint(code) >= 1<<16 { // windows uses large hex numbers
res = "exit status " + uitox(uint(code))
} else { // unix systems use small decimal integers
res = "exit status " + itoa.Itoa(code) // unix
}
case status.Signaled():
res = "signal: " + status.Signal().String()
case status.Stopped():
res = "stop signal: " + status.StopSignal().String()
if status.StopSignal() == syscall.SIGTRAP && status.TrapCause() != 0 {
res += " (trap " + itoa.Itoa(status.TrapCause()) + ")"
case siginfo.Signaled():
res = "signal: " + siginfo.Signal().String()
case siginfo.Stopped():
res = "stop signal: " + siginfo.StopSignal().String()
if siginfo.Trapped() && siginfo.TrapCause() != 0 {
res += " (trap " + itoa.Itoa(siginfo.TrapCause()) + ")"
}
case status.Continued():
case siginfo.Continued():
res = "continued"
}
if status.CoreDump() {
if siginfo.CoreDump() {
res += " (core dumped)"
}
return res
Expand All @@ -132,5 +132,5 @@ func (p *ProcessState) ExitCode() int {
if p == nil {
return -1
}
return p.status.ExitStatus()
return p.siginfo.ExitStatus()
}
113 changes: 94 additions & 19 deletions src/os/exec_unix.go
Original file line number Diff line number Diff line change
Expand Up @@ -11,8 +11,64 @@ import (
"runtime"
"syscall"
"time"
"unsafe"
)

const CLD_EXITED = 1
const CLD_KILLED = 2
const CLD_DUMPED = 3
const CLD_TRAPPED = 4
const CLD_STOPPED = 5
const CLD_CONTINUED = 6

type Siginfo struct {
Signo int32
Errno int32
Code int32
_ int32
_ [112]byte
}

func (s Siginfo) Exited() bool { return s.Code == CLD_EXITED }

func (s Siginfo) Signaled() bool { return s.Code == CLD_KILLED }

func (s Siginfo) Stopped() bool { return s.Code == CLD_STOPPED }

func (s Siginfo) Trapped() bool { return s.Code == CLD_TRAPPED }

func (s Siginfo) Continued() bool { return s.Code == CLD_CONTINUED }

func (s Siginfo) CoreDump() bool { return s.Code == CLD_DUMPED }

func (s Siginfo) ExitStatus() int {
if !s.Exited() {
return -1
}
return int(s.Errno)
}

func (s Siginfo) Signal() syscall.Signal {
if !s.Signaled() {
return -1
}
return syscall.Signal(s.Errno)
}

func (s Siginfo) StopSignal() syscall.Signal {
if !s.Signaled() {
return -1
}
return syscall.Signal(s.Errno)
}

func (s Siginfo) TrapCause() int {
if !s.Trapped() {
return -1
}
return int(s.Errno)
}

func (p *Process) wait() (ps *ProcessState, err error) {
if p.Pid == -1 {
return nil, syscall.EINVAL
Expand All @@ -34,27 +90,37 @@ func (p *Process) wait() (ps *ProcessState, err error) {
}

var (
status syscall.WaitStatus
rusage syscall.Rusage
pid1 int
e error
siginfo Siginfo
rusage syscall.Rusage
e syscall.Errno
)
for {
pid1, e = syscall.Wait4(p.Pid, &status, 0, &rusage)
if e != syscall.EINTR {
_, _, e = syscall.Syscall6(syscall.SYS_WAITID, _P_PIDFD, p.handle, uintptr(unsafe.Pointer(&siginfo)), syscall.WEXITED, uintptr(unsafe.Pointer(&rusage)), 0)
if e == syscall.EINTR {
continue
} else if e == syscall.ENOSYS {
// waitid has been available since Linux 2.6.9, but
// reportedly is not available in Ubuntu on Windows.
// See issue 16610.
panic("TODO: Implement fallback")
} else if e != 0 {
break
}
// During ptrace the wait might return also for non-exit reasons. In that case we retry.
// See: https://lwn.net/Articles/688624/
if siginfo.Exited() || siginfo.Signaled() || siginfo.CoreDump() {
break
}
}
if e != nil {
return nil, NewSyscallError("wait", e)
}
if pid1 != 0 {
p.setDone()
runtime.KeepAlive(p)
if e != 0 {
return nil, NewSyscallError("waitid", e)
}
p.setDone()
ps = &ProcessState{
pid: pid1,
status: status,
rusage: &rusage,
pid: p.Pid,
siginfo: siginfo,
rusage: &rusage,
}
return ps, nil
}
Expand All @@ -75,26 +141,35 @@ func (p *Process) signal(sig Signal) error {
if !ok {
return errors.New("os: unsupported signal type")
}
if e := syscall.Kill(p.Pid, s); e != nil {
if _, _, e := syscall.RawSyscall6(syscall.SYS_PIDFD_SEND_SIGNAL, p.handle, uintptr(s), 0, 0, 0, 0); e != 0 {
if e == syscall.ESRCH {
return ErrProcessDone
}
return e
return NewSyscallError("pidfd_send_signal", e)
}
runtime.KeepAlive(p)
return nil
}

func (p *Process) release() error {
// NOOP for unix.
e := syscall.Close(int(p.handle))
if e != nil {
return NewSyscallError("close", e)

}
p.Pid = -1
// no need for a finalizer anymore
runtime.SetFinalizer(p, nil)
return nil
}

func findProcess(pid int) (p *Process, err error) {
// NOOP for unix.
return newProcess(pid, 0), nil
fd, _, e := syscall.Syscall(syscall.SYS_PIDFD_OPEN, uintptr(pid), 0, 0)
runtime.KeepAlive(p)
if e != 0 {
return nil, NewSyscallError("pidfd_open", e)
}
return newProcess(pid, fd), nil
}

func (p *ProcessState) userTime() time.Duration {
Expand Down
41 changes: 17 additions & 24 deletions src/os/wait_waitid.go
Original file line number Diff line number Diff line change
Expand Up @@ -10,39 +10,32 @@
package os

import (
"runtime"
"internal/poll"
"syscall"
"unsafe"
)

const _P_PID = 1
const _P_PIDFD = 3

// blockUntilWaitable attempts to block until a call to p.Wait will
// succeed immediately, and reports whether it has done so.
// It does not actually call p.Wait.
func (p *Process) blockUntilWaitable() (bool, error) {
// The waitid system call expects a pointer to a siginfo_t,
// which is 128 bytes on all Linux systems.
// On darwin/amd64, it requires 104 bytes.
// We don't care about the values it returns.
var siginfo [16]uint64
psig := &siginfo[0]
var e syscall.Errno
for {
_, _, e = syscall.Syscall6(syscall.SYS_WAITID, _P_PID, uintptr(p.Pid), uintptr(unsafe.Pointer(psig)), syscall.WEXITED|syscall.WNOWAIT, 0, 0)
if e != syscall.EINTR {
break
}
fd := poll.FD{
Sysfd: int(p.handle),
}
runtime.KeepAlive(p)
if e != 0 {
// waitid has been available since Linux 2.6.9, but
// reportedly is not available in Ubuntu on Windows.
// See issue 16610.
if e == syscall.ENOSYS {
return false, nil
}
return false, NewSyscallError("waitid", e)
err := fd.Init("pidfd", false)
if err != nil {
return false, err
}
// We just want to make sure fd is ready for reading, but that is not yet available.
// See: https://github.com/golang/go/issues/15735
buf := make([]byte, 1)
_, err = fd.Read(buf)
if err == syscall.EINVAL {
// fd is ready for reading, but reading failed, as expected on pidfd.
return true, nil
} else if err != nil {
return false, err
}
return true, nil
}
13 changes: 12 additions & 1 deletion src/syscall/exec_unix.go
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,9 @@ import (
"unsafe"
)

const SYS_PIDFD_OPEN = 434
const SYS_PIDFD_SEND_SIGNAL = 424

// ForkLock is used to synchronize creation of new file descriptors
// with fork.
//
Expand Down Expand Up @@ -332,7 +335,15 @@ func ForkExec(argv0 string, argv []string, attr *ProcAttr) (pid int, err error)
// StartProcess wraps ForkExec for package os.
func StartProcess(argv0 string, argv []string, attr *ProcAttr) (pid int, handle uintptr, err error) {
pid, err = forkExec(argv0, argv, attr)
return pid, 0, err
if err != nil {
return
}
fd, _, e := Syscall(SYS_PIDFD_OPEN, uintptr(pid), 0, 0)
if e != 0 {
err = errnoErr(e)
return
}
return pid, fd, err
}

// Implemented in runtime package.
Expand Down

0 comments on commit 86ae506

Please sign in to comment.