零拷贝技术

一句话概括:零拷贝技术让数据在 磁盘 → 内核缓冲区 → 网卡 之间直接传输,避免用户态与内核态之间的冗余数据拷贝,核心是消除 CPU 参与的数据搬移。

传统 IO 路径(4 次拷贝,2 次 CPU 参与)

1
2
磁盘 → [DMA] → Page Cache (内核)[CPU] → 用户 Buffer → [CPU] → Socket Buffer (内核)[DMA] → NIC
			  ①                        ②                     ③                         ④
  • ① DMA 读磁盘到 page cache
  • ② CPU 把 page cache 拷到用户态 buffer(read)
  • ③ CPU 把用户态 buffer 拷到 socket buffer(send)
  • ④ DMA 从 socket buffer 发送到网卡

零拷贝的目标:去掉 ② 和 ③ 这两步 CPU 拷贝。


1. sendfile(最常用,静态文件服务)

1
2
sendfile(out_fd, in_fd, &offset, count);
// out_fd= socket, in_fd= 文件 fd

路径:

1
2
磁盘 → [DMA] → Page Cache → [SG-DMA 直接描述] → NIC
			  ①              ② (无 CPU 数据拷贝)

底层: Page Cache 中的页面直接通过 scatter-gather DMA 发送到网卡。内核只需要把 page 指针链入 skb 的 frag 数组,DMA 引擎直接读取。CPU 不碰数据,只传元数据(指针、长度)。

限制:

  • 数据必须从文件到 socket(不支持 socket→socket 或文件→文件)
  • out_fd 必须支持 DMA 发送(通常只有 TCP socket)
  • in_fd 必须是支持 splice_read 的文件(普通文件、块设备)

2. splice(通用 pipe 搬运,任意 fd 之间)

1
2
3
4
int pipefd[2];
pipe(pipefd);
splice(fd_in, off_in, pipefd[1], NULL, len, SPLICE_F_MOVE);
splice(pipefd[0], NULL, fd_out, off_out, len, SPLICE_F_MOVE);

路径:

1
文件 → pipe buffer (只传指针,不拷数据) → socket

底层: splice 将 fd_in 的 page 引用直接挂到 pipe 的 struct pipe_buffer 上,再从 pipe 挂到 fd_out 的 skb 上。数据始终在内核 page cache 里不动,只移动了引用计数。

sendfile vs splice:

  • sendfile 是 splice 的特化(splice 内部实现就是 pipe + 两个 splice 调用)
  • splice 可以连接任意两个 fd(文件 → socket、socket → socket、pipe → 任何),而 sendfile 只支持文件 → socket

3. mmap + write(用户态直接操作 page cache)

1
2
void *addr = mmap(NULL, len, PROT_READ, MAP_PRIVATE, fd, 0);
write(sockfd, addr, len);

路径:

1
2
3
4
磁盘 → [DMA] → Page Cache
(mmap 共用同一物理页,不拷贝)
			   用户地址空间
			   ↓ write 时内核直接从 page cache 构建 skb

底层: mmap 将 page cache 的物理页直接映射到用户地址空间,用户 write 时内核直接从映射页构造 skb->frags,无需 copy_from_user

问题:

  • 多线程并发写同一个 mmap 区域需加锁
  • 文件长度变化、截断时的 SIGBUS 处理复杂
  • 脏页回写时机不可控

4. SO_ZEROCOPY(Linux 4.14+,用户态直接发零拷贝)

1
2
3
int one = 1;
setsockopt(sockfd, SOL_SOCKET, SO_ZEROCOPY, &one, sizeof(one));
send(sockfd, buf, len, MSG_ZEROCOPY);

路径:

1
2
3
4
用户 Buffer → [DMA 直接读取] → NIC
			  ① 内核锁定用户页,记录其物理地址
			  ② DMA 直接从用户页读取数据发送
			  ③ 发送完成后通知用户页可释放

