
在介绍零拷贝之前我们先看看传统的 Java 网络 IO 编程是怎样的。下面代码展示了一个典型的 Java 网络程序。File file new File(index.jsp); RandomAccessFile rdf new RandomAccessFile(file, rw); byte[] arr new byte[(int) file.length()]; rdf.read(arr); Socket socket new ServerSocket(8080).accept(); socket.getOutputStream().write(arr);程序中调用 RandomAccessFile 的 read 方法将 index.jsp 的内容读取到字节数组中。然后调用 write 方法将字节数组中的数据写入到 Socket 对应的输出流中发送给客户端。那么 Java 应用程序中的 read、write 方法对应到 OS 底层是怎样的呢。下图展示了这个过程。图中上半部分记录了用户态和内核态的上下文切换。下半部分展示了数据的复制过程。上述 Java 代码对应的操作系统底层步骤read write:read 方法触发操作系统从用户态到切换到内核态。同时通过 DMA 的方式从磁盘读取文件到内核缓冲区。DMADirect Memory Access是 l/O 设备与主存之间由硬件组成的直接数据通路。即不需要 CPU 拷贝数据到内存而是直接由 DMA 引擎传输数据到内存。紧接着发生第二次数据拷贝即从内核缓冲区拷贝到用户缓冲区同时发生一次内核态到用户态的上下文切换。调用 write 方法时触发第三次数据拷贝即从用户缓冲区拷贝到 Socket 缓冲区。同时发生一次用户态到内核态的上下文切换。最后数据从 Socket 缓冲区异步拷贝到网络协议引擎这一步采用的是 DMA 方式。同时没有发生上下文切换。write 方法返回时触发了最后一次内核态到用户态的切换。由此可见复制的操作太频繁共有 2 次 DMA 拷贝、2 次 CPU 拷贝、4 次上下文切换。能否优化呢DMA 拷贝2 次磁盘 → 内核页缓存socket 缓冲区 → 网卡CPU 拷贝2 次内核页缓存 → 用户缓冲区用户缓冲区 → socket 内核缓冲区上下文切换4 次read用户→内核、内核→用户write用户→内核、内核→用户这就要介绍称之为零拷贝的技术。首先声明零拷贝技术依赖底层 OS 内核提供的支持。Linux 中提供的这类支持有 mmap()sendfile() 以及 splice() 系统调用。说白了就是减少数据在操作系统内核的缓冲区和用户应用程序地址空间的缓冲区之间进行拷贝即减少CPU拷贝。mmap writemmap 通过内存映射将文件通过 DMA 的方式映射到内核缓冲区。操作系统会把这段内核缓冲区与应用程序用户空间共享。这样在进行网络传输时就能减少内核空间到用户空间的拷贝次数。此时写出数据时只要从内核缓冲区拷贝到 Socket 缓冲区即可。可见减少了一次 内核缓存区–用户内存CPU 拷贝但是上下文切换次数并没有减少。整个过程共 2 次 DMA 拷贝1 次 CPU 拷贝4 次上下文切换。示意图如下。mmap把内核页缓存映射到用户虚拟内存不拷贝数据到用户 bufDMA 拷贝2 次CPU 拷贝1 次页缓存 → socket 缓冲区上下文切换4 次mmap 一次 write 一次sendFileLinux 2.1 开始提供了 sendFile 函数其基本原理是数据根本不经过用户态直接从 Kernel Buffer 进入到 Socket Buffer并且由于和用户态完全无关这就避免了2次上下文切换。下图展示了整个过程。磁盘中的数据通过 DMA 引擎从复制到内核缓冲区。调用 write 方法时从内核缓冲区拷贝到 Socket 缓冲区。由于在同一个空间因此没有发生上下文切换。最后由 Socket 缓冲区拷贝到协议引擎。整个过程共发生了 2 次 DMA 拷贝1 次 CPU 拷贝2 次上下文切换。Linux 2.4 前无 gather DMADMA 拷贝2 次磁盘→页缓存socket 缓冲区→网卡CPU 拷贝1 次内核页缓存 → socket 缓冲区内核内部内存拷贝CPU 做上下文切换2 次sendfile 一次系统调用用户→内核内核返回→用户在 Linux 2.4 版本中进一步做了优化。从 Kernel Buffer 拷贝到 Socket Buffer 的操作也省了直接拷贝到协议栈再次减少了 CPU 数据拷贝。下图展示了整个流程。本地文件 index.jsp 要传输到网络中只需 2 次拷贝。第一次是 DMA 引擎从文件拷贝到内核缓冲区第二次是从内核缓冲区将数据拷贝到网络协议栈内核缓存区只会拷贝一些元信息比如 offset 和 length 信息到 SocketBuffer基本无消耗。Linux2.4支持 gather DMA网卡支持分散 DMA只传内存地址描述符不复制数据到 socket 缓冲区DMA 拷贝2 次DMA 无法消除磁盘、网卡硬件必须 DMACPU 拷贝0 次✅零拷贝含义没有 CPU 内存复制上下文切换2 次综上所述最后一种方式发生了 2 次 DMA 拷贝、0 次 CPU 拷贝、2 次上下文切换。这就是所谓的“零拷贝”实现。总结因此零拷贝通常是站在操作系统的角度看即整个过程中内核缓冲区之间是没有重复数据的。同时伴随着更少的上下文切换。这就带来了 IO 性能质的提升实际开发中mmap 和 sendFile 都有应用可以认为是“零拷贝”的两种实现方式。它们都有各自的适用场景。mmap 更适合少量数据读写sendFile 适合大文件传输。sendFile 可以利用 DMA 方式将内核缓冲区将数据拷贝到网络协议栈减少 CPU 拷贝而 mmap 则不能必须从内核拷贝到 Socket 缓冲区。方式CPU拷贝DMA拷贝上下文切换适用场景readwrite2次2次4次通用需处理数据mmapwrite1次2次4次需访问数据内容sendfile0次2次2次磁盘→磁盘/网络splice0次2次2次管道中转硬件零拷贝0次2次2次高性能存储案例RocketMQ 在 CommitLog 和 CosumerQueue 的实现中都采用了 mmap。而 Kafka 的零拷贝实现则使用了 sendFile。✅mmap加速【磁盘 ↔ 应用】读写解决文件读写的拷贝问题✅sendfile加速【磁盘文件 → 网络 socket】传输解决网络发送的拷贝问题RocketMQ 和 Kafka 高性能的原因之一便是顺序写入和近似顺序读取 零拷贝。RocketMQ 做通用消息队列支持回溯、定时、事务、支持随机读必须 mmapKafka 做日志流平台只追新、顺序读sendfile 最优 。RocketMQ 为什么不用 sendfile选择 mmapRocketMQ 需要在 Broker 上对消息做大量原地变更事务消息提交 / 回滚要修改 commitlog 里的事务标记重试消息、死信消息更新消息属性、重试次数消息索引构建读取消息内容提取 key 构建索引消息过滤Broker 端过滤需要读取消息 body、属性做匹配如果用 sendfileBroker 进程根本读不到消息字节上面这些功能全部无法实现。引用https://zhuanlan.zhihu.com/p/543661648