各位 Linux 江湖的 “老油条” 们,今天咱们来唠唠内核里的 “数据搬运工”——splice系统调用。这货在 Linux 世界里可是个 “效率怪咖”,能让数据传输像 “隔空传物” 一样丝滑,比传统的read+write快到飞起。
先整段接地气的类比:工地搬砖界的 “传送带”
想象你是工地包工头,要把一堆砖从 A 仓库搬到 B 仓库:
- 传统操作(read+write):工人先把砖从 A 仓库搬到自己的小推车(用户态缓冲区),推到 B 仓库门口,再一块块卸下来搬进 B 仓库 —— 累得满头大汗,还容易掉砖(数据拷贝出错)。
- splice 操作:直接在 A 仓库和 B 仓库之间架一条传送带,砖从 A 仓库直接滑到 B 仓库,工人只需要按一下开关 —— 全程零搬运,效率直接翻 10 倍!
splice就是 Linux 内核里的 “传送带”,它能在两个文件描述符(比如文件、socket、管道)之间直接搬运数据,完全绕开用户态缓冲区,堪称 “零拷贝” 界的扛把子。
啥时候请这位 “搬运大神” 出山?
只要你需要在两个 “数据端点”(比如文件→网络、管道→文件、socket→socket)之间搬数据,用它就对了。尤其是高并发服务器(比如 Nginx 就爱用它),或者处理大文件传输时,splice能让你的程序从 “拖拉机” 变 “跑车”。
上车前先认认 “传送带按钮”(splice 参数)
ssize_t splice(int fd_in, loff_t *off_in, int fd_out, loff_t *off_out, size_t len, unsigned int flags);
参数看着多,其实就 5 个关键点:
- fd_in:来源 “仓库”(文件描述符,比如打开的文件、socket)。
- off_in:从来源的哪个位置开始搬(NULL 表示用当前偏移量)。
- fd_out:目标 “仓库”(另一个文件描述符)。
- off_out:搬到目标的哪个位置(NULL 同理)。
- len:要搬多少字节。
- flags:传送带的 “模式开关”(比如SPLICE_F_MOVE表示尽量移动数据,SPLICE_F_NONBLOCK表示非阻塞)。
返回值:成功搬了多少字节,失败返回 - 1(记得看errno找原因)。
案例 1:最基础的 “传送带”—— 文件内容直接怼到屏幕
目标:用splice把一个文本文件的内容直接输出到终端(stdout),跳过用户态缓冲区。
步骤 1:写代码(file_to_stdout.cpp)
cpp
运行
#include <iostream>
#include <fcntl.h>
#include <unistd.h>
#include <errno.h>
#include <string.h>
int main() {
// 1. 打开要搬运的文件(A仓库)
int fd_in = open("test.txt", O_RDONLY);
if (fd_in == -1) {
std::cerr << "打开文件失败!原因:" << strerror(errno) << std::endl;
return 1;
}
// 2. 目标是标准输出stdout(B仓库,文件描述符是1)
int fd_out = STDOUT_FILENO;
// 3. 用splice架起传送带,开始搬运
ssize_t total_bytes = 0;
const size_t BUFFER_SIZE = 4096; // 每次搬4KB(传送带宽度)
while (true) {
ssize_t bytes_moved = splice(
fd_in, // 来源:test.txt
NULL, // 从当前位置开始
fd_out, // 目标:屏幕
NULL, // 写到当前位置
BUFFER_SIZE, // 每次搬4KB
0 // 普通模式(不着急,稳着来)
);
if (bytes_moved == -1) {
std::cerr << "搬运失败!原因:" << strerror(errno) << std::endl;
close(fd_in);
return 1;
} else if (bytes_moved == 0) {
// 搬完了(文件到头了)
break;
}
total_bytes += bytes_moved;
}
// 4. 收尾工作
close(fd_in);
std::cout << "\n\n搞定!总共搬了" << total_bytes << "字节" << std::endl;
return 0;
}
步骤 2:准备工作
- 在代码同一目录下创建test.txt,随便写点内容(比如 “Hello splice!我是被搬运的测试文本~”)。
- 确保你的系统是 Linux(splice是 Linux 专属,Windows 和 macOS 没有哦)。
步骤 3:编译运行
- 打开终端,进入代码目录。
- 用 g++ 编译:g++ -o file_to_stdout file_to_stdout.cpp(不需要额外库,内核自带splice)。
- 运行程序:./file_to_stdout。
- 效果:test.txt的内容会直接显示在屏幕上,最后还会告诉你总共搬了多少字节 —— 成了!
案例 2:网络 “高速传送带”—— 服务器用 splice 给客户端发文件
目标:写一个简单的 TCP 服务器,收到客户端连接后,用splice把文件直接 “推” 给客户端,全程不碰用户态缓冲区。
步骤 1:服务器代码(splice_server.cpp)
cpp
运行
#include <iostream>
#include <fcntl.h>
#include <unistd.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <errno.h>
#include <string.h>
// 端口号(选个没人用的,比如8888)
#define PORT 8888
// 缓冲区大小(传送带宽度)
#define BUFFER_SIZE 4096
int main() {
// 1. 创建socket(相当于建个卸货点)
int server_fd = socket(AF_INET, SOCK_STREAM, 0);
if (server_fd == -1) {
std::cerr << "创建socket失败!原因:" << strerror(errno) << std::endl;
return 1;
}
// 2. 设置socket选项(允许端口复用,防止程序重启后端口被占用)
int opt = 1;
if (setsockopt(server_fd, SOL_SOCKET, SO_REUSEADDR, &opt, sizeof(opt)) == -1) {
std::cerr << "设置socket选项失败!原因:" << strerror(errno) << std::endl;
close(server_fd);
return 1;
}
// 3. 绑定端口(给卸货点挂个牌子)
sockaddr_in address;
address.sin_family = AF_INET;
address.sin_addr.s_addr = INADDR_ANY; // 监听所有本机IP
address.sin_port = htons(PORT); // 端口转网络字节序
if (bind(server_fd, (struct sockaddr*)&address, sizeof(address)) == -1) {
std::cerr << "绑定端口失败!原因:" << strerror(errno) << std::endl;
close(server_fd);
return 1;
}
// 4. 监听连接(打开卸货点大门,最多同时接待5个客户)
if (listen(server_fd, 5) == -1) {
std::cerr << "监听失败!原因:" << strerror(errno) << std::endl;
close(server_fd);
return 1;
}
std::cout << "服务器启动成功,在端口" << PORT << "等客户上门..." << std::endl;
// 5. 接受客户端连接(客户来了,开门迎接)
socklen_t addr_len = sizeof(address);
int client_fd = accept(server_fd, (struct sockaddr*)&address, &addr_len);
if (client_fd == -1) {
std::cerr << "接客失败!原因:" << strerror(errno) << std::endl;
close(server_fd);
return 1;
}
std::cout << "客户" << inet_ntoa(address.sin_addr) << "已连接,准备发文件~" << std::endl;
// 6. 打开要发送的文件(准备好要搬的货)
int file_fd = open("bigfile.dat", O_RDONLY);
if (file_fd == -1) {
std::cerr << "打开文件失败!原因:" << strerror(errno) << std::endl;
close(client_fd);
close(server_fd);
return 1;
}
// 7. 用splice把文件怼到客户端(传送带启动!)
ssize_t total_bytes = 0;
while (true) {
ssize_t bytes_moved = splice(
file_fd, // 来源:bigfile.dat
NULL, // 从当前位置开始
client_fd, // 目标:客户端socket
NULL, // 写到当前位置
BUFFER_SIZE, // 每次搬4KB
0 // 普通模式
);
if (bytes_moved == -1) {
std::cerr << "发送失败!原因:" << strerror(errno) << std::endl;
close(file_fd);
close(client_fd);
close(server_fd);
return 1;
} else if (bytes_moved == 0) {
// 文件发完了
break;
}
total_bytes += bytes_moved;
std::cout << "\r已发送:" << total_bytes << "字节" << std::flush; // 动态显示进度
}
// 8. 收尾工作
close(file_fd);
close(client_fd);
close(server_fd);
std::cout << "\n\n文件发送完毕!总共发了" << total_bytes << "字节" << std::endl;
return 0;
}
步骤 2:客户端代码(simple_client.cpp)
客户端用普通的recv接收就行,毕竟咱们主要秀服务器的splice:
cpp
运行
#include <iostream>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <unistd.h>
#include <fstream>
#include <errno.h>
#include <string.h>
#define PORT 8888
#define BUFFER_SIZE 4096
int main() {
// 1. 创建socket
int sock = socket(AF_INET, SOCK_STREAM, 0);
if (sock == -1) {
std::cerr << "创建socket失败!原因:" << strerror(errno) << std::endl;
return 1;
}
// 2. 连接服务器(服务器地址填自己的IP,本地测试用127.0.0.1)
sockaddr_in serv_addr;
serv_addr.sin_family = AF_INET;
serv_addr.sin_port = htons(PORT);
if (inet_pton(AF_INET, "127.0.0.1", &serv_addr.sin_addr) <= 0) {
std::cerr << "IP地址无效!" << std::endl;
close(sock);
return 1;
}
if (connect(sock, (struct sockaddr*)&serv_addr, sizeof(serv_addr)) == -1) {
std::cerr << "连接服务器失败!原因:" << strerror(errno) << std::endl;
close(sock);
return 1;
}
std::cout << "已连接服务器,开始接收文件..." << std::endl;
// 3. 接收文件并保存
std::ofstream outfile("received_bigfile.dat", std::ios::binary);
if (!outfile.is_open()) {
std::cerr << "创建接收文件失败!" << std::endl;
close(sock);
return 1;
}
char buffer[BUFFER_SIZE];
ssize_t bytes_read;
ssize_t total = 0;
while ((bytes_read = recv(sock, buffer, BUFFER_SIZE, 0)) > 0) {
outfile.write(buffer, bytes_read);
total += bytes_read;
std::cout << "\r已接收:" << total << "字节" << std::flush;
}
// 4. 收尾
outfile.close();
close(sock);
if (bytes_read == -1) {
std::cerr << "\n接收失败!原因:" << strerror(errno) << std::endl;
return 1;
}
std::cout << "\n\n文件接收完毕!总共收了" << total << "字节" << std::endl;
return 0;
}
步骤 3:准备工作
- 创建一个测试文件bigfile.dat(可以用dd if=/dev/zero of=bigfile.dat bs=1M count=10生成一个 10MB 的空文件,方便测试)。
- 确保服务器和客户端在同一台 Linux 机器上(本地测试),或者修改客户端的 IP 为服务器的实际 IP。
步骤 4:编译运行
- 编译服务器:g++ -o splice_server splice_server.cpp。
- 编译客户端:g++ -o simple_client simple_client.cpp。
- 先启动服务器:./splice_server(会显示 “等客户上门”)。
- 再启动客户端:./simple_client(会显示接收进度)。
- 结束后,当前目录会出现received_bigfile.dat,用ls -l看看大小和bigfile.dat是否一致 —— 完美!
为啥这货比传统方法快?
传统read+write流程:数据从内核(文件)→用户态缓冲区→内核(socket),像 “快递先送到你家,你再转寄出去”,多了两次拷贝。
splice流程:数据直接在内核里从文件 “滑” 到 socket,全程不碰用户态,像 “快递直接从仓库转发”,零拷贝 —— 这就是速度的秘密!
标题
- 《Linux 零拷贝神技:splice 让数据传输 “坐火箭”》
- 《从 “手搬肩扛” 到 “传送带”:splice 实战指南》
简介
本文用 “工地传送带” 的趣味类比,通俗解析 Linux 系统调用 splice 的工作原理,通过两个完整案例(文件到终端、服务器发文件)演示其用法,详细说明编译运行步骤,帮助开发者轻松掌握这一高效数据传输工具。
关键词
#splice #Linux 零拷贝 #系统调用 #高效文件传输 #网络编程