C++_面试15_零拷贝

📌 面试题 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 半零拷贝 大文件读写
相关推荐
Espresso Macchiato2 小时前
Leetcode 3748. Count Stable Subarrays
算法·leetcode·职场和发展·leetcode hard·leetcode 3748·leetcode周赛476·区间求和
AA陈超2 小时前
ASC学习笔记0007:用于与GameplayAbilities系统交互的核心ActorComponent
c++·笔记·学习·ue5·虚幻引擎
大袁同学2 小时前
【哈希hash】:程序的“魔法索引”,实现数据瞬移
数据结构·c++·算法·哈希算法·散列表
天真小巫2 小时前
2025.11.17总结
职场和发展
沐怡旸2 小时前
【穿越Effective C++】条款21:必须返回对象时,别妄想返回其reference——对象返回的语义与效率平衡
c++·面试
2501_941112612 小时前
C++与Docker集成开发
开发语言·c++·算法
智者知已应修善业3 小时前
【51单片机:两边向中间流水:即两边先点亮然后熄灭,次边的点亮再熄灭,直到最中间的两个点亮再熄灭,然后重复动作。】2023-3-4
c语言·c++·经验分享·笔记·嵌入式硬件·算法·51单片机
一叶飘零_sweeeet3 小时前
2025 年 Redis 面试天花板
redis·缓存·面试
米兰小铁匠173 小时前
js深入之从原型到原型链
javascript·面试