sendfile でゼロコピー転送する

大きなファイルをネットワーク経由で送信する場合、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()低い高い

ただし、小さなファイルや低速なネットワークでは差が出にくい。