Linux splice 可以在内核态搬运文件和 socket 数据

splice 是 Linux 提供的零拷贝数据搬运接口。它可以在文件描述符之间移动数据,并尽量避免把数据复制到用户态缓冲区。典型用途是文件、管道、socket 之间的数据转发。

普通读写路径通常是:

kernel -> user buffer -> kernel

splice 的目标是减少中间这次用户态拷贝:

kernel -> pipe buffer -> kernel

函数签名

#define _GNU_SOURCE
#include <fcntl.h>

ssize_t splice(int fd_in, loff_t *off_in,
               int fd_out, loff_t *off_out,
               size_t len, unsigned int flags);

参数含义:

返回值是实际移动的字节数。返回 0 通常表示输入已经到达 EOF。返回 -1 表示失败,并设置 errno

pipe 是核心中转结构

splice 的一个重要限制是:输入或输出的一端通常需要是 pipe。因此常见写法是两段式:

int pipe_fds[2];
pipe(pipe_fds);

splice(file_fd, NULL, pipe_fds[1], NULL, len, 0);
splice(pipe_fds[0], NULL, socket_fd, NULL, len, 0);

第一步把数据从文件移动到 pipe,第二步把数据从 pipe 移动到 socket。用户态代码不直接接触数据内容,只负责安排数据路径。

文件到 socket 的示例

下面的示例把本地文件内容发送到已连接的 socket:

#define _GNU_SOURCE
#include <fcntl.h>
#include <unistd.h>
#include <sys/socket.h>

int send_file_by_splice(int file_fd, int socket_fd, size_t total_len) {
    int pipe_fds[2];
    if (pipe(pipe_fds) < 0) {
        return -1;
    }

    size_t sent = 0;
    while (sent < total_len) {
        size_t chunk = total_len - sent;
        if (chunk > 65536) {
            chunk = 65536;
        }

        ssize_t moved = splice(file_fd, NULL, pipe_fds[1], NULL, chunk, 0);
        if (moved <= 0) {
            break;
        }

        ssize_t written = splice(pipe_fds[0], NULL, socket_fd, NULL, moved, 0);
        if (written <= 0) {
            break;
        }

        sent += written;
    }

    close(pipe_fds[0]);
    close(pipe_fds[1]);
    return 0;
}

这个函数适合用于代理、网关、静态文件服务等数据转发场景。它减少用户态内存拷贝,但仍然需要处理短读、短写、阻塞、错误返回等情况。

常用 flags

spliceflags 可以组合以下选项:

普通阻塞式代码可以先使用 0,把正确性处理清楚后再考虑 flags。

注意事项

splice 不等于所有场景都更快。它减少数据拷贝,但会引入 pipe 管理、系统调用次数、阻塞语义和内核路径差异。小数据量、需要用户态解析或修改内容的场景,普通 read/write 更直接。

使用 splice 时应检查每一次返回值。实际移动字节数可能小于请求的 len,网络 socket 尤其需要循环处理。错误路径也要关闭 pipe 两端,避免文件描述符泄漏。

总结

splice 的核心价值是让数据在内核对象之间流动,减少用户态缓冲区参与。典型模式是 fd -> pipe -> fd。它适合大块转发和代理类场景,不适合需要频繁检查或修改数据内容的业务逻辑。

本文由 AI 辅助生成,可能存在错误或遗漏,请以实际资料和官方文档为准。