📌 面试题 1:什么是零拷贝?
标准答案:
零拷贝(Zero-Copy)是一种减少或消除 CPU 内存复制的技术,使数据在 用户态与内核态之间不发生拷贝 。
数据只在内核内部移动,或者直接经 DMA 发送到 NIC,大幅减少 CPU 开销与上下文切换。
📌 面试题 2:Linux 上 Zero-Copy 是哪些函数实现的?
标准答案:
Linux 的 Zero-Copy 由系统调用提供:
-
sendfile()------ 文件直接发送到 socket -
splice()------ 内核 fd → fd 之间传输 -
tee()------ 管道复制但不拷贝内存 -
mmap()------ 文件映射到用户空间(读零拷贝,但写/发送不完全 zero)
这些函数避免了传统 read/write 产生的两次拷贝和两次切换。
Linux 下 Zero-Copy 的核心函数(系统调用)
#include <sys/sendfile.h>
sendfile(out_fd, in_fd, offset, count);
用途:
从文件直接发送到 socket,不经过用户态缓冲区。
数据流程(零拷贝):
Disk → Kernel Buffer → Socket Buffer → NIC
(无用户态拷贝)
2. mmap() + write() ------ 让文件直接映射到用户态
void* p = mmap(NULL, size, PROT_READ, MAP_PRIVATE, fd, 0);
用途:
让用户空间访问文件内容,不需要 read() 拷贝。
流程:
Disk → Kernel Page Cache
User read buffer 只是指针访问内核同一块内存
注意:
mmap + write 并不是严格零拷贝,还涉及一次 NIC 发送拷贝。
2. mmap() + write() ------ 让文件直接映射到用户态
void* p = mmap(NULL, size, PROT_READ, MAP_PRIVATE, fd, 0);
用途:
让用户空间访问文件内容,不需要 read() 拷贝。
Disk → Kernel Page Cache
User read buffer 只是指针访问内核同一块内存
注意:
mmap + write 并不是严格零拷贝,还涉及一次 NIC 发送拷贝。
3. splice() ------ 内核缓冲区之间管道传输(零拷贝)
splice(fd_in, NULL, fd_out, NULL, len, SPLICE_F_MORE | SPLICE_F_MOVE);
用途:
在 内核文件描述符之间 直接搬运数据,不经过用户态。
文件 → 管道 → socket
全过程不进入用户态。
4. tee() ------ 拷贝管道数据但不复制内容(零拷贝)
tee(fd_in, fd_out, len, SPLICE_F_NONBLOCK);
用途:
复制管道数据流,但不拷贝实际内容(引用计数 + page 共享)
🔥 Zero-Copy 函数组合(Nginx/Tengine 用法)
例如:
sendfile + splice + tee 常被用来实现高性能 Web 服务器。
📌 面试题 3:sendfile() 与传统 read/write 的区别?
传统方式:
文件 → 内核 → 用户 → 内核 → NIC(4 次拷贝)
sendfile:
文件 → 内核 → NIC(1 次 DMA,0 次用户拷贝)
优势:
tee
📌 面试题 6:std::move 是 Zero-copy 吗?
-
CPU 使用率更低
-
内存带宽占用更少
-
更少的系统调用和上下文切换
-
大幅提升网络 IO 性能
📌 面试题 4:mmap 是不是零拷贝?
标准答案(面试官爱听):
mmap 是 "内存映射零拷贝" ,只减少了文件读的拷贝(无需 read() 将数据拷贝到用户态)。
但如果数据需要发送到 socket,仍然需要经过:
-
Kernel Buffer → Socket Buffer → NIC
-
因此:
✔ mmap 是用于 "文件读取" 的零拷贝
✘ 不是网络发送端的完全零拷贝
-
📌 面试题 5:splice / tee 适合什么场景?
splice
-
管道模式
-
TCP 代理、反向代理
-
如多播、日志复制
-
复制管道数据但不复制内存内容
-
共享相同的 page cache
-
文件 → socket,不经过用户态
标准答案:
不是。
std::move 是编译器语义,用来把对象转为右值引用。
它减少用户态深拷贝,但与 OS 零拷贝完全无关。
Zero-copy 指的是:
🔥 用户态 ↔ 内核态之间不发生复制
完全不同层面的概念
📌 面试题 7:如何画 Zero-Copy 的数据流(终极版)
传统 read + write
read()
Disk → Kernel → User
write()
User → Kernel → NIC
总共 4 次拷贝,2 次上下文切换
sendfile()
DMA: Disk → Kernel
Zero-copy: Kernel → Socket
DMA: Socket → NIC
总共 0 次用户态拷贝,仅 1 次 DMA
Eg:功能:客户端连接后,服务器使用 sendfile() 零拷贝将一个文件直接传给客户端
特点:
✔ epoll + 非阻塞 IO
✔ sendfile 完全零拷贝
✔ 无用户态 buffer
✔ 面试标准写法
#include <sys/types.h>
#include <sys/socket.h>
#include <sys/sendfile.h>
#include <sys/epoll.h>
#include <netinet/in.h>
#include <fcntl.h>
#include <unistd.h>
#include <cstring>
#include <iostream>
#define PORT 8888
#define MAX_EVENTS 1024
int main() {
int listen_fd = socket(AF_INET, SOCK_STREAM, 0);
// 设置非阻塞
fcntl(listen_fd, F_SETFL, O_NONBLOCK);
sockaddr_in addr{};
addr.sin_family = AF_INET;
addr.sin_addr.s_addr = INADDR_ANY;
addr.sin_port = htons(PORT);
bind(listen_fd, (sockaddr*)&addr, sizeof(addr));
listen(listen_fd, 128);
// epoll 创建
int epfd = epoll_create1(0);
epoll_event ev{}, events[MAX_EVENTS];
ev.events = EPOLLIN;
ev.data.fd = listen_fd;
epoll_ctl(epfd, EPOLL_CTL_ADD, listen_fd, &ev);
std::cout << "Zero-copy server running on port " << PORT << std::endl;
const char* filename = "test.txt"; // 要发送的文件
while (true) {
int n = epoll_wait(epfd, events, MAX_EVENTS, -1);
for (int i = 0; i < n; ++i) {
int fd = events[i].data.fd;
// 处理新连接
if (fd == listen_fd) {
int client_fd = accept(listen_fd, NULL, NULL);
fcntl(client_fd, F_SETFL, O_NONBLOCK);
ev.events = EPOLLIN;
ev.data.fd = client_fd;
epoll_ctl(epfd, EPOLL_CTL_ADD, client_fd, &ev);
std::cout << "Client connected: " << client_fd << std::endl;
}
else {
// 处理客户端请求(此处简化为:只要读到数据就发送文件)
char buf[1024];
int ret = read(fd, buf, sizeof(buf));
if (ret <= 0) {
close(fd);
epoll_ctl(epfd, EPOLL_CTL_DEL, fd, nullptr);
continue;
}
// 打开文件
int file_fd = open(filename, O_RDONLY);
if (file_fd < 0) continue;
// 获取文件大小
off_t offset = 0;
off_t filesize = lseek(file_fd, 0, SEEK_END);
lseek(file_fd, 0, SEEK_SET);
// 🔥 核心:sendfile(零拷贝发送)
ssize_t sent = sendfile(fd, file_fd, &offset, filesize);
std::cout << "Sent " << sent << " bytes by zero-copy\n";
close(file_fd);
}
}
}
return 0;
}
| 技术 | 用户态拷贝 | 内核态拷贝 | 是否零拷贝 | 用途 |
|---|---|---|---|---|
| mmap + write | ❌ 有一次 | ✔ 有一次 | ❌ 半零拷贝 | 随机读/写 |
| sendfile | ❌ 0 次 | ✔ 1 次(内核内部) | ✔ 完全零拷贝 | 大文件发送 |
🧠 Windows Zero-Copy API 总表(最简答版)
| API | 类型 | 是否零拷贝 | 常用场景 |
|---|---|---|---|
| TransmitFile | ✔ 文件发送 | ✔ 完全零拷贝 | 大文件、静态资源发送 |
| TransmitPackets | ✔ 混合数据 | ✔ 零拷贝或部分零拷贝 | HTTP header + body |
| AcceptEx | ✔ 接受连接 | ✔ 零拷贝 | 高级 TCP 服务器/IOCP |
| WSASend/WSARecv | 通用 IO | ⚠ 仅 Buffer 注册时 | 高吞吐 IOCP |
| ReadFileScatter / WriteFileGather | 分散/聚集 IO | 半零拷贝 | 大文件读写 |