ICode9

精准搜索请尝试: 精确搜索
首页 > 其他分享> 文章详细

NIO中的零拷贝

2022-04-27 10:00:06  阅读:135  来源: 互联网

标签:undefined buffer 内核 new 拷贝 数据 NIO


1.什么是零拷贝

  一种避免 CPU 将数据从一块存储拷贝到另外一块存储的技术。针对操作系统中的设备驱动程序、文件系统以及网络协议堆栈而出现的各种零拷贝技术极大地提升了特定应用程序的性能,并且使得这些应用程序可以更加有效地利用系统资源。这种性能的提升就是通过在数据拷贝进行的同时,允许 CPU 执行其他的任务来实现的。

2.数据拷贝

  2.1. 传统方式下的数据拷贝原理 

    

 

 

 

①一个read系统调用后,DMA执行了一次数据拷贝,从磁盘到内核空间
②read结束后,发生第二次数据拷贝,由cpu将数据从内核空间拷贝至用户空间
③send系统调用,cpu发生第三次数据拷贝,由cpu将数据从用户空间拷贝至内核空间(socket缓冲区)
④send系统调用结束后,DMA执行第四次数据拷贝,将数据从内核拷贝至协议引擎


2.1. 基于NIO的数据零拷贝(sendfile) 

①DMA从拷贝至内核缓冲区 ②cpu将数据从内核缓冲区拷贝至内核空间(socket缓冲区) ③DMA将数据从内核拷贝至协议引擎   3.代码实现基于NIO数据零拷贝     3.1.  服务端 复制代码
public class IOServer {
    public static void main(String[] args) throws IOException {
        InetSocketAddress inetSocketAddress  = new InetSocketAddress(7001);
        ServerSocketChannel serverSocketChannel=ServerSocketChannel.open();
        //绑定
        serverSocketChannel.socket().bind(inetSocketAddress);
        //创建buffer
        ByteBuffer byteBuffer=ByteBuffer.allocate(1024);
        while (true){
            //获得socketChannel
            SocketChannel socketChannel = serverSocketChannel.accept();
            int readcount=0;
            while (-1!=readcount){
                try{
                    readcount = socketChannel.read(byteBuffer);
                }catch (Exception e){
                    break;
                }
                byteBuffer.rewind();//倒带,让position=0,mark作废
            }

        }

    }
}
复制代码

  3.2.  客户端

复制代码
public class IOClient {
    public static void main(String[] args) throws IOException {
        SocketChannel socketChannel = SocketChannel.open();
      //  socketChannel.connect(new InetSocketAddress("localhost", 7001));
        socketChannel.connect(new InetSocketAddress("127.0.0.1", 7001));
        String fileName = "1.txt";
        //获取文件通道
        FileChannel fileChannel = new FileInputStream(fileName).getChannel();
        //发送开始计时
        Long startTime = System.currentTimeMillis();

        /** transferTo实现零拷贝
         * 无论文件大小
         *在Linux系统下运行transferTo一次性可以传输完成
         * 在windows下一次只能发送8M,超出范围文件需要分段传输
         *  transferTo(long position, long count,WritableByteChannel target)
         *参数1:文件传输时的位置,作为分段传输的标注点
         * 参数2:文件大小
         * 参数3.通道
         */
        long l = fileChannel.transferTo(0, fileChannel.size(), socketChannel);

        System.out.println("发送的字节总数="+l+",耗时:"+(System.currentTimeMillis()-startTime));
        //关闭
        fileChannel.close();
    }
}

NIO的零拷贝的0指的是,0次需要cpu的拷贝,DMA拷贝不算在里面。零拷贝可以大大提高我们数据传输的效率。

传统IO

 

 


磁盘 -> 内核空间的缓存(DMA)
内核空间的缓存 -> 用户态中的程序缓存
对数据做一系列操作
用户态中的程序缓存 -> 内核空间的的socket缓存
内核空间的socket缓存->网卡缓存(DMA)
网络发送(或拷贝到磁盘)!
在一次传统的IO中,操作系统一共进行了2次拷贝,4次操作系统状态转换

2次拷贝:注意,磁盘到内核态的读写是通过DMA拷贝,外部设备(磁盘,U盘)不通过CPU直接与系统内存交换数据。所以是2次拷贝
4次系统状态切换:详情如下

 

 

