NIO 与 零拷贝
零拷贝介绍
- 零拷贝是网络编程的关键, 很多性能优化都需要零拷贝。
- 在 Java程序中, 常用的零拷贝方式有m(memory)map[内存映射] 和 sendFile。它们在OS中又是怎样的设计?
- NIO中如何使用零拷贝?
NIO 与 传统IO对比
传统IO流程示意图

- user context: 用户态
- kernel context: 内核态
- User space: 用户空间
- Kernel space: 内核空间
- Syscall read: 系统调用读取
- Syscall write: 系统调用写入
- Hard drive: 硬件驱动
- kernel buffer: 内核态缓冲区
- user buffer: 用户态缓冲区
- socket buffer: 套接字缓存
- protocol engine: 协议引擎
- DMA: Direct Memory Access: 直接内存拷贝(不使用CPU)
- 总结: 4次拷贝, 3次状态切换, 效率不高
mmap优化流程示意图

- mmap 通过内存映射, 将文件映射到内核缓冲区, 同时, 用户空间可以共享内核空间的数据。
- 这样, 在进行网络传输时, 就可以减少内核空间到用户空间的拷贝次数。
- 总结: 3次拷贝, 3次状态切换, 不是真正意义上的零拷贝。
sendFile Linux2.1版本优化流程示意图

- 数据根本不经过用户态, 直接从内核缓冲区进入到Socket Buffer, 同时, 由于和用户台完全无关, 就减少了一次上下文切换。
- 但是仍然有一次CPU拷贝, 不是真正的零拷贝(没有CPU拷贝)。
- 总结: 3次拷贝, 2次切换
sendFile Linux

