[CPython] subprocess.Popen.wait() 이벤트 기반 구현으로 효율성 개선
PR 링크: python/cpython#144047 상태: Merged | 변경: +301 / -5
들어가며
subprocess.Popen.wait(timeout=N)은 POSIX 시스템에서 waitpid(WNOHANG) + time.sleep()을 반복하는 busy loop으로 구현되어 있었다. 이 방식은 CPU를 불필요하게 소모하고, sleep 간격 때문에 프로세스 종료 감지에 최대 수백 밀리초의 지연이 발생한다. 이 PR은 Linux의 pidfd_open() + poll()과 macOS/BSD의 kqueue() + KQ_FILTER_PROC을 활용해 이벤트 기반 대기로 전환한다.
핵심 코드 분석
플랫폼 지원 감지
# Lib/subprocess.py
def _can_use_pidfd_open():
# Availability: Linux >= 5.3
if not hasattr(os, "pidfd_open"):
return False
try:
pidfd = os.pidfd_open(os.getpid(), 0)
except OSError as err:
if err.errno in {errno.EMFILE, errno.ENFILE}:
return True # transitory 'too many open files'
return False # blocked by SECCOMP etc.
else:
os.close(pidfd)
return True
def _can_use_kqueue():
# Availability: macOS, BSD
names = ("kqueue", "KQ_EV_ADD", "KQ_EV_ONESHOT",
"KQ_FILTER_PROC", "KQ_NOTE_EXIT")
if not all(hasattr(select, x) for x in names):
return False
# ... kqueue 실제 동작 검증 ...
return True
_CAN_USE_PIDFD_OPEN = not _mswindows and _can_use_pidfd_open()
_CAN_USE_KQUEUE = not _mswindows and _can_use_kqueue()
모듈 로드 시점에 한 번만 감지하고 결과를 캐싱한다. SECCOMP 정책으로 차단된 환경이나 EMFILE 같은 일시적 오류도 올바르게 처리한다.
pidfd_open 기반 대기 (Linux >= 5.3)
Before:
def _wait(self, timeout):
if self.returncode is not None:
return self.returncode
if timeout is not None:
endtime = _time() + timeout
# busy loop with exponential backoff
delay = 0.0005
while True:
(pid, sts) = self._try_wait(os.WNOHANG)
if pid == self.pid:
self._handle_exitstatus(sts)
return self.returncode
remaining = endtime - _time()
if remaining <= 0:
raise TimeoutExpired(self.args, timeout)
delay = min(delay * 2, remaining, .05)
time.sleep(delay)
After:
def _wait_pidfd(self, timeout):
if not _CAN_USE_PIDFD_OPEN:
return False
try:
pidfd = os.pidfd_open(self.pid, 0)
except OSError:
return False
try:
poller = select.poll()
poller.register(pidfd, select.POLLIN)
events = poller.poll(timeout * 1000)
if not events:
raise TimeoutExpired(self.args, timeout)
return True
finally:
os.close(pidfd)
pidfd_open()으로 프로세스 file descriptor를 얻고, select.poll()로 프로세스 종료를 이벤트 기반으로 대기한다. busy loop이 완전히 제거된다.
kqueue 기반 대기 (macOS/BSD)
def _wait_kqueue(self, timeout):
if not _CAN_USE_KQUEUE:
return False
try:
kq = select.kqueue()
except OSError:
return False
try:
kev = select.kevent(
self.pid,
filter=select.KQ_FILTER_PROC,
flags=select.KQ_EV_ADD | select.KQ_EV_ONESHOT,
fflags=select.KQ_NOTE_EXIT,
)
events = kq.control([kev], 1, timeout)
if not events:
raise TimeoutExpired(self.args, timeout)
return True
finally:
kq.close()
macOS/BSD에서는 kqueue의 KQ_FILTER_PROC + KQ_NOTE_EXIT로 프로세스 종료를 감지한다. 마찬가지로 이벤트 기반이므로 CPU를 낭비하지 않는다.
Graceful Fallback 구조
def _wait(self, timeout):
if self.returncode is not None:
return self.returncode
if timeout is not None:
if timeout < 0:
raise TimeoutExpired(self.args, timeout)
started = _time()
endtime = started + timeout
# Try efficient wait first
if self._wait_pidfd(timeout) or self._wait_kqueue(timeout):
with self._waitpid_lock:
if self.returncode is not None:
return self.returncode
(pid, sts) = self._try_wait(os.WNOHANG)
if pid == self.pid:
self._handle_exitstatus(sts)
return self.returncode
# Rare race: fallback to busy polling
elapsed = _time() - started
endtime -= elapsed
# Fallback: busy loop (same as before)
delay = 0.0005
# ...
이벤트 기반 대기를 먼저 시도하고, 실패하면 기존 busy loop으로 fallback한다. PID 재사용 race condition도 WNOHANG으로 안전하게 처리한다.
왜 이게 좋은가
- CPU 사용량 감소: busy loop의 반복적인
sleep()+waitpid(WNOHANG)호출 대신 커널 이벤트 알림을 사용한다. - 즉시 감지: 프로세스 종료 시 busy loop의 sleep 간격(최대 50ms)만큼 기다릴 필요 없이 즉시 감지한다.
- 하위 호환성: 이벤트 기반 메커니즘을 사용할 수 없는 환경에서는 기존 방식으로 자동 fallback한다.
- Windows는 변경 없음: Windows는 이미
WaitForSingleObject를 사용하고 있어 변경이 불필요하다.
정리
이 PR은 7년 이상 된 이슈(#83069)를 해결한다. Linux 5.3+의 pidfd_open과 macOS/BSD의 kqueue라는 OS 수준 메커니즘을 활용해 subprocess.Popen.wait()의 효율성을 크게 개선했다. "가능하면 효율적으로, 안되면 기존 방식으로"라는 graceful degradation 설계가 표준 라이브러리다운 견고한 접근이다.
참고 자료
- Python Issue #83069 -- subprocess.Popen.wait() busy loop 개선 이슈
- pidfd_open(2) man page -- Linux pidfd_open 시스템 콜
알림: 이 분석은 AI가 실제 코드 diff를 기반으로 작성했습니다.
관련 포스트
- [CPython 3.14] asyncio.Queue docstring의 모호한 표현 수정 (backport)
- [CPython 3.13] asyncio.Queue docstring의 모호한 표현 수정 (backport)
- [CPython] asyncio.Queue docstring의 모호한 'standard library Queue' 표현 수정
- [CPython] 64-bit ARM 커널에서 32-bit ARM Android의 sysconfig ABI 감지 수정
- [CPython 3.13] pickle fast_save_enter() 테스트 정리 (backport)
PR Analysis 의 다른글
- 이전글 [Open WebUI] asyncio.gather로 이미지 로딩 병렬화하여 지연시간 단축
- 현재글 : [CPython] subprocess.Popen.wait() 이벤트 기반 구현으로 효율성 개선
- 다음글 [Grafana Loki] Allocator에 동시 접근 감지를 추가하여 메모리 안전성 확보
댓글