Skip to content

os/exec: use pidfd for waiting and signaling of processes #60461

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Draft
wants to merge 1 commit into
base: master
Choose a base branch
from
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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.Stopped() {
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