Skip to content

Commit 23c0fb8

Browse files
authored
bpo-41586: Add pipesize parameter to subprocess & F_GETPIPE_SZ and F_SETPIPE_SZ to fcntl. (GH-21921)
* Add F_SETPIPE_SZ and F_GETPIPE_SZ to fcntl module * Add pipesize parameter for subprocess.Popen class This will allow the user to control the size of the pipes. On linux the default is 64K. When a pipe is full it blocks for writing. When a pipe is empty it blocks for reading. On processes that are very fast this can lead to a lot of wasted CPU cycles. On a typical Linux system the max pipe size is 1024K which is much better. For high performance-oriented libraries such as xopen it is nice to be able to set the pipe size. The workaround without this feature is to use my_popen_process.stdout.fileno() in conjuction with fcntl and 1031 (value of F_SETPIPE_SZ) to acquire this behavior.
1 parent bf83822 commit 23c0fb8

File tree

8 files changed

+102
-3
lines changed

8 files changed

+102
-3
lines changed

Doc/library/fcntl.rst

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -39,6 +39,11 @@ descriptor.
3939
On Linux(>=3.15), the fcntl module exposes the ``F_OFD_GETLK``, ``F_OFD_SETLK``
4040
and ``F_OFD_SETLKW`` constants, which working with open file description locks.
4141

42+
.. versionchanged:: 3.10
43+
On Linux >= 2.6.11, the fcntl module exposes the ``F_GETPIPE_SZ`` and
44+
``F_SETPIPE_SZ`` constants, which allow to check and modify a pipe's size
45+
respectively.
46+
4247
The module defines the following functions:
4348

4449

Doc/library/subprocess.rst

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -341,7 +341,7 @@ functions.
341341
startupinfo=None, creationflags=0, restore_signals=True, \
342342
start_new_session=False, pass_fds=(), \*, group=None, \
343343
extra_groups=None, user=None, umask=-1, \
344-
encoding=None, errors=None, text=None)
344+
encoding=None, errors=None, text=None, pipesize=-1)
345345

346346
Execute a child program in a new process. On POSIX, the class uses
347347
:meth:`os.execvp`-like behavior to execute the child program. On Windows,
@@ -625,6 +625,14 @@ functions.
625625
* :data:`CREATE_DEFAULT_ERROR_MODE`
626626
* :data:`CREATE_BREAKAWAY_FROM_JOB`
627627

628+
*pipesize* can be used to change the size of the pipe when
629+
:data:`PIPE` is used for *stdin*, *stdout* or *stderr*. The size of the pipe
630+
is only changed on platforms that support this (only Linux at this time of
631+
writing). Other platforms will ignore this parameter.
632+
633+
.. versionadded:: 3.10
634+
The ``pipesize`` parameter was added.
635+
628636
Popen objects are supported as context managers via the :keyword:`with` statement:
629637
on exit, standard file descriptors are closed, and the process is waited for.
630638
::

Lib/subprocess.py

Lines changed: 18 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -62,6 +62,11 @@
6262
import grp
6363
except ImportError:
6464
grp = None
65+
try:
66+
import fcntl
67+
except ImportError:
68+
fcntl = None
69+
6570

