Skip to content

Commit 2b93f52

Browse files
kulikjakgpshead
andauthored
gh-113117: Support posix_spawn in subprocess.Popen with close_fds=True (#113118)
Add support for `os.POSIX_SPAWN_CLOSEFROM` and `posix_spawn_file_actions_addclosefrom_np` and have the `subprocess` module use them when available. This means `posix_spawn` can now be used in the default `close_fds=True` situation on many platforms. Co-authored-by: Gregory P. Smith [Google LLC] <[email protected]>
1 parent 32d87a8 commit 2b93f52

File tree

9 files changed

+91
-5
lines changed

9 files changed

+91
-5
lines changed

Doc/library/os.rst

Lines changed: 13 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -4601,10 +4601,17 @@ written in Python, such as a mail server's external command delivery program.
46014601

46024602
Performs ``os.dup2(fd, new_fd)``.
46034603

4604+
.. data:: POSIX_SPAWN_CLOSEFROM
4605+
4606+
(``os.POSIX_SPAWN_CLOSEFROM``, *fd*)
4607+
4608+
Performs ``os.closerange(fd, INF)``.
4609+
46044610
These tuples correspond to the C library
46054611
:c:func:`!posix_spawn_file_actions_addopen`,
4606-
:c:func:`!posix_spawn_file_actions_addclose`, and
4607-
:c:func:`!posix_spawn_file_actions_adddup2` API calls used to prepare
4612+
:c:func:`!posix_spawn_file_actions_addclose`,
4613+
:c:func:`!posix_spawn_file_actions_adddup2`, and
4614+
:c:func:`!posix_spawn_file_actions_addclosefrom_np` API calls used to prepare
46084615
for the :c:func:`!posix_spawn` call itself.
46094616

46104617
The *setpgroup* argument will set the process group of the child to the value
@@ -4649,6 +4656,10 @@ written in Python, such as a mail server's external command delivery program.
46494656
.. versionchanged:: 3.13
46504657
*env* parameter accepts ``None``.
46514658

4659+
.. versionchanged:: 3.13
4660+
``os.POSIX_SPAWN_CLOSEFROM`` is available on platforms where
4661+
:c:func:`!posix_spawn_file_actions_addclosefrom_np` exists.
4662+
46524663
.. availability:: Unix, not Emscripten, not WASI.
46534664

46544665
.. function:: posix_spawnp(path, argv, env, *, file_actions=None, \

Doc/whatsnew/3.13.rst

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -293,6 +293,11 @@ os
293293
process use the current process environment.
294294
(Contributed by Jakub Kulik in :gh:`113119`.)
295295

296+
* :func:`os.posix_spawn` gains an :attr:`os.POSIX_SPAWN_CLOSEFROM` attribute for
297+
use in ``file_actions=`` on platforms that support
298+
:c:func:`!posix_spawn_file_actions_addclosefrom_np`.
299+
(Contributed by Jakub Kulik in :gh:`113117`.)
300+
296301
pathlib
297302
-------
298303

@@ -342,6 +347,21 @@ sqlite3
342347
object is not :meth:`closed <sqlite3.Connection.close>` explicitly.
343348
(Contributed by Erlend E. Aasland in :gh:`105539`.)
344349

350+
subprocess
351+
----------
352+
353+
* The :mod:`subprocess` module now uses the :func:`os.posix_spawn` function in
354+
more situations. Notably in the default case of ``close_fds=True`` on more
355+
recent versions of platforms including Linux, FreeBSD, and Solaris where the
356+
C library provides :c:func:`!posix_spawn_file_actions_addclosefrom_np`.
357+
On Linux this should perform similar to our existing Linux :c:func:`!vfork`
358+
based code. A private control knob :attr:`!subprocess._USE_POSIX_SPAWN` can
359+
be set to ``False`` if you need to force :mod:`subprocess` not to ever use
360+
:func:`os.posix_spawn`. Please report your reason and platform details in
361+
the CPython issue tracker if you set this so that we can improve our API
362+
selection logic for everyone.
363+
(Contributed by Jakub Kulik in :gh:`113117`.)
364+
345365
sys
346366
---
347367

@@ -415,6 +435,12 @@ Optimizations
415435
* :func:`textwrap.indent` is now ~30% faster than before for large input.
416436
(Contributed by Inada Naoki in :gh:`107369`.)
417437

438+
* The :mod:`subprocess` module uses :func:`os.posix_spawn` in more situations
439+
including the default where ``close_fds=True`` on many modern platforms. This
440+
should provide a noteworthy performance increase launching processes on
441+
FreeBSD and Solaris. See the ``subprocess`` section above for details.
442+
(Contributed by Jakub Kulik in :gh:`113117`.)
443+
418444

419445
Deprecated
420446
==========

Lib/subprocess.py

Lines changed: 8 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -748,6 +748,7 @@ def _use_posix_spawn():
748748
# guarantee the given libc/syscall API will be used.
749749
_USE_POSIX_SPAWN = _use_posix_spawn()
750750
_USE_VFORK = True
751+
_HAVE_POSIX_SPAWN_CLOSEFROM = hasattr(os, 'POSIX_SPAWN_CLOSEFROM')
751752

752753

753754
class Popen:
@@ -1751,7 +1752,7 @@ def _get_handles(self, stdin, stdout, stderr):
17511752
errread, errwrite)
17521753

17531754

1754-
def _posix_spawn(self, args, executable, env, restore_signals,
1755+
def _posix_spawn(self, args, executable, env, restore_signals, close_fds,
17551756
p2cread, p2cwrite,
17561757
c2pread, c2pwrite,
17571758
errread, errwrite):
@@ -1777,6 +1778,10 @@ def _posix_spawn(self, args, executable, env, restore_signals,
17771778
):
17781779
if fd != -1:
17791780
file_actions.append((os.POSIX_SPAWN_DUP2, fd, fd2))
1781+
1782+
if close_fds:
1783+
file_actions.append((os.POSIX_SPAWN_CLOSEFROM, 3))
1784+
17801785
if file_actions:
17811786
kwargs['file_actions'] = file_actions
17821787

@@ -1824,7 +1829,7 @@ def _execute_child(self, args, executable, preexec_fn, close_fds,
18241829
if (_USE_POSIX_SPAWN
18251830
and os.path.dirname(executable)
18261831
and preexec_fn is None
1827-
and not close_fds
1832+
and (not close_fds or _HAVE_POSIX_SPAWN_CLOSEFROM)
18281833
and not pass_fds
18291834
and cwd is None
18301835
and (p2cread == -1 or p2cread > 2)
@@ -1836,7 +1841,7 @@ def _execute_child(self, args, executable, preexec_fn, close_fds,
18361841
and gids is None
18371842
and uid is None
18381843
and umask < 0):
1839-
self._posix_spawn(args, executable, env, restore_signals,
1844+
self._posix_spawn(args, executable, env, restore_signals, close_fds,
18401845
p2cread, p2cwrite,
18411846
c2pread, c2pwrite,
18421847
errread, errwrite)

Lib/test/test_subprocess.py

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3348,6 +3348,7 @@ def exit_handler():
33483348
@unittest.skipIf(not sysconfig.get_config_var("HAVE_VFORK"),
33493349
"vfork() not enabled by configure.")
33503350
@mock.patch("subprocess._fork_exec")
3351+
@mock.patch("subprocess._USE_POSIX_SPAWN", new=False)
33513352
def test__use_vfork(self, mock_fork_exec):
33523353
self.assertTrue(subprocess._USE_VFORK) # The default value regardless.
33533354
mock_fork_exec.side_effect = RuntimeError("just testing args")
@@ -3366,9 +3367,13 @@ def test__use_vfork(self, mock_fork_exec):
33663367
@unittest.skipIf(not sysconfig.get_config_var("HAVE_VFORK"),
33673368
"vfork() not enabled by configure.")
33683369
@unittest.skipIf(sys.platform != "linux", "Linux only, requires strace.")
3370+
@mock.patch("subprocess._USE_POSIX_SPAWN", new=False)
33693371
def test_vfork_used_when_expected(self):
33703372
# This is a performance regression test to ensure we default to using
33713373
# vfork() when possible.
3374+
# Technically this test could pass when posix_spawn is used as well
3375+
# because libc tends to implement that internally using vfork. But
3376+
# that'd just be testing a libc+kernel implementation detail.
33723377
strace_binary = "/usr/bin/strace"
33733378
# The only system calls we are interested in.
33743379
strace_filter = "--trace=clone,clone2,clone3,fork,vfork,exit,exit_group"
Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
The :mod:`subprocess` module can now use the :func:`os.posix_spawn` function
2+
with ``close_fds=True`` on platforms where
3+
``posix_spawn_file_actions_addclosefrom_np`` is available.
4+
Patch by Jakub Kulik.

Modules/posixmodule.c

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6834,6 +6834,9 @@ enum posix_spawn_file_actions_identifier {
68346834
POSIX_SPAWN_OPEN,
68356835
POSIX_SPAWN_CLOSE,
68366836
POSIX_SPAWN_DUP2
6837+
#ifdef HAVE_POSIX_SPAWN_FILE_ACTIONS_ADDCLOSEFROM_NP
6838+
,POSIX_SPAWN_CLOSEFROM
6839+
#endif
68376840
};
68386841

68396842
#if defined(HAVE_SCHED_SETPARAM) || defined(HAVE_SCHED_SETSCHEDULER) || defined(POSIX_SPAWN_SETSCHEDULER) || defined(POSIX_SPAWN_SETSCHEDPARAM)
@@ -7074,6 +7077,24 @@ parse_file_actions(PyObject *file_actions,
70747077
}
70757078
break;
70767079
}
7080+
#ifdef HAVE_POSIX_SPAWN_FILE_ACTIONS_ADDCLOSEFROM_NP
7081+
case POSIX_SPAWN_CLOSEFROM: {
7082+
int fd;
7083+
if (!PyArg_ParseTuple(file_action, "Oi"
7084+
";A closefrom file_action tuple must have 2 elements",
7085+
&tag_obj, &fd))
7086+
{
7087+
goto fail;
7088+
}
7089+
errno = posix_spawn_file_actions_addclosefrom_np(file_actionsp,
7090+
fd);
7091+
if (errno) {
7092+
posix_error();
7093+
goto fail;
7094+
}
7095+
break;
7096+
}
7097+
#endif
70777098
default: {
70787099
PyErr_SetString(PyExc_TypeError,
70797100
"Unknown file_actions identifier");
@@ -16774,6 +16795,9 @@ all_ins(PyObject *m)
1677416795
if (PyModule_AddIntConstant(m, "POSIX_SPAWN_OPEN", POSIX_SPAWN_OPEN)) return -1;
1677516796
if (PyModule_AddIntConstant(m, "POSIX_SPAWN_CLOSE", POSIX_SPAWN_CLOSE)) return -1;
1677616797
if (PyModule_AddIntConstant(m, "POSIX_SPAWN_DUP2", POSIX_SPAWN_DUP2)) return -1;
16798+
#ifdef HAVE_POSIX_SPAWN_FILE_ACTIONS_ADDCLOSEFROM_NP
16799+
if (PyModule_AddIntMacro(m, POSIX_SPAWN_CLOSEFROM)) return -1;
16800+
#endif
1677716801
#endif
1677816802

1677916803
#if defined(HAVE_SPAWNV) || defined (HAVE_RTPSPAWN)

configure

Lines changed: 6 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

configure.ac

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4757,6 +4757,7 @@ AC_CHECK_FUNCS([ \
47574757
lockf lstat lutimes madvise mbrtowc memrchr mkdirat mkfifo mkfifoat \
47584758
mknod mknodat mktime mmap mremap nice openat opendir pathconf pause pipe \
47594759
pipe2 plock poll posix_fadvise posix_fallocate posix_spawn posix_spawnp \
4760+
posix_spawn_file_actions_addclosefrom_np \
47604761
pread preadv preadv2 pthread_condattr_setclock pthread_init pthread_kill \
47614762
pwrite pwritev pwritev2 readlink readlinkat readv realpath renameat \
47624763
rtpSpawn sched_get_priority_max sched_rr_get_interval sched_setaffinity \

pyconfig.h.in

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -905,6 +905,10 @@
905905
/* Define to 1 if you have the `posix_spawnp' function. */
906906
#undef HAVE_POSIX_SPAWNP
907907

908+
/* Define to 1 if you have the `posix_spawn_file_actions_addclosefrom_np'
909+
function. */
910+
#undef HAVE_POSIX_SPAWN_FILE_ACTIONS_ADDCLOSEFROM_NP
911+
908912
/* Define to 1 if you have the `pread' function. */
909913
#undef HAVE_PREAD
910914

0 commit comments

Comments
 (0)