@@ -748,6 +748,60 @@ def _use_posix_spawn():
748748 return False
749749
750750
751+ def _can_use_pidfd_open ():
752+ # Availability: Linux >= 5.3
753+ if not hasattr (os , "pidfd_open" ):
754+ return False
755+ try :
756+ pidfd = os .pidfd_open (os .getpid (), 0 )
757+ except OSError as err :
758+ if err .errno in {errno .EMFILE , errno .ENFILE }:
759+ # transitory 'too many open files'
760+ return True
761+ # likely blocked by security policy like SECCOMP (EPERM,
762+ # EACCES, ENOSYS)
763+ return False
764+ else :
765+ os .close (pidfd )
766+ return True
767+
768+
769+ def _can_use_kqueue ():
770+ # Availability: macOS, BSD
771+ names = (
772+ "kqueue" ,
773+ "KQ_EV_ADD" ,
774+ "KQ_EV_ONESHOT" ,
775+ "KQ_FILTER_PROC" ,
776+ "KQ_NOTE_EXIT" ,
777+ )
778+ if not all (hasattr (select , x ) for x in names ):
779+ return False
780+ kq = None
781+ try :
782+ kq = select .kqueue ()
783+ kev = select .kevent (
784+ os .getpid (),
785+ filter = select .KQ_FILTER_PROC ,
786+ flags = select .KQ_EV_ADD | select .KQ_EV_ONESHOT ,
787+ fflags = select .KQ_NOTE_EXIT ,
788+ )
789+ kq .control ([kev ], 1 , 0 )
790+ return True
791+ except OSError as err :
792+ if err .errno in {errno .EMFILE , errno .ENFILE }:
793+ # transitory 'too many open files'
794+ return True
795+ return False
796+ finally :
797+ if kq is not None :
798+ kq .close ()
799+
800+
801+ _CAN_USE_PIDFD_OPEN = not _mswindows and _can_use_pidfd_open ()
802+ _CAN_USE_KQUEUE = not _mswindows and _can_use_kqueue ()
803+
804+
751805# These are primarily fail-safe knobs for negatives. A True value does not
752806# guarantee the given libc/syscall API will be used.
753807_USE_POSIX_SPAWN = _use_posix_spawn ()
@@ -2046,14 +2100,100 @@ def _try_wait(self, wait_flags):
20462100 sts = 0
20472101 return (pid , sts )
20482102
2103+ def _wait_pidfd (self , timeout ):
2104+ """Wait for PID to terminate using pidfd_open() + poll().
2105+ Linux >= 5.3 only.
2106+ """
2107+ if not _CAN_USE_PIDFD_OPEN :
2108+ return False
2109+ try :
2110+ pidfd = os .pidfd_open (self .pid , 0 )
2111+ except OSError :
2112+ # May be:
2113+ # - ESRCH: no such process
2114+ # - EMFILE, ENFILE: too many open files (usually 1024)
2115+ # - ENODEV: anonymous inode filesystem not supported
2116+ # - EPERM, EACCES, ENOSYS: undocumented; may happen if
2117+ # blocked by security policy like SECCOMP
2118+ return False
2119+
2120+ try :
2121+ poller = select .poll ()
2122+ poller .register (pidfd , select .POLLIN )
2123+ events = poller .poll (timeout * 1000 )
2124+ if not events :
2125+ raise TimeoutExpired (self .args , timeout )
2126+ return True
2127+ finally :
2128+ os .close (pidfd )
2129+
2130+ def _wait_kqueue (self , timeout ):
2131+ """Wait for PID to terminate using kqueue(). macOS and BSD only."""
2132+ if not _CAN_USE_KQUEUE :
2133+ return False
2134+ try :
2135+ kq = select .kqueue ()
2136+ except OSError :
2137+ # likely EMFILE / ENFILE (too many open files)
2138+ return False
2139+
2140+ try :
2141+ kev = select .kevent (
2142+ self .pid ,
2143+ filter = select .KQ_FILTER_PROC ,
2144+ flags = select .KQ_EV_ADD | select .KQ_EV_ONESHOT ,
2145+ fflags = select .KQ_NOTE_EXIT ,
2146+ )
2147+ try :
2148+ events = kq .control ([kev ], 1 , timeout ) # wait
2149+ except OSError :
2150+ return False
2151+ else :
2152+ if not events :
2153+ raise TimeoutExpired (self .args , timeout )
2154+ return True
2155+ finally :
2156+ kq .close ()
20492157
20502158 def _wait (self , timeout ):
2051- """Internal implementation of wait() on POSIX."""
2159+ """Internal implementation of wait() on POSIX.
2160+
2161+ Uses efficient pidfd_open() + poll() on Linux or kqueue()
2162+ on macOS/BSD when available. Falls back to polling
2163+ waitpid(WNOHANG) otherwise.
2164+ """
20522165 if self .returncode is not None :
20532166 return self .returncode
20542167
20552168 if timeout is not None :
2056- endtime = _time () + timeout
2169+ if timeout < 0 :
2170+ raise TimeoutExpired (self .args , timeout )
2171+ started = _time ()
2172+ endtime = started + timeout
2173+
2174+ # Try efficient wait first.
2175+ if self ._wait_pidfd (timeout ) or self ._wait_kqueue (timeout ):
2176+ # Process is gone. At this point os.waitpid(pid, 0)
2177+ # will return immediately, but in very rare races
2178+ # the PID may have been reused.
2179+ # os.waitpid(pid, WNOHANG) ensures we attempt a
2180+ # non-blocking reap without blocking indefinitely.
2181+ with self ._waitpid_lock :
2182+ if self .returncode is not None :
2183+ return self .returncode # Another thread waited.
2184+ (pid , sts ) = self ._try_wait (os .WNOHANG )
2185+ assert pid == self .pid or pid == 0
2186+ if pid == self .pid :
2187+ self ._handle_exitstatus (sts )
2188+ return self .returncode
2189+ # os.waitpid(pid, WNOHANG) returned 0 instead
2190+ # of our PID, meaning PID has not yet exited,
2191+ # even though poll() / kqueue() said so. Very
2192+ # rare and mostly theoretical. Fallback to busy
2193+ # polling.
2194+ elapsed = _time () - started
2195+ endtime -= elapsed
2196+
20572197 # Enter a busy loop if we have a timeout. This busy loop was
20582198 # cribbed from Lib/threading.py in Thread.wait() at r71065.
20592199 delay = 0.0005 # 500 us -> initial delay of 1 ms
@@ -2085,6 +2225,7 @@ def _wait(self, timeout):
20852225 # http://bugs.python.org/issue14396.
20862226 if pid == self .pid :
20872227 self ._handle_exitstatus (sts )
2228+
20882229 return self .returncode
20892230
20902231
0 commit comments