Movatterモバイル変換


[0]ホーム

URL:


Skip to content

Navigation Menu

Sign in
Appearance settings

Search code, repositories, users, issues, pull requests...

Provide feedback

We read every piece of feedback, and take your input very seriously.

Saved searches

Use saved searches to filter your results more quickly

Sign up
Appearance settings

gh-99813: Start usingSSL_sendfile when available#99907

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 ourterms of service andprivacy statement. We’ll occasionally send you account related emails.

Already on GitHub?Sign in to your account

Open
illia-v wants to merge54 commits intopython:main
base:main
Choose a base branch
Loading
fromillia-v:SSL_sendfile
Open
Show file tree
Hide file tree
Changes fromall commits
Commits
Show all changes
54 commits
Select commitHold shift + click to select a range
a018beb
Add methods for checking whether kTLS is used
illia-vSep 11, 2022
72e7f5e
Start using `SSL_sendfile` when available
illia-vNov 30, 2022
cf24d78
Try fixing warnings
illia-vDec 5, 2022
e9ef747
Use unsigned `size_t` instead of `Py_ssize_t`
illia-vDec 5, 2022
646d328
Merge branch 'main' into SSL_sendfile
illia-vDec 5, 2022
76353c7
Merge branch 'main'
illia-vMar 13, 2023
14832de
Make `sendfile_impl` more similar to `write_impl`
illia-vMar 13, 2023
6b41d31
Modify `test_ssl` to test `SSL_sendfile` calls
illia-vMar 13, 2023
8ac6ff9
Modify documentation and add a news entry
illia-vMar 13, 2023
5cf9483
Fix a test
illia-vMar 13, 2023
fc8e82f
Rename `uses_ktls_for_write` to `uses_ktls_for_send`
illia-vMar 13, 2023
7fc9b50
Merge branch 'main' into SSL_sendfile
illia-vJul 25, 2023
1ba04ae
Update `versionchanged`
illia-vJul 25, 2023
365e0c0
Update a test to get successful HTTP responses
illia-vJul 25, 2023
8fbc955
Add setting errors
illia-vJul 25, 2023
21ea0ca
Fix conditions
illia-vJul 25, 2023
17d9685
Add handling of `SSL_R_UNINITIALIZED`
illia-vJul 26, 2023
a92ae7e
Merge branch 'main' into SSL_sendfile
illia-vJul 26, 2023
4e8b6d8
Modify `test_sendfile` to avoid the internet
illia-vJul 26, 2023
379d242
Refactor `test_sendfile` a bit
illia-vJul 26, 2023
7de3dcb
Merge branch 'main' into SSL_sendfile
illia-vOct 4, 2023
bbe21c0
Merge branch 'main' into SSL_sendfile
illia-vOct 19, 2023
7323ec0
Try to fix new warnings
illia-vOct 19, 2023
2dc6947
Merge branch 'main' into SSL_sendfile
illia-vFeb 27, 2024
6e902ad
Merge branch 'main' into SSL_sendfile
illia-vMar 27, 2024
e435b9a
Apply a change from ea9a296fce2f786b4cf43c7924e5de01061f27ca
illia-vMar 27, 2024
ac1b2b2
Merge branch 'main' into SSL_sendfile
illia-vJun 6, 2024
7d11a59
Merge branch 'main' into SSL_sendfile
illia-vJul 31, 2024
99e89d3
Merge branch 'main' into SSL_sendfile
illia-vSep 28, 2024
dc626c8
Merge branch 'main' into SSL_sendfile
illia-vDec 27, 2024
291a5b7
Set `versionchanged` to next in docs
illia-vDec 27, 2024
3193ba4
Merge branch 'main' into SSL_sendfile
illia-vFeb 3, 2025
d870f92
Merge branch 'main' into SSL_sendfile
illia-vApr 10, 2025
94e522e
Apply some suggestions from code review
illia-vApr 12, 2025
25bde6f
Apply suggestions to Python code
illia-vApr 12, 2025
9a6a120
Improve style of `_ssl__SSLSocket_sendfile_impl`
illia-vApr 12, 2025
eaa0b2c
Merge branch 'main' into SSL_sendfile
illia-vApr 12, 2025
05a0c6c
Drop `_sendfile_use_ssl_sendfile`
illia-vApr 13, 2025
6b4eed6
Merge remote-tracking branch 'python/main' into SSL_sendfile
illia-vApr 13, 2025
31ed52d
Apply suggestions from code review
illia-vApr 18, 2025
5a22e6b
Merge branch 'main' into SSL_sendfile
illia-vApr 18, 2025
ada3f30
Improve kTLS checks for older OpenSSL
illia-vApr 18, 2025
3350854
Merge branch 'main' into SSL_sendfile
illia-vApr 28, 2025
3339673
Rename `uses_ktls_for_read` to `uses_ktls_for_recv`
illia-vApr 28, 2025
ee573ad
Apply PEP 7
illia-vApr 28, 2025
6795f62
Use an alternative method of returning booleans
illia-vApr 28, 2025
6a35ac6
Mark new functions with `critical_section`
illia-vMay 2, 2025
9dffdbd
Merge two if blocks
illia-vMay 2, 2025
19d5746
Reword docs
illia-vMay 2, 2025
ef3744e
Update the news entry
illia-vMay 2, 2025
e64f329
Use `Py_RETURN_FALSE` again
illia-vMay 2, 2025
028067a
Merge remote-tracking branch 'python/main' into SSL_sendfile
illia-vMay 2, 2025
6e099fa
Merge branch 'main' into SSL_sendfile
illia-vMay 5, 2025
f363ec3
Merge branch 'main' into SSL_sendfile
illia-vMay 19, 2025
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
10 changes: 8 additions & 2 deletionsDoc/library/ssl.rst
View file
Open in desktop
Original file line numberDiff line numberDiff line change
Expand Up@@ -1078,8 +1078,9 @@ SSL Sockets
(but passing a non-zero ``flags`` argument is not allowed)
- :meth:`~socket.socket.send`, :meth:`~socket.socket.sendall` (with
the same limitation)
- :meth:`~socket.socket.sendfile` (but :mod:`os.sendfile` will be used
for plain-text sockets only, else :meth:`~socket.socket.send` will be used)
- :meth:`~socket.socket.sendfile` (it may be high-performant only when
the kernel TLS is enabled by setting :data:`~ssl.OP_ENABLE_KTLS` or when a
socket is plain-text, else :meth:`~socket.socket.send` will be used)
- :meth:`~socket.socket.shutdown`

