Skip to content

Commit 3ccdd9b

Browse files
authored
closes bpo-38692: Add a pidfd child process watcher to asyncio. (GH-17069)
1 parent dad6be5 commit 3ccdd9b

File tree

5 files changed

+102
-0
lines changed

5 files changed

+102
-0
lines changed

Doc/library/asyncio-policy.rst

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -257,6 +257,18 @@ implementation used by the asyncio event loop:
257257
This solution requires a running event loop in the main thread to work, as
258258
:class:`SafeChildWatcher`.
259259

260+
.. class:: PidfdChildWatcher
261+
262+
This implementation polls process file descriptors (pidfds) to await child
263+
process termination. In some respects, :class:`PidfdChildWatcher` is a
264+
"Goldilocks" child watcher implementation. It doesn't require signals or
265+
threads, doesn't interfere with any processes launched outside the event
266+
loop, and scales linearly with the number of subprocesses launched by the
267+
event loop. The main disadvantage is that pidfds are specific to Linux, and
268+
only work on recent (5.3+) kernels.
269+
270+
.. versionadded:: 3.9
271+
260272

261273
Custom Policies
262274
===============

Doc/whatsnew/3.9.rst

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -130,6 +130,9 @@ that schedules a shutdown for the default executor that waits on the
130130
:func:`asyncio.run` has been updated to use the new :term:`coroutine`.
131131
(Contributed by Kyle Stanley in :issue:`34037`.)
132132

133+
Added :class:`asyncio.PidfdChildWatcher`, a Linux-specific child watcher
134+
implementation that polls process file descriptors. (:issue:`38692`)
135+
133136
curses
134137
------
135138

Lib/asyncio/unix_events.py

Lines changed: 67 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -878,6 +878,73 @@ def __exit__(self, a, b, c):
878878
raise NotImplementedError()
879879

880880

881+
class PidfdChildWatcher(AbstractChildWatcher):
882+
"""Child watcher implementation using Linux's pid file descriptors.
883+
884+
This child watcher polls process file descriptors (pidfds) to await child
885+
process termination. In some respects, PidfdChildWatcher is a "Goldilocks"
886+
child watcher implementation. It doesn't require signals or threads, doesn't
887+
interfere with any processes launched outside the event loop, and scales
888+
linearly with the number of subprocesses launched by the event loop. The
889+
main disadvantage is that pidfds are specific to Linux, and only work on
890+
recent (5.3+) kernels.
891+
"""
892+
893+
def __init__(self):
894+
self._loop = None
895+
self._callbacks = {}
896+
897+
def __enter__(self):
898+
return self
899+
900+
def __exit__(self, exc_type, exc_value, exc_traceback):
901+
pass
902+
903+
def is_active(self):
904+
return self._loop is not None and self._loop.is_running()
905+
906+
def close(self):
907+
self.attach_loop(None)
908+
909+
def attach_loop(self, loop):
910+
if self._loop is not None and loop is None and self._callbacks:
911+
warnings.warn(
912+
'A loop is being detached '
913+
'from a child watcher with pending handlers',
914+
RuntimeWarning)
915+
for pidfd, _, _ in self._callbacks.values():
916+
self._loop._remove_reader(pidfd)
917+
os.close(pidfd)
918+
self._callbacks.clear()
919+
self._loop = loop
920+
921+
def add_child_handler(self, pid, callback, *args):
922+
existing = self._callbacks.get(pid)
923+
if existing is not None:
924+
self._callbacks[pid] = existing[0], callback, args
925+
else:
926+
pidfd = os.pidfd_open(pid)
927+
self._loop._add_reader(pidfd, self._do_wait, pid)
928+
self._callbacks[pid] = pidfd, callback, args
929+
930+
def _do_wait(self, pid):
931+
pidfd, callback, args = self._callbacks.pop(pid)
932+
self._loop._remove_reader(pidfd)
933+
_, status = os.waitpid(pid, 0)
934+
os.close(pidfd)
935+
returncode = _compute_returncode(status)
936+
callback(pid, returncode, *args)
937+
938+
def remove_child_handler(self, pid):
939+
try:
940+
pidfd, _, _ = self._callbacks.pop(pid)
941+
except KeyError:
942+
return False
943+
self._loop._remove_reader(pidfd)
944+
os.close(pidfd)
945+
return True
946+
947+
881948
def _compute_returncode(status):
882949
if os.WIFSIGNALED(status):
883950
# The child process died because of a signal.

Lib/test/test_asyncio/test_subprocess.py

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
import os
12
import signal
23
import sys
34
import unittest
@@ -691,6 +692,23 @@ class SubprocessFastWatcherTests(SubprocessWatcherMixin,
691692

692693
Watcher = unix_events.FastChildWatcher
693694

695+
def has_pidfd_support():
696+
if not hasattr(os, 'pidfd_open'):
697+
return False
698+
try:
699+
os.close(os.pidfd_open(os.getpid()))
700+
except OSError:
701+
return False
702+
return True
703+
704+
@unittest.skipUnless(
705+
has_pidfd_support(),
706+
"operating system does not support pidfds",
707+
)
708+
class SubprocessPidfdWatcherTests(SubprocessWatcherMixin,
709+
test_utils.TestCase):
710+
Watcher = unix_events.PidfdChildWatcher
711+
694712
else:
695713
# Windows
696714
class SubprocessProactorTests(SubprocessMixin, test_utils.TestCase):
Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
Add :class:`asyncio.PidfdChildWatcher`, a Linux-specific child watcher
2+
implementation that polls process file descriptors.

0 commit comments

Comments
 (0)