From 7ec568c8037359a37a36eeaf24e18ac014bc3e58 Mon Sep 17 00:00:00 2001 From: Qing Deng Date: Thu, 2 Oct 2025 22:57:38 -0700 Subject: [PATCH] gh-86354: Fix child processes not reusing forkserver created by parent --- Lib/multiprocessing/forkserver.py | 41 +++++++++++++++---- Lib/test/_test_multiprocessing.py | 36 ++++++++++++++++ ...5-10-02-23-05-42.gh-issue-86354.0RGEbU.rst | 1 + 3 files changed, 69 insertions(+), 9 deletions(-) create mode 100644 Misc/NEWS.d/next/Library/2025-10-02-23-05-42.gh-issue-86354.0RGEbU.rst diff --git a/Lib/multiprocessing/forkserver.py b/Lib/multiprocessing/forkserver.py index cc8947c5e04fb1..06161a6065b97d 100644 --- a/Lib/multiprocessing/forkserver.py +++ b/Lib/multiprocessing/forkserver.py @@ -135,12 +135,27 @@ def ensure_running(self): if not pid: # still alive return - # dead, launch it again - os.close(self._forkserver_alive_fd) - self._forkserver_authkey = None - self._forkserver_address = None - self._forkserver_alive_fd = None - self._forkserver_pid = None + if self._forkserver_alive_fd is not None: + # forkserver may be inherited from parent, is it still running? + try: + # check by writing a probe + os.write(self._forkserver_alive_fd, b'P') + except OSError: + pass + else: + # still alive + return + + # dead, launch it again + if self._forkserver_alive_fd is not None: + try: + os.close(self._forkserver_alive_fd) + except OSError: + pass + self._forkserver_authkey = None + self._forkserver_address = None + self._forkserver_alive_fd = None + self._forkserver_pid = None cmd = ('from multiprocessing.forkserver import main; ' + 'main(%d, %d, %r, **%r)') @@ -207,6 +222,7 @@ def main(listener_fd, alive_r, preload, main_path=None, sys_path=None, os.close(authkey_r) else: authkey = b'' + _forkserver._forkserver_authkey = authkey if preload: if sys_path is not None: @@ -268,9 +284,16 @@ def sigchld_handler(*_unused): break if alive_r in rfds: - # EOF because no more client processes left - assert os.read(alive_r, 1) == b'', "Not at EOF?" - raise SystemExit + data = os.read(alive_r, 1) + if data == b'P': + # probe from client + pass + elif data == b'': + # EOF because no more client processes left + raise SystemExit + else: + raise RuntimeError("Unknown data received in alive fd") + if sig_r in rfds: # Got SIGCHLD diff --git a/Lib/test/_test_multiprocessing.py b/Lib/test/_test_multiprocessing.py index 850744e47d0e0b..f7d4cc9bff5cca 100644 --- a/Lib/test/_test_multiprocessing.py +++ b/Lib/test/_test_multiprocessing.py @@ -1002,6 +1002,42 @@ def test_forkserver_without_auth_fails(self): proc.start() proc.join() + @classmethod + def _create_child_and_get_ppid(cls, queue, remaining): + queue.put(os.getppid()) + remaining -= 1 + if remaining > 0: + proc = cls.Process(target=cls._create_child_and_get_ppid, args=(queue, remaining)) + proc.start() + proc.join() + + + def test_forkserver_reuse(self): + # existing forkserver spawned by parent should be reused by children + if self.TYPE == "threads": + self.skipTest(f"test not appropriate for {self.TYPE}") + if multiprocessing.get_start_method() != "forkserver": + self.skipTest("forkserver start method specific") + + forkserver = multiprocessing.forkserver._forkserver + forkserver.ensure_running() + self.assertTrue(forkserver._forkserver_pid) + forkserver_pid = forkserver._forkserver_pid + + # start children recursively + ppid_queue = self.Queue() + proc = self.Process(target=self._create_child_and_get_ppid, args=(ppid_queue, 3)) + proc.start() + proc.join() + for i in range(3): + # all descendants should have the same ppid (forkserver) + ppid = ppid_queue.get() + assert ppid == forkserver_pid + + # forkserver should live after descendants ends + forkserver.ensure_running() + assert forkserver._forkserver_pid == forkserver_pid + # # # diff --git a/Misc/NEWS.d/next/Library/2025-10-02-23-05-42.gh-issue-86354.0RGEbU.rst b/Misc/NEWS.d/next/Library/2025-10-02-23-05-42.gh-issue-86354.0RGEbU.rst new file mode 100644 index 00000000000000..3831e13e0ab6be --- /dev/null +++ b/Misc/NEWS.d/next/Library/2025-10-02-23-05-42.gh-issue-86354.0RGEbU.rst @@ -0,0 +1 @@ +Fix child processes not reusing forkserver created by parent