- 避免了从内核缓冲区拷贝到Socket buffer的操作, 直接拷贝到协议栈, 从而再一次减少了数据拷贝。
- 其实是有一次cpu拷贝的, kernel buffer -> socket buffer, 但是拷贝的信息很少, length, offset, 消耗低, 基本可以忽略。
- 总结: 2次拷贝(如果忽略消耗低的cpu拷贝的话), 2次切换, 基本可以认为是零拷贝了。
零拷贝理解
- 零拷贝是从操作系统的角度来看的。内核缓冲区之间, 没有数据是重复的(只有kernel buffer有一份数据)。
- 零拷贝不仅仅带来更少的数据复制, 还能带来其他的性能优势: 如更少的上下文切换, 更少的 CPU 缓存伪共享以及无CPU校验和计算。
mmap 与 sendFile 总结
- mmap适合小数据两读写, sendFile适合大文件传输
- mmap 需要3次上下文切换, 3次数据拷贝; sendFile 需要3次上下文切换, 最少2次数据拷贝。
- sendFile 可以利用 DMA 方式, 减少 CPU 拷贝, 而 mmap则不能(必须从内核拷贝到Socket缓冲区)。
NIO实现零拷贝
服务端
package com.ronnie.nio.zeroCopy; import java.io.IOException;
import java.net.InetSocketAddress;
import java.net.ServerSocket;
import java.nio.ByteBuffer;
import java.nio.channels.ServerSocketChannel;
import java.nio.channels.SocketChannel; public class NewIOServer { public static void main(String[] args) throws IOException { InetSocketAddress address = new InetSocketAddress(8096); ServerSocketChannel serverSocketChannel = ServerSocketChannel.open(); ServerSocket serverSocket = serverSocketChannel.socket(); serverSocket.bind(address); // 创建Buffer
ByteBuffer byteBuffer = ByteBuffer.allocate(4096); while (true){
SocketChannel socketChannel = serverSocketChannel.accept(); int readCount = 0; while (-1 != readCount){
try {
readCount = socketChannel.read(byteBuffer);
} catch (IOException e) {
e.printStackTrace();
}
// 倒带, position设为0, mark重置为-1
byteBuffer.rewind();
}
}
}
}客户端
package com.ronnie.nio.zeroCopy; import java.io.FileInputStream;
import java.io.IOException;
import java.net.InetSocketAddress;
import java.nio.channels.FileChannel;
import java.nio.channels.SocketChannel; public class NewIOClient {
public static void main(String[] args) throws IOException { SocketChannel socketChannel = SocketChannel.open(); socketChannel.connect(new InetSocketAddress("localhost", 8096)); String filename = "flink-1.9.0-bin-scala_2.12.tgz"; // 得到一个文件channel
FileChannel fileChannel = new FileInputStream(filename).getChannel(); // 准备发送
long startTime = System.currentTimeMillis(); // 在Linux下一次transferTo方法就可以完成传输
// 在Windows下一次调用transferTo 只能发送 8M, 就需要分段传输文件, 而且主要传输时的位置需要记录 long transferCount = 0L; if (fileChannel.size() <= 8){
// transferTo() 参数1: 从什么位置开始, 参数2: 截多少, 参数3: 可写的管道对象)
transferCount = fileChannel.transferTo(0, fileChannel.size(), socketChannel);
} else {
int times = (int) (fileChannel.size() / 8 + 1);
for (int i = 1; i < times; i++){
transferCount += fileChannel.transferTo(8 * i, 8 * i + 8, socketChannel);
}
} System.out.println("Total byte count: " + transferCount + " time consumed: " + (System.currentTimeMillis() - startTime)); // 关闭
fileChannel.close();
}
}
核心方法transferTo()
代码(这是fileChannelImpl中的反编译代码)
public long transferTo(long var1, long var3, WritableByteChannel var5) throws IOException {
// 确认当前管道已经开启, 检查到未开启会抛出异常
this.ensureOpen();
// 如果传入的管道未开启, 抛出异常
if (!var5.isOpen()) {
throw new ClosedChannelException();
// 如果当前管道不可读, 抛出异常
} else if (!this.readable) {
throw new NonReadableChannelException();
// 如果传入的管道是实现类 且 该管道不可写, 抛出异常
} else if (var5 instanceof FileChannelImpl && !((FileChannelImpl)var5).writable) {
throw new NonWritableChannelException();
// 如果 position >= 0 且 count >= 0
} else if (var1 >= 0L && var3 >= 0L) {
// 获取当前管道的长度
long var6 = this.size();
// 如果position已经超过当前管道末尾, 就返回0
if (var1 > var6) {
return 0L;
} else {
// 将count数与2147483647L比较并获取其中最小值, 再转换成int, 传给var8, 其实这里就是做了一个防止count越界的处理
int var8 = (int)Math.min(var3, 2147483647L);
// 如果管道末尾到position之间的长度小于var8
if (var6 - var1 < (long)var8) {
// 就把该值赋给var8
var8 = (int)(var6 - var1);
} long var9;
// transferToDirectly 直接传输
if ((var9 = this.transferToDirectly(var1, var8, var5)) >= 0L) {
return var9;
} else {
// transferToTrustedChannel 传输到可靠的管道
// transferToArbitraryChannel 传输到任意的管道
// 其实就是先尝试传输到可靠的管道, 如果传输失败, 再用任意管道继续传输
return (var9 = this.transferToTrustedChannel(var1, (long)var8, var5)) >= 0L ? var9 : this.transferToArbitraryChannel(var1, var8, var5);
}
}
} else {
throw new IllegalArgumentException();
}
}
NIO 与 零拷贝的更多相关文章
- Linux、JDK、Netty中的NIO与零拷贝
一.先理解内核空间与用户空间 Linux 按照特权等级,把进程的运行空间分为内核空间和用户空间,分别对应着下图中, CPU 特权等级分为4个,Linux 使用 Ring 0 和 Ring 3. 内核空 ...
- NIO零拷贝的深入分析
深入分析通过Socket进行数据文件传递中的传统IO的弊端以及NIO的零拷贝实现原理,及用户空间和内核空间的切换方式 传统的IO流程 在这个过程中: 数据从磁盘拷贝进内核空间缓冲区 从内核空间缓冲区拷 ...
- 零拷贝的原理及Java实现
在谈论Kafka高性能时不得不提到零拷贝.Kafka通过采用零拷贝大大提供了应用性能,减少了内核和用户模式之间的上下文切换次数.那么什么是零拷贝,如何实现零拷贝呢? 什么是零拷贝 WIKI中对其有如下 ...
- 框架篇:Linux零拷贝机制和FileChannel
前言 大白话解释,零拷贝就是没有把数据从一个存储区域拷贝到另一个存储区域.但是没有数据的复制,怎么可能实现数据的传输呢?其实我们在java NIO.netty.kafka遇到的零拷贝,并不是不复制数据 ...
- Netty 零拷贝(一)NIO 对零拷贝的支持
Netty 零拷贝(二)NIO 对零拷贝的支持 Netty 系列目录 (https://www.cnblogs.com/binarylei/p/10117436.html) 非直接缓冲区(HeapBy ...
- 零拷贝详解 Java NIO学习笔记四(零拷贝详解)
转 https://blog.csdn.net/u013096088/article/details/79122671 Java NIO学习笔记四(零拷贝详解) 2018年01月21日 20:20:5 ...
- NIO学习笔记,从Linux IO演化模型到Netty—— Java NIO零拷贝
同样只是大致上的认识. 其中,当使用transferFrom,transferTo的时候用的sendfile(). 如果系统内核不支持 sendfile,进一步执行 transferToTrusted ...
- NIO学习笔记,从Linux IO演化模型到Netty—— Linux零拷贝
这里只是感性地认识Linux零拷贝,不涉及具体细节. 1.Linux传统的数据拷贝 用户进程是不能直接访问文件系统的,要先切换到内核态,发起系统调用,DMA把磁盘中的数据写入内核空间,内核再把数据拷贝 ...
- NIO学习笔记,从Linux IO演化模型到Netty—— Netty零拷贝
Netty的中零拷贝与上述零拷贝是不一样的,它并不是系统层面上的零拷贝,只是相对于ByteBuf而言的,更多的是偏向于数据操作优化这样的概念. Netty中的零拷贝: 1.CompositeByteB ...
随机推荐
- 01初步启动Hadoop服务
1.rz命令将hadoop压缩包上传至Linux服务器中 2.tar -zxvf hadoop-2.7.7.tar.gz(解压即可用) 3.将解压出来的hadoop移到想要放的位置 mv hadoop ...
- Unity表面着色器
表面着色器和之前无光照着色器不同,其中没有顶点着色器和片元着色器,而增加了光照函数: 接下写了一个求两个贴图的光照效果 两个贴图做插值运算: Shader "Custom/SurfaceSh ...
- 嵌入式编程中使用 do{...} while(0) 的解释
最近在看esp32的idf,有一些宏定义使用了do while(0)这种看起来好像没啥用的代码.然后我查了一下资料,发现在linux内核代码中经常用到这个东西! 现在就将这个东西整理一下. 为什么在内 ...
- Java中正确使用hashCode和equals方法
在这篇文章中,我将告诉大家我对hashCode和equals方法的理解.我将讨论他们的默认实现,以及如何正确的重写他们.我也将使用Apache Commons提供的工具包做一个实现. 目录: hash ...
- [CMake笔记] 初识CMake
与CMake相遇 做C/C++的人都应该经历过,以往拿到一个开源代码,想自己编译出来,总会因为VS的版本不同而出现各种各样的问题,使编译过程困难重重,更多时候就会放弃使用它了.而这个过程反过来,也会使 ...
- Day11 - J - Brave Game HDU - 1846
十年前读大学的时候,中国每年都要从国外引进一些电影大片,其中有一部电影就叫<勇敢者的游戏>(英文名称:Zathura),一直到现在,我依然对于电影中的部分电脑特技印象深刻.今天,大家选择上 ...
- [Linux] day02——什么是Linux
什么是linux? 一种操作系统计算机 = 硬件 +软件系统软件 = 内核 + 驱动应用软件 编程 上网 linux系统构成linux内核基本库 应用程序-------------------常见的操 ...
- LeetCode刷题笔记和想法(C++)
主要用于记录在LeetCode刷题的过程中学习到的一些思想和自己的想法,希望通过leetcode提升自己的编程素养 :p 高效leetcode刷题小诀窍(这只是目前对我自己而言的小方法,之后会根据自己 ...
- 二、java基础-运算符_控制语句_break_continue_return
1.运算符 1)算术运算符:6个+ — * \ % ++ --2)关系运算符:6个> >= < <= == !=3)布尔运算符&&并且 ||或 ...
- Address localhost:1099 is already in use(IDEA启动Tomcat报错1099 is already in use)
IDEA中启动Tomcat报错,Error running Tomcat7.0.52: Address localhost:1099 is already in use 或者是 java.rmi.se ...