零拷贝基本介绍
内核空间和用户空间
操作系统的核心是内核,独立于普通的应用程序,可以访问受保护的内存空间,也有访问底层硬件设备的所有权限。为了保证用户进程不能直接操作内核( kernel),保证内核的安全,操作系统将虚拟空间划分为两部分:
一部分为内核空间,一部分为用户空间。以下代码为例,介绍内存copy
File file = new File("D://test.txt");
RandomAccessFile raf = new RandomAccessFile(file,"rw");
byte[] bytes = new byte[(int) file.length()];
// 1 读
raf.read(bytes);
Socket socket = new ServerSocket(8080).accept();
// 2 写
socket.getOutputStream().write(bytes);
IO流读文件
当读取文件时,首先会从用户态切换内核态,读取磁盘到内核空间(DMA copy:直接内存拷贝);
然后再将数据读到用户空间,此时又会从内核态切换到用户态,所以需要2次切换,2次 copy.
IO流写文件
当写时,首先将文件写到socker buffer ,会从用户态切换到内核态,
再从socker buffer,写道协议栈中,完成后会切换会用户态。也需要2次切换,2次 copy.
完整流程:
总结:在上述流程中,两次CPU copy显然时多余的,对此提出优化。零拷贝是网络编程的关键,很多性能优化都离不开。在Java程序中,常用的零拷贝有mmap(内存映射)和 sendfile。
虚拟内存
现代操作系统使用虚拟内存,即虚拟地址取代物理地址,使用虚拟内存可以有2个好处:
- 虚拟内存空间可以远远大于物理内存空间
- 多个虚拟内存可以指向同一个物理地址
正是多个虚拟内存可以指向同一个物理地址,可以把内核空间和用户空间的虚拟地址映射到同一个物理地址,这样的话,就可以减少IO的数据拷贝次数啦
mmap 内存映射优化
mmap正是才有虚拟内存技术,不再将数据复制到用户空间,直接对内核空间的数据进行操作
总结:用户空间可以共享某块内核空间,减少了一次copy
sendfile优化
linux 2.1提供,实现原理是数据不经过用户态,直接从内核空间copy到socket buffer;
linux 2.4优化,避免了从内核空间copy到socket buffer,而只是将描述(长度/偏移量等)copy到socket buffer
sendfile表示在两个文件描述符之间传输数据,它是在操作系统内核中操作的,避免了数据从内核缓冲区和用户缓冲区之间的拷贝操作,因此可以使用它来实现零拷贝。
总结:一共需要经历 3次 copy, 2次切换。
mmap与sendfile的区别
- mmap适合小数据量读写, sendfile适合大文件传输
- mmap需要4次上下文切换,3次数据拷贝; send File需要3次上下文切换,最少2次数据拷贝
- sendfile可以利用DMA方式,减少CPU拷贝,mmap则不能
NIO零拷贝使用
transferTo : 基于sendfile,DMA引擎直接把数据从内核缓冲区传输到协议引擎,从而消除了最后一次CPU
copy。经过上述过程,数据只经过了2次copy就从磁盘传送出去了。实现了真正的Zero-Copy
// 服务端
public class NioFileServer {
public static void main(String[] args) throws IOException {
ServerSocketChannel serverSocketChannel = ServerSocketChannel.open();
// 创建 serverSocketChannel 并绑定端口
serverSocketChannel.socket().bind(new InetSocketAddress(7001));
ByteBuffer buffer = ByteBuffer.allocate(1024);
while (true) {
SocketChannel client = serverSocketChannel.accept();
int count = 0;
while (-1 != count) {
try {
count = client.read(buffer);
} catch (IOException e) {
break;
}
// 倒带 就是复用
buffer.rewind();
}
}
}
}
// 客户端
public class NioFileClient {
public static void main(String[] args) throws IOException {
SocketChannel channel = SocketChannel.open();
channel.socket().connect(new InetSocketAddress("127.0.0.1",7001));
String filePath = "H:\\资料\\书籍\\Java并发编程之美.pdf";
FileChannel fileChannel = new FileInputStream(filePath).getChannel();
long start = System.currentTimeMillis();
// linux 下调用 transferTo 就可以完成
// window 下调用 transferTo 一次最多 8M ,所以需要分段传输,注意每次的传输位置
long fileSize = fileChannel.size();
System.out.println("原始文件大小: "+fileSize);
int pageSize = 8 * 1024 * 1024 ; // 8M
int pageIndex = 0;
long transCount = 0 ;
while (true){
int i = pageIndex * pageSize;
if (i < fileSize) {
transCount += fileChannel.transferTo(i, pageSize, channel);
pageIndex ++;
System.out.printf("第%d 批 ",pageIndex);
}else {
break;
}
}
System.out.println();
System.out.println("传输文件大小:"+transCount +" 耗时:" + (System.currentTimeMillis() - start));
fileChannel.close();
}
}