基本介绍:
1.零拷贝是网络编程的关键,很多性能优化都离不开
2.零拷贝是哦那个操作系统角度来看的,是没有CPU拷贝
3.在java程序中,常用的零拷贝有 mmap (内存映射) 和 sendFile
DMA:直接内存拷贝(不使用CPU)
场景:将磁盘上的文件读取出来,然后通过网络协议发送给客户端。
传统文件IO
1.很明显发生了4次拷贝
第一次拷贝,把磁盘上的数据拷贝到操作系统内核的缓冲区里,这个拷贝是通过 DMA 的。
第二次拷贝,把内核缓冲区的数据拷贝到用户的缓冲区里,于是应用程序就可以使用这部分数据了,这个拷贝是由 CPU 完成的。
第三次拷贝,把刚才拷贝到用户的缓冲区里的数据,再拷贝到内核的 socket 的缓冲区里,这个过程依然由 CPU 完成的。
第四次拷贝,把内核的 socket 缓冲区里的数据,拷贝到协议栈里,这个过程又是由 DMA 完成的。
2.发生了4次用户上下文切换,因为发生了两个系统调用read和write。一个系统调用对应两次上下文切换,所以上下文切换次数在一般情况下只可能是偶数。想要优化文件传输的性能只有两个方向 1.减少上下文切换次数 2.减少数据拷贝次数 因为这两个操作是最耗时的
mmap
read()
系统调用的过程中会把内核缓冲区的数据拷贝到用户的缓冲区里,为了减少这一步开销,我们可以用mmap()
替换read()
系统调用函数。mmap()
系统调用函数会直接把内核缓冲区里的数据映射到用户空间,这样,操作系统内核与用户空间共享缓冲区,就不需要再进行任何的数据拷贝操作。
总的来说mmap减少了一次数据拷贝,总共4次上下文切换,3次数据拷贝
sendFile
Linux2.1
版本提供了sendFile()
函数,其基本原理如下:数据根本不经过用户态,直接从内核缓冲区进入到 SocketBuffer
总的来说有2次上下文切换,3次数据拷贝。
sendFile再优化
Linux2.4
版本中,做了一些修改,避免了从内核缓冲区拷贝到 Socketbuffer
的操作,直接拷贝到协议栈,从而再一次减少了数据拷贝
mmap 和 sendFile的区别
1.mmap适合小数据量读写,森的File适合大文件传输
2.mmap需要4次上线文切换,三次数据拷贝;sendFile需要三次上下文切换,最少两次数据拷贝
3.sendFile 可用利用DMA方式,减少CPU拷贝,mmap则不能(必须从内核拷贝到Socket缓冲区)
零拷贝的再次理解
1.我们说零拷贝,是从操作系统的角度来说的,因为内核缓冲区之间,没有数据是重复的(只有kernel buffer有一份数据)
2.零拷贝不仅仅带来更少的数据复制,还能带来其他的性能优势,例如更少的上下文切换,更少的CPU缓存伪共享以及无CPU校验和计算
零拷贝案例 — transferTo
//服务端
public class NIOServer {
public static void main(String[] args) throws Exception {
InetSocketAddress address = new InetSocketAddress(7001);
ServerSocketChannel serverSocketChannel = ServerSocketChannel.open();
serverSocketChannel.socket().bind(address);
//创建buffer
ByteBuffer buffer = ByteBuffer.allocate(4096);
while (true) {
SocketChannel socketChannel = serverSocketChannel.accept();
int readCount = 0;
while (readCount != -1) {
try {
readCount = socketChannel.read(buffer);
} catch (Exception e) {
break;
}
buffer.rewind(); //倒带
//buffer.clear();
}
}
}
}
//客户端
public class NIOClient {
public static void main(String[] args) throws Exception {
SocketChannel socketChannel = SocketChannel.open();
socketChannel.connect(new InetSocketAddress("127.0.0.1", 7001));
String fileName = "f:\\1.txt";
//得到文件channel
FileInputStream fis = new FileInputStream(fileName);
FileChannel fileChannel = fis.getChannel();
long start = System.currentTimeMillis();
//零拷贝 windows系统一次最多8MB
//transferTo底层使用零拷贝
long transferCount = fileChannel.transferTo(0, fileChannel.size(), socketChannel);
System.out.println("总字节数 = " + transferCount);
System.out.println("time:" + (System.currentTimeMillis() - start));
fileChannel.close();
}
}