6671
__all__ = ["Popen", "PIPE", "STDOUT", "call", "check_call", "getstatusoutput",
6772
"getoutput", "check_output", "run", "CalledProcessError", "DEVNULL",
@@ -756,7 +761,7 @@ def __init__(self, args, bufsize=-1, executable=None,
756761
startupinfo=None, creationflags=0,
757762
restore_signals=True, start_new_session=False,
758763
pass_fds=(), *, user=None, group=None, extra_groups=None,
759-
encoding=None, errors=None, text=None, umask=-1):
764+
encoding=None, errors=None, text=None, umask=-1, pipesize=-1):
760765
"""Create new Popen instance."""
761766
_cleanup()
762767
# Held while anything is calling waitpid before returncode has been
@@ -773,6 +778,11 @@ def __init__(self, args, bufsize=-1, executable=None,
773778
if not isinstance(bufsize, int):
774779
raise TypeError("bufsize must be an integer")
775780

781+
if pipesize is None:
782+
pipesize = -1 # Restore default
783+
if not isinstance(pipesize, int):
784+
raise TypeError("pipesize must be an integer")
785+
776786
if _mswindows:
777787
if preexec_fn is not None:
778788
raise ValueError("preexec_fn is not supported on Windows "
@@ -797,6 +807,7 @@ def __init__(self, args, bufsize=-1, executable=None,
797807
self.returncode = None
798808
self.encoding = encoding
799809
self.errors = errors
810+
self.pipesize = pipesize
800811

801812
# Validate the combinations of text and universal_newlines
802813
if (text is not None and universal_newlines is not None
@@ -1575,6 +1586,8 @@ def _get_handles(self, stdin, stdout, stderr):
15751586
pass
15761587
elif stdin == PIPE:
15771588
p2cread, p2cwrite = os.pipe()
1589+
if self.pipesize > 0 and hasattr(fcntl, "F_SETPIPE_SZ"):
1590+
fcntl.fcntl(p2cwrite, fcntl.F_SETPIPE_SZ, self.pipesize)
15781591
elif stdin == DEVNULL:
15791592
p2cread = self._get_devnull()
15801593
elif isinstance(stdin, int):
@@ -1587,6 +1600,8 @@ def _get_handles(self, stdin, stdout, stderr):
15871600
pass
15881601
elif stdout == PIPE:
15891602
c2pread, c2pwrite = os.pipe()
1603+
if self.pipesize > 0 and hasattr(fcntl, "F_SETPIPE_SZ"):
1604+
fcntl.fcntl(c2pwrite, fcntl.F_SETPIPE_SZ, self.pipesize)
15901605
elif stdout == DEVNULL:
15911606
c2pwrite = self._get_devnull()
15921607
elif isinstance(stdout, int):
@@ -1599,6 +1614,8 @@ def _get_handles(self, stdin, stdout, stderr):
15991614
pass
16001615
elif stderr == PIPE:
16011616
errread, errwrite = os.pipe()
1617+
if self.pipesize > 0 and hasattr(fcntl, "F_SETPIPE_SZ"):
1618+
fcntl.fcntl(errwrite, fcntl.F_SETPIPE_SZ, self.pipesize)
16021619
elif stderr == STDOUT:
16031620
if c2pwrite != -1:
16041621
errwrite = c2pwrite

Lib/test/test_fcntl.py

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -190,6 +190,19 @@ def test_fcntl_f_getpath(self):
190190
res = fcntl.fcntl(self.f.fileno(), fcntl.F_GETPATH, bytes(len(expected)))
191191
self.assertEqual(expected, res)
192192

193+
@unittest.skipIf(not (hasattr(fcntl, "F_SETPIPE_SZ") and hasattr(fcntl, "F_GETPIPE_SZ")),
194+
"F_SETPIPE_SZ and F_GETPIPE_SZ are not available on all unix platforms.")
195+
def test_fcntl_f_pipesize(self):
196+
test_pipe_r, test_pipe_w = os.pipe()
197+
# Get the default pipesize with F_GETPIPE_SZ
198+
pipesize_default = fcntl.fcntl(test_pipe_w, fcntl.F_GETPIPE_SZ)
199+
# Multiply the default with 2 to get a new value.
200+
fcntl.fcntl(test_pipe_w, fcntl.F_SETPIPE_SZ, pipesize_default * 2)
201+
self.assertEqual(fcntl.fcntl(test_pipe_w, fcntl.F_GETPIPE_SZ), pipesize_default * 2)
202+
os.close(test_pipe_r)
203+
os.close(test_pipe_w)
204+
205+
193206
def test_main():
194207
run_unittest(TestFcntl)
195208

Lib/test/test_subprocess.py

Lines changed: 46 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -39,6 +39,11 @@
3939
except ImportError:
4040
grp = None
4141

42+
try:
43+
import fcntl
44+
except:
45+
fcntl = None
46+
4247
if support.PGO:
4348
raise unittest.SkipTest("test is not helpful for PGO")
4449

@@ -661,6 +666,46 @@ def test_stdin_devnull(self):
661666
p.wait()
662667
self.assertEqual(p.stdin, None)
663668

669+
def test_pipesizes(self):
670+
# stdin redirection
671+
pipesize = 16 * 1024
672+
p = subprocess.Popen([sys.executable, "-c",
673+
'import sys; sys.stdin.read(); sys.stdout.write("out"); sys.stderr.write("error!")'],
674+
stdin=subprocess.PIPE,
675+
stdout=subprocess.PIPE,
676+
stderr=subprocess.PIPE,
677+
pipesize=pipesize)
678+
# We only assert pipe size has changed on platforms that support it.
679+
if sys.platform != "win32" and hasattr(fcntl, "F_GETPIPE_SZ"):
680+
for fifo in [p.stdin, p.stdout, p.stderr]:
681+
self.assertEqual(fcntl.fcntl(fifo.fileno(), fcntl.F_GETPIPE_SZ), pipesize)
682+
# Windows pipe size can be acquired with the GetNamedPipeInfoFunction
683+
# https://docs.microsoft.com/en-us/windows/win32/api/namedpipeapi/nf-namedpipeapi-getnamedpipeinfo
684+
# However, this function is not yet in _winapi.
685+
p.stdin.write(b"pear")
686+
p.stdin.close()
687+
p.wait()
688+
689+
def test_pipesize_default(self):
690+
p = subprocess.Popen([sys.executable, "-c",
691+
'import sys; sys.stdin.read(); sys.stdout.write("out");'
692+
' sys.stderr.write("error!")'],
693+
stdin=subprocess.PIPE,
694+
stdout=subprocess.PIPE,
695+
stderr=subprocess.PIPE,
696+
pipesize=-1)
697+
# UNIX tests using fcntl
698+
if sys.platform != "win32" and hasattr(fcntl, "F_GETPIPE_SZ"):
699+
fp_r, fp_w = os.pipe()
700+
default_pipesize = fcntl.fcntl(fp_w, fcntl.F_GETPIPE_SZ)
701+
for fifo in [p.stdin, p.stdout, p.stderr]:
702+
self.assertEqual(
703+
fcntl.fcntl(fifo.fileno(), fcntl.F_GETPIPE_SZ), default_pipesize)
704+
# On other platforms we cannot test the pipe size (yet). But above code
705+
# using pipesize=-1 should not crash.
706+
p.stdin.close()
707+
p.wait()
708+
664709
def test_env(self):
665710
newenv = os.environ.copy()
666711
newenv["FRUIT"] = "orange"
@@ -3503,7 +3548,7 @@ def test_getoutput(self):
35033548

35043549
def test__all__(self):
35053550
"""Ensure that __all__ is populated properly."""
3506-
intentionally_excluded = {"list2cmdline", "Handle", "pwd", "grp"}
3551+
intentionally_excluded = {"list2cmdline", "Handle", "pwd", "grp", "fcntl"}
35073552
exported = set(subprocess.__all__)
35083553
possible_exports = set()
35093554
import types

Misc/ACKS

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1818,6 +1818,7 @@ Johannes Vogel
18181818
Michael Vogt
18191819
Radu Voicilas
18201820
Alex Volkov
1821+
Ruben Vorderman
18211822
Guido Vranken
18221823
Martijn Vries
18231824
Sjoerd de Vries
Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
Add F_SETPIPE_SZ and F_GETPIPE_SZ to fcntl module. Allow setting pipesize on
2+
subprocess.Popen.

Modules/fcntlmodule.c

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -565,6 +565,14 @@ all_ins(PyObject* m)
565565
if (PyModule_AddIntMacro(m, F_SHLCK)) return -1;
566566
#endif
567567

568+
/* Linux specifics */
569+
#ifdef F_SETPIPE_SZ
570+
if (PyModule_AddIntMacro(m, F_SETPIPE_SZ)) return -1;
571+
#endif
572+
#ifdef F_GETPIPE_SZ
573+
if (PyModule_AddIntMacro(m, F_GETPIPE_SZ)) return -1;
574+
#endif
575+
568576
/* OS X specifics */
569577
#ifdef F_FULLFSYNC
570578
if (PyModule_AddIntMacro(m, F_FULLFSYNC)) return -1;

0 commit comments

Comments
 (0)