Netty学习-java NIO零拷贝

Netty学习-java NIO零拷贝

二月 18, 2020

零拷贝基本介绍

  1. CPU不执行拷贝数据从一个存储区域到另一个存储区域的任务,这通常用于通过网络传输一个文件时以减少CPU周期和内存带宽(零拷贝是从操作系统的角度来考虑, 是不经过cpu拷贝, 但是DMA拷贝(直接内存映射)不可避免)
  2. 在java程序中, 常用的零拷贝有 mmap(内存映射) 和 sendFile.
  3. 我们说的零拷贝是从操作系统的角度来说的.因为内核缓冲区之间没有数据是重复的(只有kernel buffer有一份数据).

零拷贝的优点

  • 减少甚至完全避免不必要的CPU拷贝,从而让CPU解脱出来去执行其他的任务
  • 减少内存带宽的占用
  • 通常零拷贝技术还能够减少用户空间和操作系统内核空间之间的上下文切换

传统io读写方式

1
2
3
4
5
6
File file = new File("index.html");
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);

相关说明:

  • 用户态: use context
  • 内核态: kernel context
  • DMA copy: 直接内存复制(数据)
  • CPU copy: 通过cpu进行数据复制
  • User space: 用户态空间
  • Kernel space: 内核态空间


如图传统io的数据读写过程经历了4次拷贝(2次DMA拷贝, 2次CPU拷贝), 4次上下文切换

使用mmap读写方式

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

图示

如图, 使用mmap读写方式经历了3次拷贝(2次DMA拷贝, 1次CPU拷贝), 4次上下文切换.对比传统io的读写, mmap方式减少了在 user buffer ->socket buffer 这个地方的CPU拷贝

sendfile

Linux 2.1版的sendfile函数

数据根本不经过用户态,直接从内核缓冲区进入到 Socket Buffer,同时,由于和用户态完全无关,就减少了一次上下文切换。

如图, Linux 2.1版的sendfile方式经历了3次拷贝(2次DMA拷贝, 1次CPU拷贝), 3次上下文切换. 对比mmap方式, 减少了一次用户态到内核态的切换

Linux 2.4版的sendfile函数

Linux 在 2.4 版本中,做了一些修改,避免了从内核缓冲区拷贝到 Socket buffer 的操作,直接拷贝到协议栈,从而再一次减少了数据拷贝。

如图, Linux 2.4版的sendfile方式只经历了2次DMA拷贝, 3次上下文切换, 组织实现了零拷贝(无CPU拷贝)

mmap 和 sendfile 的区别

  1. mmap适合小数据量读写, sendfile适合大文件传输.
  2. mmap需要4次上下文切换, 3次数据拷贝
    sendflie 需要3次上下文切换, 最少2次数据拷贝(因linux内核版本不同)
  3. sendfile可以利用DMA方式减少CPU拷贝, 而mmap必须使用内核拷贝到Socket缓冲区的方式

NIO零拷贝实例

  1. 使用NIO零拷贝方式传递一个大文件

Server端代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
public class NioZeroCopyServer {
public static void main(String[] args) throws Exception {

InetSocketAddress address = new InetSocketAddress(8888);
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);
if (readCount != 0) {
System.out.println(readCount);
}
} catch (Exception e) {
break;
}

/* 倒带 position = 0 mark作废 */
byteBuffer.rewind();
}

}

}

}

Client端代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
public class NioZeroCopyClient {
public static void main(String[] args) throws Exception{
SocketChannel socketChannel = SocketChannel.open();
socketChannel.connect(new InetSocketAddress("localhost", 8888));
String fileName = "xxx.zip";

/* 获取一个文件 channel */
FileChannel fileChannel = new FileInputStream(fileName).getChannel();

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

/*
1.linux下一个transferTo方法就可以完成传输,
在Windows下一次调用transferTo方法最多发送的数据大小有限制(最多8M), 需要分段传输.

2.transferTo底层使用的是零拷贝
*/

long position = 0;
long size = fileChannel.size();
long total = 0;
while (position < size) {

long transfer = fileChannel.transferTo(position, fileChannel.size(), socketChannel);
System.out.println("发送:" + transfer);
if (transfer <= 0) {
break;
}
total += transfer;
position += transfer;
}

/*
linux下直接加入以下代码即可
long total = fileChannel.transferTo(0, fileChannel.size(), socketChannel)
*/
System.out.println("发送的总字节数: = " + total + "耗时: " + (System.currentTimeMillis() - startTime));
/* 关闭 */
fileChannel.close();
}
}

结果:

xxx.zip 文件大小: 13.4 MB (14,151,048 字节)

Client: 发送:8388608 发送:5762440 发送的总字节数: = 14151048耗时: 80

Server: (比较长, 截取最后部分) ... 4096 4096 4096 4096 4096 4096 4096 3484