操作系统学习笔记(四):零拷贝技术

写在前面

本文讲零拷贝(Zero-Copy)——把数据从磁盘发到网络,能少拷贝几次就少几次。这是 Nginx 静态文件飞快、Kafka 高吞吐的关键技术之一。


一、传统读写的开销

场景:服务器从磁盘读一个文件,通过网络发给客户端。

1.1 传统方式

1
2
3
4
// 传统 4 步
char buf[4096];
read(fd, buf, 4096);        // 磁盘 → 用户态
write(sock, buf, 4096);     // 用户态 → 网络

看似两行,实际发生了4 次数据拷贝 + 4 次上下文切换

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
                       内核空间                 用户空间
  ┌─────────┐        ┌──────────┐         ┌──────────┐
  │  磁盘    │──DMA──→│ 内核缓冲区 │──CPU拷贝─→│ 用户buf  │   read()
  └─────────┘        └──────────┘         └──────────┘
                          ↑                    │
                       (步骤1,2)           CPU拷贝
                     ┌──────────┐         ┌──────────┐
                     │ Socket   │←─CPU拷贝─│ 用户buf  │   write()
                     │ 缓冲区    │         └──────────┘
                     └─────┬────┘
                          DMA
                     ┌─────▼────┐
                     │  网卡     │
                     └──────────┘
                         (步骤3,4)

  4 次拷贝:
    1. 磁盘 → 内核缓冲区(DMA)
    2. 内核缓冲区 → 用户 buf(CPU)   ← 多余!用户根本没处理数据
    3. 用户 buf → Socket 缓冲区(CPU) ← 多余!
    4. Socket 缓冲区 → 网卡(DMA)

  4 次上下文切换:read(2次) + write(2次)
1
2
3
4
5
6
问题:
  数据从磁盘到网卡,根本不需要经过用户空间
  但传统 read+write 强制数据绕道用户态
  白白多了 2 次 CPU 拷贝 + 2 次上下文切换

  零拷贝的目标:消除这 2 次多余的 CPU 拷贝

二、mmap + write(减少一次拷贝)

mmap 把内核缓冲区和用户空间映射到同一块内存,省去内核→用户的拷贝。

1
2
3
char *p = mmap(NULL, size, PROT_READ, MAP_SHARED, fd, 0);  // 映射
write(sock, p, size);   // 直接从映射区写
munmap(p, size);
1
2
3
4
5
6
7
8
拷贝流程:
  磁盘 →(DMA)→ 内核缓冲区(= 用户映射区,共享)
  内核缓冲区 →(CPU)→ Socket 缓冲区 →(DMA)→ 网卡

  省了「内核缓冲区→用户buf」这次拷贝
  剩 3 次拷贝(2 DMA + 1 CPU),4 次上下文切换

  适合:用户需要读数据内容(如处理后再发)

三、sendfile(真正的零拷贝)

Linux 2.1 引入,专门为"文件→Socket"传输设计。数据全程不进用户空间

1
2
#include <sys/sendfile.h>
sendfile(sock, fd, &offset, size);   // 一步到位
1
2
3
4
5
6
7
8
拷贝流程(原始 sendfile):
  磁盘 →(DMA)→ 内核缓冲区 →(CPU)→ Socket 缓冲区 →(DMA)→ 网卡

  数据不进用户态!
  2 次拷贝(2 DMA)+ 1 次 CPU 拷贝
  2 次上下文切换(一次系统调用)

  对比传统:4次拷贝+4次切换 → 3次拷贝+2次切换

SG-DMA 优化(Linux 2.4+,终极零拷贝)

如果网卡支持 SG-DMA(Scatter-Gather DMA),连那 1 次 CPU 拷贝都省了:

1
2
3
4
5
6
7
拷贝流程(SG-DMA):
  磁盘 →(DMA)→ 内核缓冲区
  内核只把「缓冲区地址 + 长度」描述符发给 Socket 缓冲区
  网卡根据描述符直接从内核缓冲区 DMA → 网卡

  全程只有 2 次 DMA 拷贝,0 次 CPU 拷贝!
  这就是真正的"零拷贝"(CPU 零拷贝)
1
2
  Nginx 的 sendfile on; 就是开启这个
  静态文件传输极快的根本原因

四、splice(管道零拷贝)

sendfile 只能文件→Socket。splice 更通用,任意两个 fd 之间(通过管道)零拷贝。

1
2
3
4
int pipefd[2];
pipe(pipefd);
splice(fd, &offset, pipefd[1], NULL, size, SPLICE_F_MOVE);    // 文件 → 管道
splice(pipefd[0], NULL, sock, NULL, size, SPLICE_F_MOVE);      // 管道 → Socket
1
2
适合:两个非 Socket 的 fd 间传输(如文件→文件、管道中转)
本质:用内核管道缓冲区做中转,避免数据进用户态

五、对比总结

1
2
3
4
5
6
7
方式              CPU拷贝   DMA拷贝   上下文切换   数据进用户态
────────────────────────────────────────────────────────────────
传统 read+write    2         2          4          是
mmap + write       1         2          4          是(映射)
sendfile           1         2          2          否
sendfile + SG-DMA  0         2          2          否   ← 最优
splice             0         2          2          否

六、实际应用

6.1 Nginx

1
2
3
4
5
6
http {
    sendfile on;           # 开启 sendfile 零拷贝
    tcp_nopush on;         # 配合 sendfile,等数据攒够再发
}

# 静态文件传输直接走 sendfile,磁盘→网卡不进用户态

6.2 Kafka

1
2
3
4
5
6
7
Kafka 高吞吐的关键之一:消费者拉消息用 sendfile

  消息存在磁盘日志(顺序写,快)
  消费者拉取 → 直接 sendfile 从磁盘文件发到网络
  不经过 JVM 堆,无 GC 压力,无内存拷贝

  这让 Kafka 能轻松百万级 TPS

6.3 Java 的零拷贝 API

1
2
3
4
5
Java NIO:
  FileChannel.transferTo()  → 底层就是 sendfile
  MappedByteBuffer          → 底层是 mmap

  Kafka、Netty 都用这些实现零拷贝

七、.NET 的零拷贝

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
.NET 用 Stream 的 CopyToAsync 在内部尽量优化
  但不是直接对应 sendfile

ASP.NET Core 的静态文件中间件:
  用一些平台优化(如 Linux 下用 sendfile 语义)
  + Pipelines 减少分配

Span<T> / Memory<T>:
  不是网络零拷贝,但提供了"零拷贝切片"内存视图
  避免切片字符串/数组时的拷贝

八、小结

  • 传统读写:4 次拷贝(2 CPU + 2 DMA)+ 4 次切换,数据多余绕道用户态
  • mmap:内核/用户共享内存,省 1 次 CPU 拷贝(适合需要读内容)
  • sendfile:文件→Socket 不进用户态,省拷贝 + 切换
  • sendfile + SG-DMA:CPU 零拷贝,只剩 2 次 DMA(最优)
  • splice:任意两 fd 间零拷贝(通过管道)
  • 应用:Nginx sendfile、Kafka transferTo、Java FileChannel.transferTo

零拷贝 + I/O 多路复用 + Reactor = 高性能网络服务器三件套。下一篇讲内存管理(虚拟内存、页表、malloc)。