免费获取学习方案
ARTICLE DETAIL

资讯详情

深耕编程基础知识与建站技术分享的一线实战洞察。

零拷贝技术详解

零拷贝技术详解 文章目录零拷贝技术详解从传统 I/O 到 sendfile SG-DMA1. 前置知识用户态与内核态、DMA 与上下文切换1.1 用户态User Mode与内核态Kernel Mode1.2 DMADirect Memory Access直接内存访问1.3 上下文切换1.4 关键缓冲区位置2. 零拷贝技术演进总览各方案速览对比3. 传统 I/Oread write3.1 数据流过程8 个步骤3.2 传统 I/O 的核心问题3.3 代价总结4. mmap write4.1 数据流过程4.2 代价总结5. sendfile早期版本Linux 2.15.1 数据流过程5.2 代价总结6. sendfile SG-DMALinux 2.4真正的零拷贝6.1 核心突破6.2 数据流过程6.3 SG-DMA 的分散-聚集原理6.4 代价总结7. spliceLinux 2.6.177.1 工作原理7.2 splice 与 sendfile 的对比7.3 splice 示例8. 五种方案综合对比9. 实际应用场景9.1 Kafka 的零拷贝9.2 Nginx 的 sendfile9.3 Tomcat / Netty 中的 FileRegion9.4 编程语言中的零拷贝9.5 适用条件10. 重要注意事项总结11.1 一句话总结11.2 关键数字记忆零拷贝技术详解从传统 I/O 到 sendfile SG-DMA零拷贝Zero-Copy的目标在数据传输过程中消除 CPU 参与的数据拷贝并且减少上下文切换让 CPU 从搬运工变成指挥官。1. 前置知识用户态与内核态、DMA 与上下文切换1.1 用户态User Mode与内核态Kernel Mode理解用户态和内核态是掌握零拷贝的基础。CPU 运行时有两种不同的权限级别目的是保护系统的稳定性和安全性。把操作系统想象成一个高度戒备的公司维度用户态User Mode内核态Kernel Mode身份普通员工管理员/保安主管权限受限只能访问自己的内存空间至高无上可访问所有内存和硬件可操作应用程序代码你的程序、浏览器等操作系统内核代码管理硬件和核心资源硬件访问❌ 禁止直接访问✅ 允许直接访问稳定性崩溃只影响自身进程崩溃导致整个系统蓝屏/Kernel Panic进入方式程序启动默认状态发生中断/异常/执行系统调用关键交互系统调用用户态程序没有权限直接操作硬件当需要读取文件、发送网络数据时必须通过系统调用请求内核代为执行用户态程序 → 调用 read() / write() / sendfile() → CPU 切换到内核态 → 内核执行 → CPU 切回用户态每次切换都要保存现场、切换权限、恢复现场这就是**上下文切换Context Switch**的开销来源。1.2 DMADirect Memory Access直接内存访问DMA 是一种允许外设磁盘、网卡直接访问系统内存的机制不需要 CPU 逐字节搬运数据。没有 DMA磁盘 → CPU → 内存 CPU 做搬运工累 有 DMA 磁盘 → 内存 DMA 控制器做搬运工CPU 解放为什么零拷贝能谈 DMA现代系统中磁盘 ↔ 内存、内存 ↔ 网卡的数据搬运都由 DMA 完成CPU 只需要发指令。零拷贝要消除的是内核缓冲区 → 用户缓冲区 → socket 缓冲区之间由 CPU 执行的拷贝。1.3 上下文切换上下文切换的代价用户态与内核态之间的切换成本约1~3 微秒每次频繁切换是性能杀手。每次切换包含保存用户态寄存器/程序计数器切换到内核态执行系统调用恢复用户态上下文1.4 关键缓冲区位置┌──────────────────────────────────┐ │ 用户空间 │ │ 应用程序缓冲区app buffer │ ├──────────────────────────────────┤ │ 内核空间 │ │ ┌────────────────────────────┐ │ │ │ Page Cache页缓存 │ │ │ │ 磁盘数据 → 内核缓冲区 │ │ │ └────────────────────────────┘ │ │ ┌────────────────────────────┐ │ │ │ Socket Buffersocket 缓冲区│ │ │ │ 网络发送队列 │ │ │ └────────────────────────────┘ │ ├──────────────────────────────────┤ │ 硬件层 │ │ 磁盘 ←→ DMA ←→ 内存 │ │ 网卡 ←→ DMA ←→ 内存 │ └──────────────────────────────────┘2. 零拷贝技术演进总览Linux 早期 │ ▼ read() write() 2 次 CPU 拷贝4 次上下文切换 │ │ Linux 发展发现瓶颈 ▼ mmap() write() 1 次 CPU 拷贝4 次上下文切换 │ │ Linux 2.1 ▼ sendfile()早期版本 1 次 CPU 拷贝2 次上下文切换 │ │ Linux 2.4网卡支持 SG-DMA ▼ sendfile() SG-DMA 0 次 CPU 拷贝2 次上下文切换 ★ 真正的零拷贝 │ │ Linux 2.6.17 ▼ splice() / tee() 0 次 CPU 拷贝更通用任意 fd 间传输 │ │ 持续优化 ▼ io_uring 零拷贝 现代高性能方案各方案速览对比方案上下文切换CPU 拷贝DMA 拷贝系统调用用户缓冲区传统 readwrite4222需要mmapwrite4122需要mmap 映射sendfile早期2121不需要sendfile SG-DMA2021不需要splice取决于用法022不需要“零拷贝指的是 0 次 CPU 参与的数据拷贝。DMA 拷贝不算拷贝”因为 CPU 没有参与搬运数据。零拷贝只消除 CPU 拷贝不消除 DMA 拷贝。下面逐节展开每个方案。3. 传统 I/Oread write// 伪代码从磁盘读取文件发送到网络fdopen(file.txt,O_RDONLY);sockfdsocket(AF_INET,SOCK_STREAM,0);charbuf[4096];read(fd,buf,4096);// 磁盘 → 内核缓冲区 → 用户缓冲区write(sockfd,buf,4096);// 用户缓冲区 → socket 缓冲区 → 网卡3.1 数据流过程8 个步骤步骤1read() 系统调用 用户态 → 内核态上下文切换1 步骤2DMA 拷贝磁盘 → Page Cache 拷贝1 [DMA] 步骤3CPU 拷贝Page Cache → 用户缓冲区buf 拷贝2 [CPU] 步骤4返回用户态 上下文切换2 步骤5write() 系统调用 用户态 → 内核态上下文切换3 步骤6CPU 拷贝用户缓冲区 → socket 缓冲区 拷贝3 [CPU] 步骤7DMA 拷贝socket 缓冲区 → 网卡 拷贝4 [DMA] 步骤8返回用户态 上下文切换43.2 传统 I/O 的核心问题数据路径 磁盘 → [DMA] → Page Cache → [CPU] → 用户缓冲区 → [CPU] → Socket Buffer → [DMA] → 网卡 ├──── DMA 拷贝1 ────┤├── CPU拷贝1 ──┤├─── CPU拷贝2 ───┤├── DMA 拷贝2 ────┤为什么说这是浪费如果应用程序只是把文件从磁盘读到内存再发到网络如静态文件服务器数据从未被修改却白白经历了 2 次 CPU 拷贝和 4 次上下文切换。用户缓冲区buf只起到了中转站的作用。3.3 代价总结指标数量上下文切换4 次DMA 拷贝2 次CPU 拷贝2 次系统调用2 次 (read write)4. mmap writemmap将内核缓冲区Page Cache直接映射到用户地址空间省掉从内核到用户的 CPU 拷贝。// 伪代码fdopen(file.txt,O_RDONLY);// mmap把文件映射到用户空间本质是共享 Page Cachechar*bufmmap(NULL,size,PROT_READ,MAP_SHARED,fd,0);sockfdsocket(AF_INET,SOCK_STREAM,0);write(sockfd,buf,size);// 直接从 mmap 区域写入 socket4.1 数据流过程步骤1mmap() 系统调用 — 建立映射不拷贝数据 步骤2read 触发缺页中断DMA 拷贝磁盘 → Page Cache 拷贝1 [DMA] mmap 映射后用户空间直接能看到 Page Cache 的内容页表映射非复制 步骤3write() 系统调用 用户态 → 内核态上下文切换3 步骤4CPU 拷贝Page Cache → socket 缓冲区 拷贝2 [CPU] ← 注意这里还是需要一次 CPU 拷贝 步骤5DMA 拷贝socket 缓冲区 → 网卡 拷贝3 [DMA] 步骤6返回用户态 上下文切换44.2 代价总结指标传统 readwritemmapwrite上下文切换4 次4 次没减少CPU 拷贝2 次1 次少了一次DMA 拷贝2 次2 次系统调用2 次2 次改进点mmap 省了一次 CPU 拷贝内核→用户。代价上下文切换仍是 4 次且有内存映射的开销建立/解除映射、管理页表、缺页中断。隐患如果另一个进程在 write 期间截断文件write 会收到 SIGBUS 信号导致进程崩溃。5. sendfile早期版本Linux 2.1Linux 2.1 引入sendfile将两次系统调用合并为一次减少上下文切换。#includesys/sendfile.h// 一行搞定从 in_fd 读取直接发送到 out_fd要求 out_fd 是 socketssize_tsendfile(intout_fd,intin_fd,off_t*offset,size_tcount);// 使用示例intfdopen(file.txt,O_RDONLY);intsockfdsocket(AF_INET,SOCK_STREAM,0);sendfile(sockfd,fd,NULL,file_size);5.1 数据流过程步骤1sendfile() 系统调用 用户态 → 内核态上下文切换1 步骤2DMA 拷贝磁盘 → Page Cache 拷贝1 [DMA] 步骤3CPU 拷贝Page Cache → socket 缓冲区 拷贝2 [CPU] 步骤4DMA 拷贝socket 缓冲区 → 网卡 拷贝3 [DMA] 步骤5返回用户态 上下文切换25.2 代价总结指标mmapwritesendfile早期上下文切换4 次2 次减半CPU 拷贝1 次1 次仍是 1 次DMA 拷贝2 次2 次系统调用2 次1 次用户缓冲区需要不需要数据处理可在用户态修改不能在中间修改数据sendfile 减少的是什么上下文切换4→2系统调用次数2→1不需要用户缓冲区省内存sendfile 的局限性out_fd必须是 socket不能是普通文件in_fd必须是支持mmap操作的文件通常是磁盘文件不能是 socket数据处理不经过用户态无法在传输前修改数据6. sendfile SG-DMALinux 2.4真正的零拷贝Linux 2.4 改进了sendfile引入 SG-DMAScatter-Gather DMA分散-聚集 DMA。6.1 核心突破sendfile 早期版本仍有 1 次 CPU 拷贝Page Cache → socket buffer原因是socket 缓冲区需要存放连续的完整数据包。SG-DMA 通过两个机制消除这次 CPU 拷贝socket 缓冲区只存描述符buffer descriptor而不是数据本身SG-DMA 从 Page Cache 直接 gather 数据配合描述符组装成完整数据包发送6.2 数据流过程步骤1sendfile() 系统调用 用户态 → 内核态上下文切换1 步骤2DMA 拷贝磁盘 → Page Cache 拷贝1 [DMA] 步骤3CPU 向 socket 缓冲区写入数据描述符不是数据 - 描述符记录数据在 Page Cache 的地址 长度 - 这只是几十字节的元数据不算真正的数据拷贝 步骤4SG-DMA 直接从 Page Cache gather 数据组装成数据包发送到网卡 拷贝2 [DMA] ← SG-DMA 支持从多个不连续内存位置聚集数据 步骤5返回用户态 上下文切换26.3 SG-DMA 的分散-聚集原理传统 DMA需要一个连续的物理内存区域 ┌──────────────────────┐ │ 连续数据块 │ └──────────────────────┘ SG-DMA 可以从多个不连续的内存位置聚集数据组成一个完整的数据包 ┌──────┐ ┌────────┐ ┌───────────┐ │块1 │ │块2 │ │块3 │ ← 分散在不同位置 └──┬───┘ └───┬────┘ └─────┬─────┘ │ │ │ └──────────┼────────────┘ ▼ ┌────────────────────┐ │ 完整网络数据包 │ ← SG-DMA 自动聚集 └────────────────────┘6.4 代价总结指标sendfile早期sendfile SG-DMA上下文切换2 次2 次CPU 拷贝1 次0 次 ★ 真正的零拷贝DMA 拷贝2 次2 次系统调用1 次1 次7. spliceLinux 2.6.17Linux 2.6.17 引入splice在两个文件描述符之间建立管道传输数据同样是零拷贝。#define_GNU_SOURCE#includefcntl.h// 在两个 fd 之间零拷贝传输数据必须有一个是管道ssize_tsplice(intfd_in,loff_t*off_in,intfd_out,loff_t*off_out,size_tlen,unsignedintflags);7.1 工作原理splice 的原理是在内核空间建立两个文件描述符之间的管道桥梁 fd_in ──→ Pipe ──→ fd_out 数据从 fd_in 的 Page Cache 直接流向 fd_out 的缓冲区 不经过用户空间不经过 CPU 拷贝。7.2 splice 与 sendfile 的对比特性sendfilesplice源 fd必须支持 mmap文件任意 fd目标 fd必须是 socket任意 fd中间媒介无必须有一个是管道pipe适用性文件→网络任意两个 fd 间的数据传输零拷贝是SG-DMA版是使用复杂度简单一个调用较高可能多次调用7.3 splice 示例// 使用 splice 实现文件到 socket 的零拷贝intfdopen(file.txt,O_RDONLY);intsockfdsocket(AF_INET,SOCK_STREAM,0);intpipefd[2];pipe(pipefd);// 创建管道// splice: 文件 → 管道零拷贝splice(fd,NULL,pipefd[1],NULL,file_size,SPLICE_F_MOVE);// splice: 管道 → socket零拷贝splice(pipefd[0],NULL,sockfd,NULL,file_size,SPLICE_F_MOVE);8. 五种方案综合对比CPU 拷贝次数 传统 readwrite ████████████████████ 2 mmap write ██████████ 1 sendfile早期 ██████████ 1 sendfile SG-DMA ░░░░░░░░░░ 0 ← 零拷贝 splice ░░░░░░░░░░ 0 ← 零拷贝 上下文切换次数 传统 readwrite ████████████████████ 4 mmap write ████████████████████ 4 sendfile早期 ██████████ 2 sendfile SG-DMA ██████████ 2 splice (取决于具体使用方式)方案上下文切换CPU 拷贝DMA 拷贝系统调用用户缓冲区传统 readwrite4222需要mmapwrite4122需要mmapsendfile早期2121不需要sendfileSG-DMA2021不需要splice2~3022不需要9. 实际应用场景9.1 Kafka 的零拷贝Kafka 使用sendfileJava 的FileChannel.transferTo()将磁盘上的日志文件直接发送给消费者Producer → Kafka Broker磁盘→ Consumer Broker 端直接把磁盘上的消息文件通过 sendfile 发给网卡 数据不需要经过 Broker 的用户态进程。9.2 Nginx 的 sendfile# nginx.conf sendfile on; # 开启 sendfile tcp_nopush on; # 配合使用累积数据包一起发送当 Nginx 发送静态文件时直接调用sendfile数据从磁盘 Page Cache 直达网卡。9.3 Tomcat / Netty 中的 FileRegionNetty 使用FileRegion封装FileChannel.transferTo()底层调用sendfile实现零拷贝文件传输。9.4 编程语言中的零拷贝语言API底层机制JavaFileChannel.transferTo()/transferFrom()通常使用sendfile()或等效机制Goio.Copy()源是*os.File目标是net.TCPConn时尝试使用sendfile()C/C直接调用sendfile()系统调用直接9.5 适用条件sendfile 零拷贝生效的前提条件网卡支持 SG-DMA现代网卡基本都支持不需要在传输过程中修改数据数据不经过用户态源是文件目标是 socket文件数据在 Page Cache 中如果不在需要先从磁盘读入10. 重要注意事项并非所有场景都适用如果应用程序确实需要处理数据内容如解密、压缩、修改数据还是需要拷贝到用户空间零拷贝无法完全避免拷贝。硬件依赖真正的零拷贝DMA Scatter/Gather需要网卡等硬件支持。内核版本不同内核版本支持的零拷贝特性如sendfile的范围、splice的可用性可能不同。小文件开销对于非常小的文件零拷贝技术尤其是mmap的固定开销系统调用、内存映射建立可能抵消其收益甚至不如传统方式快。需要根据实际场景测试。总结11.1 一句话总结零拷贝是指消除 CPU 在内存缓冲区之间搬运数据的过程。Linux 通过 sendfile SG-DMA 实现了真正的零拷贝DMA 将磁盘数据加载到 Page CacheSG-DMA 直接从 Page Cache 将数据发给网卡全程 CPU 不参与数据搬运。11.2 关键数字记忆方案上下文切换CPU拷贝传统 readwrite42mmapwrite41sendfile早期21sendfileSG-DMA20splice2~30
返回列表