tips: 为什么数据不直接从磁盘拷贝到用户程序空间呢?通过局部性原理我们知道,当我们从磁盘上取一个数据的时候,有很大可能下一次读取的数据就是本次读取数据周围的数据。因此操作系统为了提高性能,在读取数据的时候,会先把该目标数据周围的数据(NTFS下是4KB为单位)也一并读到操作系统的read buffer中,下次读取时,就有很大可能命中read buffer,减少了磁盘IO

NIO 零拷贝
不需要对数据进行操作
windows Linux2.4以下

 

 


sendFile()
该版本下的sendFile,只需要2次状态切换,即 开始: 用户态->内核态(read buffer 到 socket buffer)->用户态,但是还需要把Read buffer中的数据通过cpu完整的拷贝到Socket Buffer,所以不能算是真正的0拷贝。

该种方法是2次切换,1次拷贝
Linux 2.4以后

 

 


sendFile()
该版本下,sendFile()直接将内核态的Read buffer发送到NIC的 buffer,只有部分描述信息,如缓存位移,描述符经过socket buffer,通过这些信息,DMA直接把数据拷贝到外围设备缓存中。由于数据描述信息量很少,基本可以视作0,所以这也是0拷贝
需要对数据进行操作
当我们需要在用户空间对数据进行处理时,上述方法都不能解决问题

mmap,把内核态的缓存空间和用户态的缓存空间映射到一块物理地址,实现共享,减少了内核态缓存到用户态缓存的一次拷贝。(因为使用mmap后,内存共享了,进程直接对内核空间映射到进程的虚拟地址进行读写就行,无需拷贝,但系统状态还是需要切换的)
该种方法是4次上下文切换,3次拷贝(2次DMA拷贝)。

3次上下文切换:
发出mmap系统调用,用户态->内核态,通过DMA将磁盘文件拷贝到read buffer中
mmap系统调用返回,内核态->用户态,此时用户空间和内核空间中的read buffer共享一块缓冲区内存空间
用户修改数据,并发出write调用,用户态->内核态,read buffer中的数据通过cpu拷贝到socket buffer
write调用返回,socker buffer通过DMA拷贝到外围设备。内核态->用户态
零拷贝的再次理解
我们说零拷贝,是从cpu的角度来说的,减少对cpu占用可以大大提升效率,而DMA拷贝无法避免。
零拷贝不仅仅带来更少的数据复制,还能带来其他的性能优势,例如更少的上下文切换,更少的CPU缓存伪共享以及无CPU校验和计算。
mmap和sendFile的区别
mmap适合小数据读写, sendFile适合大文件传输
mmap需要4次上下文切换,1次数据拷贝(还有);sendFile需要3次上下文切换,最小号2次数据拷贝
sendFile可以利用DMA,减少CPU拷贝,mmap则不能(必须从内核拷贝到Socket缓冲区)

 

ava中零拷贝有2种(零拷贝是指没有CPU拷贝)

1,mmap(内存映射)

2,sendfile

传统IO数据读写;

File file = new File("test.txt");

RandomAccessFile raf = new RandomAccessFile(file, "rw");

byte[] arr = new byte[(int) file.length()]; raf.read(arr);

Socket socket = new ServerSocket(8080).accept();

socket.getOutputStream().write(arr);

过程如下;

 

 

 

传统IO发现copy了4次,cpu状态切换三次,

所以要优化;

MMAP优化:

mmap 通过内存映射,将文件映射到内核缓冲区,同时,用户空间可以共享内核空间的数据。这样,在进行网络传输时,就可以减少内核空间到用户控件的拷贝次数。如下图

 

 

 

 

此时copy 3次,状态也是3次,

 

sendFile 优化

Linux 2.1 版本 提供了 sendFile 函数,其基本原理如下:数据根本不经过用户态,直接从内核缓冲区进入到 Socket Buffer,同时,由于和用户态完全无关,就减少了一次上下文切换 示意图和小结 提示:零拷贝从操作系统角度,是没有cpu 拷贝

 

 

 

此时是3次拷贝,切换变成了2次;

Linux 在 2.4 版本中,做了一些修改,避免了从内核缓冲区拷贝到 Socket buffer 的操作,直接拷贝到协议栈,从而再一次减少了数据拷贝。具体如下图和小结: 这里其实有 一次cpu 拷贝 kernel buffer -> socket buffer 但是,拷贝的信息很少,比如 lenght , offset , 消耗低,可以忽略

 

 

 

上图中socketbuffer变成灰色,kernel buffer可以通过DMA copy进入 protocol engine;

