Skip to content

Commit

Permalink
bpo-41625: Expose the splice() system call in the os module (pythonGH…
Browse files Browse the repository at this point in the history
  • Loading branch information
pablogsal authored Nov 17, 2020
1 parent cce3f0b commit a57b3d3
Show file tree
Hide file tree
Showing 10 changed files with 349 additions and 78 deletions.
32 changes: 32 additions & 0 deletions Doc/library/os.rst
Original file line number Diff line number Diff line change
Expand Up @@ -1419,6 +1419,38 @@ or `the MSDN <https://msdn.microsoft.com/en-us/library/z0kc8e3z.aspx>`_ on Windo
.. versionadded:: 3.3


.. function:: splice(src, dst, count, offset_src=None, offset_dst=None)

Transfer *count* bytes from file descriptor *src*, starting from offset
*offset_src*, to file descriptor *dst*, starting from offset *offset_dst*.
At least one of the file descriptors must refer to a pipe. If *offset_src*
is None, then *src* is read from the current position; respectively for
*offset_dst*. The offset associated to the file descriptor that refers to a
pipe must be ``None``. The files pointed by *src* and *dst* must reside in
the same filesystem, otherwise an :exc:`OSError` is raised with
:attr:`~OSError.errno` set to :data:`errno.EXDEV`.

This copy is done without the additional cost of transferring data
from the kernel to user space and then back into the kernel. Additionally,
some filesystems could implement extra optimizations. The copy is done as if
both files are opened as binary.

Upon successful completion, returns the number of bytes spliced to or from
the pipe. A return value of 0 means end of input. If *src* refers to a
pipe, then this means that there was no data to transfer, and it would not
make sense to block because there are no writers connected to the write end
of the pipe.

.. availability:: Linux kernel >= 2.6.17 or glibc >= 2.5

.. versionadded:: 3.10


.. data:: SPLICE_F_MOVE
SPLICE_F_NONBLOCK
SPLICE_F_MORE


.. function:: readv(fd, buffers)

Read from a file descriptor *fd* into a number of mutable :term:`bytes-like
Expand Down
5 changes: 5 additions & 0 deletions Doc/whatsnew/3.10.rst
Original file line number Diff line number Diff line change
Expand Up @@ -233,6 +233,11 @@ Added a new function :func:`os.eventfd` and related helpers to wrap the
``eventfd2`` syscall on Linux.
(Contributed by Christian Heimes in :issue:`41001`.)

Added :func:`os.splice()` that allows to move data between two file
descriptors without copying between kernel address space and user
address space, where one of the file descriptors must refer to a
pipe. (Contributed by Pablo Galindo in :issue:`41625`.)

py_compile
----------

Expand Down
117 changes: 117 additions & 0 deletions Lib/test/test_os.py
Original file line number Diff line number Diff line change
Expand Up @@ -381,6 +381,123 @@ def test_copy_file_range_offset(self):
self.assertEqual(read[out_seek:],
data[in_skip:in_skip+i])

@unittest.skipUnless(hasattr(os, 'splice'), 'test needs os.splice()')
def test_splice_invalid_values(self):
with self.assertRaises(ValueError):
os.splice(0, 1, -10)

@unittest.skipUnless(hasattr(os, 'splice'), 'test needs os.splice()')
def test_splice(self):
TESTFN2 = os_helper.TESTFN + ".3"
data = b'0123456789'

create_file(os_helper.TESTFN, data)
self.addCleanup(os_helper.unlink, os_helper.TESTFN)

in_file = open(os_helper.TESTFN, 'rb')
self.addCleanup(in_file.close)
in_fd = in_file.fileno()

read_fd, write_fd = os.pipe()
self.addCleanup(lambda: os.close(read_fd))
self.addCleanup(lambda: os.close(write_fd))

try:
i = os.splice(in_fd, write_fd, 5)
except OSError as e:
# Handle the case in which Python was compiled
# in a system with the syscall but without support
# in the kernel.
if e.errno != errno.ENOSYS:
raise
self.skipTest(e)
else:
# The number of copied bytes can be less than
# the number of bytes originally requested.
self.assertIn(i, range(0, 6));

self.assertEqual(os.read(read_fd, 100), data[:i])

@unittest.skipUnless(hasattr(os, 'splice'), 'test needs os.splice()')
def test_splice_offset_in(self):
TESTFN4 = os_helper.TESTFN + ".4"
data = b'0123456789'
bytes_to_copy = 6
in_skip = 3

create_file(os_helper.TESTFN, data)
self.addCleanup(os_helper.unlink, os_helper.TESTFN)

in_file = open(os_helper.TESTFN, 'rb')
self.addCleanup(in_file.close)
in_fd = in_file.fileno()

read_fd, write_fd = os.pipe()
self.addCleanup(lambda: os.close(read_fd))
self.addCleanup(lambda: os.close(write_fd))

try:
i = os.splice(in_fd, write_fd, bytes_to_copy, offset_src=in_skip)
except OSError as e:
# Handle the case in which Python was compiled
# in a system with the syscall but without support
# in the kernel.
if e.errno != errno.ENOSYS:
raise
self.skipTest(e)
else:
# The number of copied bytes can be less than
# the number of bytes originally requested.
self.assertIn(i, range(0, bytes_to_copy+1));

read = os.read(read_fd, 100)
# 012 are skipped (in_skip)
# 345678 are copied in the file (in_skip + bytes_to_copy)
self.assertEqual(read, data[in_skip:in_skip+i])

@unittest.skipUnless(hasattr(os, 'splice'), 'test needs os.splice()')
def test_splice_offset_out(self):
TESTFN4 = os_helper.TESTFN + ".4"
data = b'0123456789'
bytes_to_copy = 6
out_seek = 3

create_file(os_helper.TESTFN, data)
self.addCleanup(os_helper.unlink, os_helper.TESTFN)

read_fd, write_fd = os.pipe()
self.addCleanup(lambda: os.close(read_fd))
self.addCleanup(lambda: os.close(write_fd))
os.write(write_fd, data)

out_file = open(TESTFN4, 'w+b')
self.addCleanup(os_helper.unlink, TESTFN4)
self.addCleanup(out_file.close)
out_fd = out_file.fileno()

try:
i = os.splice(read_fd, out_fd, bytes_to_copy, offset_dst=out_seek)
except OSError as e:
# Handle the case in which Python was compiled
# in a system with the syscall but without support
# in the kernel.
if e.errno != errno.ENOSYS:
raise
self.skipTest(e)
else:
# The number of copied bytes can be less than
# the number of bytes originally requested.
self.assertIn(i, range(0, bytes_to_copy+1));

with open(TESTFN4, 'rb') as in_file:
read = in_file.read()
# seeked bytes (5) are zero'ed
self.assertEqual(read[:out_seek], b'\x00'*out_seek)
# 012 are skipped (in_skip)
# 345678 are copied in the file (in_skip + bytes_to_copy)
self.assertEqual(read[out_seek:], data[:i])


# Test attributes on return values from os.*stat* family.
class StatAttributeTests(unittest.TestCase):
def setUp(self):
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
Expose the :c:func:`splice` as :func:`os.splice` in the :mod:`os` module.
Patch by Pablo Galindo
106 changes: 105 additions & 1 deletion Modules/clinic/posixmodule.c.h

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Loading

0 comments on commit a57b3d3

Please sign in to comment.