底层:

  1. 内核调用 pin_user_pages 固定用户缓冲区物理页(防止被 swap 出去)
  2. 将用户页地址填入 skb frags,DMA 引擎直接读取用户态内存
  3. 发送完成通过 recvmsg 返回 SO_EE_ORIGIN_ZEROCOPY 通知用户释放

去掉了一次内核态到用户态的拷贝(copy_from_user),但需要 pin/unpin 开销。

适用场景: 大块数据发送(>10KB),小包反而更慢(pin/unpin 开销 > 直接拷贝)


5. io_uring + 注册缓冲区(Linux 5.1+)

1
2
3
4
5
6
7
8
// 注册固定缓冲区(一次 pin,多次复用)
struct iovec iov = { .iov_base = buf, .iov_len = len };
io_uring_register_buf_ring(ring, &iov, 1);

// 提交 IO,直接引用已 pin 的 buffer
struct io_uring_sqe *sqe = io_uring_get_sqe(ring);
io_uring_prep_read_fixed(sqe, fd, buf, len, 0, buf_index);
io_uring_prep_write_fixed(sqe, fd, buf, len, 0, buf_index);

路径:

1
磁盘 → [DMA] → 用户注册 Buffer(直接)

底层: io_uring_register_buf_ring 一次 pin_user_pages 后返回 buffer index,后续所有 IO 请求直接通过 index 引用 buffer,无需再 pin/unpin。避免了每次 IO 的页锁定开销,同时去掉 CPU 拷贝。


6. RDMA(InfiniBand / RoCE / iWARP,硬件级零拷贝)

1
2
3
4
5
6
7
8
// 注册 MR (Memory Region)
struct ibv_mr *mr = ibv_reg_mr(pd, buf, len, IBV_ACCESS_LOCAL_WRITE | IBV_ACCESS_REMOTE_WRITE);

// 远端直接写本端内存(不需要本端 CPU 参与)
struct ibv_sge sge = { .addr = (uintptr_t)buf, .length = len, .lkey = mr->lkey };
struct ibv_send_wr wr = { .wr_id = 1, .sg_list = &sge, .num_sge = 1,
                          .opcode = IBV_WR_RDMA_WRITE };
ibv_post_send(qp, &wr, &bad_wr);

路径:

1
2
远端内存 → [网卡硬件 DMA] → 本端用户内存
          全程不需要本端 CPU 参与

底层: 网卡通过 ibv_reg_mr 获取用户虚拟地址 → 物理页的映射表,远端网卡直接写入本端物理页,旁路内核,零 CPU。本端应用直接看到数据,不经过内核 socket buffer。

代价:

  • 需要 RDMA 硬件(InfiniBand 交换机/网卡或支持 RoCE 的以太网卡)
  • 内存注册(ibv_reg_mr)开销大(页表遍历 + pin pages),需池化复用

7. DPDK(用户态驱动,完全绕过内核)

1
2
3
应用 → DPDK PMD → NIC
  全程用户态操作

网卡直接把数据 DMA 到用户态预先分配的 hugepage 里。没有系统调用,没有上下文切换,没有内核数据结构。

但代价是应用层必须实现 TCP 协议栈(如 mTCP、F-Stack)。


各场景推荐

场景推荐技术
Web 服务器静态文件sendfile
反向代理/网关转发splice
大块数据发送(视频服务)SO_ZEROCOPY
高性能存储 + 网络io_uring + registered buffers
超低延迟(< 10us)RDMA
纯包转发(> 100Gbps)DPDK

注意: 零拷贝不是银弹。小包场景下,CPU 拷贝的延迟远小于 pin/unpin 或上下文切换的开销,传统路径反而更快。Nginx 对 < 16KB 的文件直接用 read + write,超过才用 sendfile——这也是一种工程权衡。

Licensed under CC BY-NC-SA 4.0
使用 Hugo 构建
主题 StackJimmy 设计