From 9daecf37a571e98aaf43a387bcc9e41a7132f477 Mon Sep 17 00:00:00 2001 From: Victor Stinner Date: Wed, 16 Jan 2019 00:02:35 +0100 Subject: bpo-35537: subprocess uses os.posix_spawn in some cases (GH-11452) The subprocess module can now use the os.posix_spawn() function in some cases for better performance. Currently, it is only used on macOS and Linux (using glibc 2.24 or newer) if all these conditions are met: * executable path contains a directory * close_fds=False * preexec_fn, pass_fds, cwd, stdin, stdout, stderr and start_new_session parameters are not set Co-authored-by: Joannah Nanjekye --- Doc/whatsnew/3.8.rst | 9 +++ Lib/subprocess.py | 82 ++++++++++++++++++++++ Lib/test/pythoninfo.py | 6 ++ .../2018-12-20-16-24-51.bpo-35537.z4E7aA.rst | 2 + 4 files changed, 99 insertions(+) create mode 100644 Misc/NEWS.d/next/Library/2018-12-20-16-24-51.bpo-35537.z4E7aA.rst diff --git a/Doc/whatsnew/3.8.rst b/Doc/whatsnew/3.8.rst index 370ef46..053fe90 100644 --- a/Doc/whatsnew/3.8.rst +++ b/Doc/whatsnew/3.8.rst @@ -275,6 +275,15 @@ xml Optimizations ============= +* The :mod:`subprocess` module can now use the :func:`os.posix_spawn` function + in some cases for better performance. Currently, it is only used on macOS + and Linux (using glibc 2.24 or newer) if all these conditions are met: + + * *close_fds* is false; + * *preexec_fn*, *pass_fds*, *cwd*, *stdin*, *stdout*, *stderr* and + *start_new_session* parameters are not set; + * the *executable* path contains a directory. + * :func:`shutil.copyfile`, :func:`shutil.copy`, :func:`shutil.copy2`, :func:`shutil.copytree` and :func:`shutil.move` use platform-specific "fast-copy" syscalls on Linux, macOS and Solaris in order to copy the file diff --git a/Lib/subprocess.py b/Lib/subprocess.py index 6966176..b94575b 100644 --- a/Lib/subprocess.py +++ b/Lib/subprocess.py @@ -606,6 +606,57 @@ def getoutput(cmd): return getstatusoutput(cmd)[1] +def _use_posix_spawn(): + """Check is posix_spawn() can be used for subprocess. + + subprocess requires a posix_spawn() implementation that reports properly + errors to the parent process, set errno on the following failures: + + * process attribute actions failed + * file actions failed + * exec() failed + + Prefer an implementation which can use vfork in some cases for best + performances. + """ + if _mswindows or not hasattr(os, 'posix_spawn'): + # os.posix_spawn() is not available + return False + + if sys.platform == 'darwin': + # posix_spawn() is a syscall on macOS and properly reports errors + return True + + # Check libc name and runtime libc version + try: + ver = os.confstr('CS_GNU_LIBC_VERSION') + # parse 'glibc 2.28' as ('glibc', (2, 28)) + parts = ver.split(maxsplit=1) + if len(parts) != 2: + # reject unknown format + raise ValueError + libc = parts[0] + version = tuple(map(int, parts[1].split('.'))) + + if sys.platform == 'linux' and libc == 'glibc' and version >= (2, 24): + # glibc 2.24 has a new Linux posix_spawn implementation using vfork + # which properly reports errors to the parent process. + return True + # Note: Don't use the POSIX implementation of glibc because it doesn't + # use vfork (even if glibc 2.26 added a pipe to properly report errors + # to the parent process). + except (AttributeError, ValueError, OSError): + # os.confstr() or CS_GNU_LIBC_VERSION value not available + pass + + # By default, consider that the implementation does not properly report + # errors. + return False + + +_USE_POSIX_SPAWN = _use_posix_spawn() + + class Popen(object): """ Execute a child program in a new process. @@ -1390,6 +1441,23 @@ class Popen(object): errread, errwrite) + def _posix_spawn(self, args, executable, env, restore_signals): + """Execute program using os.posix_spawn().""" + if env is None: + env = os.environ + + kwargs = {} + if restore_signals: + # See _Py_RestoreSignals() in Python/pylifecycle.c + sigset = [] + for signame in ('SIGPIPE', 'SIGXFZ', 'SIGXFSZ'): + signum = getattr(signal, signame, None) + if signum is not None: + sigset.append(signum) + kwargs['setsigdef'] = sigset + + self.pid = os.posix_spawn(executable, args, env, **kwargs) + def _execute_child(self, args, executable, preexec_fn, close_fds, pass_fds, cwd, env, startupinfo, creationflags, shell, @@ -1414,6 +1482,20 @@ class Popen(object): if executable is None: executable = args[0] + + if (_USE_POSIX_SPAWN + and os.path.dirname(executable) + and preexec_fn is None + and not close_fds + and not pass_fds + and cwd is None + and p2cread == p2cwrite == -1 + and c2pread == c2pwrite == -1 + and errread == errwrite == -1 + and not start_new_session): + self._posix_spawn(args, executable, env, restore_signals) + return + orig_executable = executable # For transferring possible exec failure from child to parent. diff --git a/Lib/test/pythoninfo.py b/Lib/test/pythoninfo.py index 7ce6bf7..7e94a31 100644 --- a/Lib/test/pythoninfo.py +++ b/Lib/test/pythoninfo.py @@ -610,6 +610,11 @@ def collect_get_config(info_add): info_add('%s[%s]' % (prefix, key), repr(config[key])) +def collect_subprocess(info_add): + import subprocess + copy_attributes(info_add, subprocess, 'subprocess.%s', ('_USE_POSIX_SPAWN',)) + + def collect_info(info): error = False info_add = info.add @@ -639,6 +644,7 @@ def collect_info(info): collect_cc, collect_gdbm, collect_get_config, + collect_subprocess, # Collecting from tests should be last as they have side effects. collect_test_socket, diff --git a/Misc/NEWS.d/next/Library/2018-12-20-16-24-51.bpo-35537.z4E7aA.rst b/Misc/NEWS.d/next/Library/2018-12-20-16-24-51.bpo-35537.z4E7aA.rst new file mode 100644 index 0000000..b14d749 --- /dev/null +++ b/Misc/NEWS.d/next/Library/2018-12-20-16-24-51.bpo-35537.z4E7aA.rst @@ -0,0 +1,2 @@ +The :mod:`subprocess` module can now use the :func:`os.posix_spawn` function in +some cases for better performance. -- cgit v0.12