5.NIO零拷贝与传统IO的文件传输性能比较
Posted PacosonSWJTU
tags:
篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了5.NIO零拷贝与传统IO的文件传输性能比较相关的知识,希望对你有一定的参考价值。
【README】
1.本文总结自B站《netty-尚硅谷》,很不错;
2.本文部分内容参考自 NIO效率高的原理之零拷贝与直接内存映射 - 腾讯云开发者社区-腾讯云
【1】零拷贝原理
【1.1】传统IO的文件拷贝
【图解】
- step1)调用 sys_read系统调用,从用户态进入内核态;借助DMA通道把磁盘驱动数据 读入内核读缓冲区(无需拷贝,因为零拷贝讲的是 无需耗费CPU资源的拷贝,而是DMA拷贝);
- step2)借助CPU把 内核缓冲区数据 读取到用户态缓冲区后,从sys_read系统调用返回,从内核态切换回用户态;(第1次拷贝)
- step3)调用 sys_write 系统调用,从用户态进入内核态;借助CPU把 用户态缓冲区 数据写入 socket 缓冲区(第2次拷贝)
- step4)把 socket 缓冲区数据 借助DMA通道 写入到 网卡驱动(无需cpu参与的拷贝,仅DMA);
- step5)写入完成后,从内核态返回用户态;
小结: 上述过程中,操作系统底层 有4次用户态与内核态的切换,2次cpu拷贝(DMA拷贝不占CPU,不计入);文件读写(拷贝)性能较低;
补充:
- 虽然DMA拷贝不占用cpu,但它占用系统总线,一定程度上也会影响cpu性能(但这不是本文重点,可以忽略不计);
【1.2】零拷贝
1)零拷贝 :
- Linux 在 2.4 版本中,做了一些修改,sendFile() 系统调用 避免了从内核缓冲区拷贝到 Socket buffer 的操作,直接拷贝到协议栈,从而再一次减少了数据拷贝。
2)零拷贝流程图 :
【图解】
- step1)调用 sys_read系统调用,从用户态进入内核态,借助DMA通道 把磁盘驱动数据 读入到 内核读缓冲区;(DMA拷贝)
- step2)接着在内核态中,调用系统调用 sendfile ,cpu把 读缓冲区的数据写出到 socket缓冲区;(第1次CPU拷贝)
- step3)借助DMA通道,把 socket缓冲区数据 写出到 网卡缓冲区;(DMA拷贝)
- step4)最后从内核态返回到用户态;
【小结】
- 上述过程中,操作系统底层 有2次用户态与内核态的切换,1次cpu拷贝(非真正零拷贝,因为有1次cpu拷贝);
- 显然,相比传统IO过程,NIO的零拷贝技术的文件传输性能更高;
- 补充:*若网卡驱动支持 gather操作,DMA可以直接把数据从内核读缓冲区直接拷贝到网卡驱动,而无需cpu拷贝(真正实现CPU零拷贝);
NIO零拷贝适用于以下场景:
- 文件较大,读写较慢,追求速度;
- JVM内存不足,不能加载太大数据;
- 内存带宽不够,即存在其他程序或线程存在大量的IO操作,导致带宽本来就小;
【2】代码实现
【2.1】基于传统IO传输文件
注意:字节缓冲 4M (字节缓冲大小会影响传输性能,当然了,一定条件下,缓冲越大越好);
1)服务器:
/**
* @Description 传统IO服务器
* @author xiao tang
* @version 1.0.0
* @createTime 2022年08月20日
*/
public class Oldioserver
public static void main(String[] args) throws IOException
// 服务器监听端口 7001
ServerSocket serverSocket = new ServerSocket(7001);
while (true)
// 阻塞式等待客户端请求链接
Socket socket = serverSocket.accept();
DataInputStream dataInputStream = new DataInputStream(socket.getInputStream());
try
byte[] byteArr = new byte[4096];
// 读取客户端的数据到字节数组
while (dataInputStream.read(byteArr, 0, byteArr.length) != -1) ;
catch (Exception e)
e.printStackTrace();
2)客户端:
/**
* @Description 传统IO客户端
* @author xiao tang
* @version 1.0.0
* @createTime 2022年08月20日
*/
public class OldIOClient
public static void main(String[] args) throws IOException
Socket socket = new Socket("127.0.0.1", 7001);
// 传输 一个 zip文件
InputStream inputStream = new FileInputStream("D:\\\\cmb\\\\studynote\\\\netty\\\\temp\\\\springboot.zip");
DataOutputStream dataOutputStream = new DataOutputStream(socket.getOutputStream());
byte[] buffer = new byte[4096];
long readCount;
long total = 0;
long startTime = System.currentTimeMillis();
while ((readCount = inputStream.read(buffer)) >= 0)
total += readCount;
dataOutputStream.write(buffer);
long cost = System.currentTimeMillis() - startTime;
System.out.println("发送总字节数 " + total + ", 耗时 = " + cost);
// 关闭资源
dataOutputStream.close();
socket.close();
inputStream.close();
3)效果:
发送总字节数 4491230, 耗时 = 50
【2.2】基于NIO零拷贝传输文件
注意:字节缓冲 4M (字节缓冲大小会影响传输性能,当然了,一定条件下,缓冲越大越好);
1)服务器:
/**
* @Description nio实现零拷贝服务器
* @author xiao tang
* @version 1.0.0
* @createTime 2022年08月20日
*/
public class ZeroCopyNIOServer
public static void main(String[] args) throws IOException
// 服务器套接字通道
ServerSocketChannel serverSocketChannel = ServerSocketChannel.open();
// 绑定端口
serverSocketChannel.socket().bind(new InetSocketAddress("127.0.0.1", 7001));
ByteBuffer buffer = ByteBuffer.allocate(4096);
while (true)
// 等待客户端连接
SocketChannel socketChannel = serverSocketChannel.accept();
// 读取数据
while (socketChannel.read(buffer) != -1)
// 缓冲倒带, 设置 position=0, mark作废
buffer.rewind();
2)客户端:
/**
* @Description nio实现零拷贝服务器
* @author xiao tang
* @version 1.0.0
* @createTime 2022年08月20日
*/
public class ZeroCopyNIOClient
public static void main(String[] args) throws IOException
SocketChannel socketChannel = SocketChannel.open(new InetSocketAddress("127.0.0.1", 7001));
FileChannel fileChannel = new FileInputStream("D:\\\\cmb\\\\studynote\\\\netty\\\\temp\\\\springboot.zip").getChannel();
long startTime = System.currentTimeMillis();
// 在 linux 下,一次调用 transferTo 方法就可以完成传输
// 在 window下,一次调用 transferTo 只能发送 8M,如果大于8M,则需要分段传输文件
// transferTo 底层就用到了 零拷贝
long transferCount = fileChannel.transferTo(0, fileChannel.size(), socketChannel);
long cost = System.currentTimeMillis() - startTime;
System.out.println("客户端发送数据成功,耗时= " + cost + ", 传输的字节总数 = " + transferCount);
3)效果:
客户端发送数据成功,耗时= 10, 传输的字节总数 = 4491230
4)补充: 关于 FileChannel.transferTo 方法
- 在 linux 下,一次调用 FileChannel.transferTo 方法就可以完成传输;
- 在 window下,一次调用 transferTo 只能发送 8M,如果大于8M,则需要分段传输文件;
- transferTo 底层就用到了 零拷贝;
5)transferTo方法底层使用的是 sendfile 系统调用(零拷贝)
- 该系统调用 实现了数据直接从内核的读缓冲区传输到套接字缓冲区,避免了用户态(User-space) 与内核态(Kernel-space) 之间的数据拷贝。
【4】性能比较
【表】传统IO与NIO零拷贝传输性能对比 (文件大小 4M)
Java IO类型 | 传输耗时 | 备注 |
传统IO | 50ms | |
NIO零拷贝 | 10ms | 性能更优 |
以上是关于5.NIO零拷贝与传统IO的文件传输性能比较的主要内容,如果未能解决你的问题,请参考以下文章