However, since the SSL (and TLS) protocol has its own framing atop
Expand DownExpand Up@@ -1113,6 +1114,11 @@ SSL Sockets
functions support reading and writing of data larger than 2 GB. Writing
zero-length data no longer fails with a protocol violation error.

.. versionchanged:: next
Python now uses ``SSL_sendfile`` internally when possible. The
function sends a file more efficiently because it performs TLS encryption
in the kernel to avoid additional context switches.

SSL sockets also have the following additional methods and attributes:

.. method:: SSLSocket.read(len=1024, buffer=None)
Expand Down
143 changes: 76 additions & 67 deletionsLib/socket.py
View file
Open in desktop
Original file line numberDiff line numberDiff line change
Expand Up@@ -56,6 +56,7 @@
import os
import sys
from enum import IntEnum, IntFlag
from functools import partial

try:
import errno
Expand DownExpand Up@@ -348,75 +349,83 @@ def makefile(self, mode="r", buffering=None, *,
text.mode = mode
return text

if hasattr(os, 'sendfile'):
def _sendfile_zerocopy(self, zerocopy_func, giveup_exc_type, file,
offset=0, count=None):
"""
Send a file using a zero-copy function.
"""
import selectors

def _sendfile_use_sendfile(self, file, offset=0, count=None):
# Lazy import to improve module import time
import selectors
self._check_sendfile_params(file, offset, count)
sockno = self.fileno()
try:
fileno = file.fileno()
except (AttributeError, io.UnsupportedOperation) as err:
raise giveup_exc_type(err) # not a regular file
try:
fsize = os.fstat(fileno).st_size
except OSError as err:
raise giveup_exc_type(err) # not a regular file
if not fsize:
return 0 # empty file
# Truncate to 1GiB to avoid OverflowError, see bpo-38319.
blocksize = min(count or fsize, 2 ** 30)
timeout = self.gettimeout()
if timeout == 0:
raise ValueError("non-blocking sockets are not supported")
# poll/select have the advantage of not requiring any
# extra file descriptor, contrarily to epoll/kqueue
# (also, they require a single syscall).
if hasattr(selectors, 'PollSelector'):
selector = selectors.PollSelector()
else:
selector = selectors.SelectSelector()
selector.register(sockno, selectors.EVENT_WRITE)

self._check_sendfile_params(file, offset, count)
sockno = self.fileno()
try:
fileno = file.fileno()
except (AttributeError, io.UnsupportedOperation) as err:
raise _GiveupOnSendfile(err) # not a regular file
try:
fsize = os.fstat(fileno).st_size
except OSError as err:
raise _GiveupOnSendfile(err) # not a regular file
if not fsize:
return 0 # empty file
# Truncate to 1GiB to avoid OverflowError, see bpo-38319.
blocksize = min(count or fsize, 2 ** 30)
timeout = self.gettimeout()
if timeout == 0:
raise ValueError("non-blocking sockets are not supported")
# poll/select have the advantage of not requiring any
# extra file descriptor, contrarily to epoll/kqueue
# (also, they require a single syscall).
if hasattr(selectors, 'PollSelector'):
selector = selectors.PollSelector()
else:
selector = selectors.SelectSelector()
selector.register(sockno, selectors.EVENT_WRITE)

total_sent = 0
# localize variable access to minimize overhead
selector_select = selector.select
os_sendfile = os.sendfile
try:
while True:
if timeout and not selector_select(timeout):
raise TimeoutError('timed out')
if count:
blocksize = min(count - total_sent, blocksize)
if blocksize <= 0:
break
try:
sent = os_sendfile(sockno, fileno, offset, blocksize)
except BlockingIOError:
if not timeout:
# Block until the socket is ready to send some
# data; avoids hogging CPU resources.
selector_select()
continue
except OSError as err:
if total_sent == 0:
# We can get here for different reasons, the main
# one being 'file' is not a regular mmap(2)-like
# file, in which case we'll fall back on using
# plain send().
raise _GiveupOnSendfile(err)
raise err from None
else:
if sent == 0:
break # EOF
offset += sent
total_sent += sent
return total_sent
finally:
if total_sent > 0 and hasattr(file, 'seek'):
file.seek(offset)
total_sent = 0
# localize variable access to minimize overhead
selector_select = selector.select
try:
while True:
if timeout and not selector_select(timeout):
raise TimeoutError('timed out')
if count:
blocksize = min(count - total_sent, blocksize)
if blocksize <= 0:
break
try:
sent = zerocopy_func(fileno, offset, blocksize)
except BlockingIOError:
if not timeout:
# Block until the socket is ready to send some
# data; avoids hogging CPU resources.
selector_select()
continue
except OSError as err:
if total_sent == 0:
# We can get here for different reasons, the main
# one being 'file' is not a regular mmap(2)-like
# file, in which case we'll fall back on using
# plain send().
raise giveup_exc_type(err)
raise err from None
else:
if sent == 0:
break # EOF
offset += sent
total_sent += sent
return total_sent
finally:
if total_sent > 0 and hasattr(file, 'seek'):
file.seek(offset)

if hasattr(os, 'sendfile'):
def _sendfile_use_sendfile(self, file, offset=0, count=None):
return self._sendfile_zerocopy(
partial(os.sendfile, self.fileno()),
_GiveupOnSendfile,
file, offset, count,
)
else:
def _sendfile_use_sendfile(self, file, offset=0, count=None):
raise _GiveupOnSendfile(
Expand Down
27 changes: 21 additions & 6 deletionsLib/ssl.py
View file
Open in desktop
Original file line numberDiff line numberDiff line change
Expand Up@@ -975,6 +975,10 @@ def _sslcopydoc(func):
return func


class _GiveupOnSSLSendfile(Exception):
pass


class SSLSocket(socket):
"""This class implements a subtype of socket.socket that wraps
the underlying OS socket in an SSL context when necessary, and
Expand DownExpand Up@@ -1266,15 +1270,26 @@ def sendall(self, data, flags=0):
return super().sendall(data, flags)

def sendfile(self, file, offset=0, count=None):
"""Send a file, possibly by usingos.sendfile()if this is a
clear-text socket. Return the total number of bytes sent.
"""Send a file, possibly by usingan efficientsendfile()call if
the system supports it. Return the total number of bytes sent.
"""
if self._sslobj is not None:
return self._sendfile_use_send(file, offset, count)
else:
# os.sendfile() works with plain sockets only
if self._sslobj is None:
return super().sendfile(file, offset, count)

if not self._sslobj.uses_ktls_for_send():
return self._sendfile_use_send(file, offset, count)

sendfile = getattr(self._sslobj, "sendfile", None)
if sendfile is None:
return self._sendfile_use_send(file, offset, count)

try:
return self._sendfile_zerocopy(
sendfile, _GiveupOnSSLSendfile, file, offset, count,
)
except _GiveupOnSSLSendfile:
return self._sendfile_use_send(file, offset, count)

def recv(self, buflen=1024, flags=0):
self._checkClosed()
if self._sslobj is not None:
Expand Down
23 changes: 17 additions & 6 deletionsLib/test/test_ssl.py
View file
Open in desktop
Original file line numberDiff line numberDiff line change
Expand Up@@ -4297,19 +4297,30 @@ def test_read_write_after_close_raises_valuerror(self):
self.assertRaises(ValueError, s.write, b'hello')

def test_sendfile(self):
"""Try to send a file using kTLS if possible."""
TEST_DATA = b"x" * 512
with open(os_helper.TESTFN, 'wb') as f:
f.write(TEST_DATA)
self.addCleanup(os_helper.unlink, os_helper.TESTFN)
client_context, server_context, hostname = testing_context()
client_context.options |= getattr(ssl, 'OP_ENABLE_KTLS', 0)
server = ThreadedEchoServer(context=server_context, chatty=False)
with server:
with client_context.wrap_socket(socket.socket(),
server_hostname=hostname) as s:
s.connect((HOST, server.port))
# kTLS seems to work only with a connection created before
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others.Learn more.

Can you check whether it "seems" to work or not? (for a personal project I would accept this but I'm interested in knowing whether this is an issue with Python SSL or OpenSSL)

Copy link
ContributorAuthor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others.Learn more.

Okay, I'll post more details about this soon

Copy link
ContributorAuthor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others.Learn more.

I can reproduce the same issue with C code using OpenSSL (without Python). Please check my findingshere. I mentioned a possible patch, let me know if we can apply it or there can be undesirable consequences.

FYI,ssl.OP_ENABLE_KTLS to enable kTLS has been available since Python 3.12 (and it could be added to options as a simple integer even before). And the issue is not specific toSSL_sendfile, it affects kTLS availability for simple reads and writes.

I added my reproducer to an existing OpenSSL issueopenssl/openssl#19676 (comment).

# wrapping `sock` by the SSL context in contrast to calling
# `sock.connect()` after the wrapping.
with server, socket.create_connection((HOST, server.port)) as sock:
with client_context.wrap_socket(
sock, server_hostname=hostname
) as ssock:
if support.verbose:
ktls_used = ssock._sslobj.uses_ktls_for_send()
print(
'kTLS is',
'available' if ktls_used else 'unavailable',
)
with open(os_helper.TESTFN, 'rb') as file:
s.sendfile(file)
self.assertEqual(s.recv(1024), TEST_DATA)
ssock.sendfile(file)
self.assertEqual(ssock.recv(1024), TEST_DATA)

def test_session(self):
client_context, server_context, hostname = testing_context()
Expand Down
View file
Open in desktop
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
:mod:`ssl` now uses ``SSL_sendfile`` internally when it is possible (see
:data:`~ssl.OP_ENABLE_KTLS`). The function sends a file more efficiently
because it performs TLS encryption in the kernel to avoid additional context
switches. Patch by Illia Volochii.
Loading
Loading

[8]ページ先頭

©2009-2025 Movatter.jp