我们说零拷贝,是从操作系统的角度来说的。因为内核缓冲区之间,没有数据是重复的(只有 kernel buffer 有一份数据)。 零拷贝不仅仅带来更少的数据复制,还能带来其他的性能优势,例如更少的上下文切换,更少的 CPU 缓存伪共享以及无 CPU 校验和计

 

mmap 和 sendFile 的区别 mmap 适合小数据量读写,sendFile 适合大文件传输。 mmap 需要 4 次上下文切换,3 次数据拷贝;sendFile 需要 3 次上下文切换,最少 2 次数据拷贝。 sendFile 可以利用 DMA 方式,减少 CPU 拷贝,mmap 则不能(必须从内核拷贝到 Socket 缓冲区)。

例子:

比如下面的零拷贝

public class NewIOClient {undefined
public static void main(String[] args) throws Exception {undefined

SocketChannel socketChannel = SocketChannel.open();
socketChannel.connect(new InetSocketAddress("localhost", 6666));
String filename = "pro*****.zip";

//得到一个文件channel
FileChannel fileChannel = new FileInputStream(filename).getChannel();

//准备发送
long startTime = System.currentTimeMillis();

//在linux下一个transferTo 方法就可以完成传输
//在windows 下 一次调用 transferTo 只能发送8m , 就需要分段传输文件, 而且要主要
//传输时的位置 =》
//transferTo 底层使用到零拷贝
long transferCount = fileChannel.transferTo(0, fileChannel.size(), socketChannel);

System.out.println("发送的总的字节数 =" + transferCount + " 耗时:" + (System.currentTimeMillis() - startTime));

//关闭
fileChannel.close();

}
}

//服务器
public class NewIOServer {undefined
public static void main(String[] args) throws Exception {undefined

InetSocketAddress address = new InetSocketAddress(6666);

ServerSocketChannel serverSocketChannel = ServerSocketChannel.open();

ServerSocket serverSocket = serverSocketChannel.socket();

serverSocket.bind(address);

//创建buffer
ByteBuffer byteBuffer = ByteBuffer.allocate(4096);

while (true) {undefined
SocketChannel socketChannel = serverSocketChannel.accept();

int readcount = 0;
while (-1 != readcount) {undefined
try {undefined

readcount = socketChannel.read(byteBuffer);

}catch (Exception ex) {undefined
// ex.printStackTrace();
break;
}
//
byteBuffer.rewind(); //倒带 position = 0 mark 作废
}
}
}
}

 

以及老的拷贝方式:

public class OldIOClient {undefined

public static void main(String[] args) throws Exception {undefined
Socket socket = new Socket("localhost", 6666);

String fileName = "proto***.zip";
InputStream inputStream = new FileInputStream(fileName);

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) {undefined
total += readCount;
dataOutputStream.write(buffer);
}

System.out.println("发送总字节数: " + total + ", 耗时: " + (System.currentTimeMillis() - startTime));

dataOutputStream.close();
socket.close();
inputStream.close();
}
}

//java IO 的服务器
public class OldIOServer {undefined

public static void main(String[] args) throws Exception {undefined
ServerSocket serverSocket = new ServerSocket(6666);

while (true) {undefined
Socket socket = serverSocket.accept();
DataInputStream dataInputStream = new DataInputStream(socket.getInputStream());

try {undefined
byte[] byteArray = new byte[4096];

while (true) {undefined
int readCount = dataInputStream.read(byteArray, 0, byteArray.length);

if (-1 == readCount) {undefined
break;
}
}
} catch (Exception ex) {undefined
ex.printStackTrace();
}
}
}
}
old用时:

 

标签:undefined,buffer,内核,new,拷贝,数据,NIO
来源: https://www.cnblogs.com/hanease/p/16197735.html

本站声明: 1. iCode9 技术分享网(下文简称本站)提供的所有内容,仅供技术学习、探讨和分享;
2. 关于本站的所有留言、评论、转载及引用,纯属内容发起人的个人观点,与本站观点和立场无关;
3. 关于本站的所有言论和文字,纯属内容发起人的个人观点,与本站观点和立场无关;
4. 本站文章均是网友提供,不完全保证技术分享内容的完整性、准确性、时效性、风险性和版权归属;如您发现该文章侵犯了您的权益,可联系我们第一时间进行删除;
5. 本站为非盈利性的个人网站,所有内容不会用来进行牟利,也不会利用任何形式的广告来间接获益,纯粹是为了广大技术爱好者提供技术内容和技术思想的分享性交流网站。

专注分享技术,共同学习,共同进步。侵权联系[81616952@qq.com]

Copyright (C)ICode9.com, All Rights Reserved.

ICode9版权所有