大きなファイルをネットワーク経由で送信する場合、sendfile システムコールを使うとカーネル空間だけでデータを転送でき、ユーザー空間へのコピーを省略できる。これをゼロコピー転送と呼ぶ。
通常のファイル送信の問題
通常の方法では、データがカーネル空間とユーザー空間を何度も行き来する。
# ❌ 効率が悪い(4回のコピー)
with open('large_file.bin', 'rb') as f:
data = f.read() # カーネル → ユーザー空間(コピー1, 2)
socket.sendall(data) # ユーザー空間 → カーネル(コピー3, 4)
この方法では、CPU とメモリの帯域幅を無駄に消費する。
os.sendfile() を使う
Python 3.3 以降、os.sendfile() でゼロコピー転送ができる(Unix 系のみ)。
import os
import socket
def send_file_zero_copy(sock, filepath):
with open(filepath, 'rb') as f:
# ファイルサイズを取得
file_size = os.fstat(f.fileno()).st_size
# sendfile でゼロコピー転送
sent = 0
while sent < file_size:
sent += os.sendfile(
sock.fileno(), # 送信先ソケット
f.fileno(), # 送信元ファイル
sent, # オフセット
file_size - sent # 送信バイト数
)
return sent
os.sendfile() はカーネル空間内でファイルからソケットに直接データを転送する。
socket.sendfile() を使う(推奨)
Python 3.5 以降では、socket.sendfile() がより使いやすい。
import socket
def send_file_easy(sock, filepath):
with open(filepath, 'rb') as f:
sock.sendfile(f)
内部で os.sendfile() を使用し、フォールバック処理も行ってくれる。
HTTP サーバーでの例
簡易的な HTTP サーバーでファイルを効率的に送信する例を示す。
import socket
import os
def serve_file(client_sock, filepath):
file_size = os.path.getsize(filepath)
# HTTP ヘッダーを送信
header = (
f'HTTP/1.1 200 OK\r\n'
f'Content-Length: {file_size}\r\n'
f'Content-Type: application/octet-stream\r\n'
f'\r\n'
)
client_sock.sendall(header.encode())
# ファイル本体をゼロコピーで送信
with open(filepath, 'rb') as f:
client_sock.sendfile(f)
shutil.copyfileobj() との比較
shutil.copyfileobj() はユーザー空間でコピーを行う。
import shutil
# ユーザー空間コピー(チャンク単位)
with open('src.bin', 'rb') as src, open('dst.bin', 'wb') as dst:
shutil.copyfileobj(src, dst, length=1024*1024)
ファイル間のコピーには shutil.copy() が使えるが、内部的にはユーザー空間を経由する。
copy_file_range() を使う(Linux 4.5+)
Linux 4.5 以降では os.copy_file_range() でファイル間のゼロコピーコピーができる。
import os
def zero_copy_file(src_path, dst_path):
with open(src_path, 'rb') as src:
with open(dst_path, 'wb') as dst:
src_size = os.fstat(src.fileno()).st_size
copied = 0
while copied < src_size:
copied += os.copy_file_range(
src.fileno(),
dst.fileno(),
src_size - copied,
copied, # src_offset
copied # dst_offset
)
splice() の代替
Unix の splice() システムコールは Python 標準ライブラリにはないが、ctypes で呼び出すことができる。通常は sendfile() で十分だ。
Windows での制限
os.sendfile() は Windows では利用できない。Windows では TransmitFile API を使う必要があるが、Python 標準ライブラリではサポートされていない。
import os
import sys
def send_file_cross_platform(sock, filepath):
with open(filepath, 'rb') as f:
if hasattr(sock, 'sendfile') and sys.platform != 'win32':
sock.sendfile(f)
else:
# フォールバック:チャンク単位で送信
while chunk := f.read(65536):
sock.sendall(chunk)
パフォーマンス比較
大きなファイル(数 GB)の転送では、ゼロコピーにより数十パーセントの性能向上が見込める。特に CPU 使用率が大幅に下がる。
| 方式 | CPU 使用率 | スループット |
|---|---|---|
| read() + send() | 高い | 標準 |
| sendfile() | 低い | 高い |
ただし、小さなファイルや低速なネットワークでは差が出にくい。