Skip to content

bpo-35537: subprocess uses os.posix_spawn in some cases #11452

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 5 commits into from
Jan 15, 2019
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 9 additions & 0 deletions Doc/whatsnew/3.8.rst
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
82 changes: 82 additions & 0 deletions Lib/subprocess.py
Original file line number Diff line number Diff line change
Expand Up @@ -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.

Expand Down Expand Up @@ -1390,6 +1441,23 @@ def _get_handles(self, stdin, stdout, stderr):
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,
Expand All @@ -1414,6 +1482,20 @@ def _execute_child(self, args, executable, preexec_fn, close_fds,

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.
Expand Down
6 changes: 6 additions & 0 deletions Lib/test/pythoninfo.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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,
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
The :mod:`subprocess` module can now use the :func:`os.posix_spawn` function in
some cases for better performance.