Netty系列整体栏目
内容 | 链接地址 |
---|---|
【一】深入理解网络通信基本原理和tcp/ip协议 | https://zhenghuisheng.blog.csdn.net/article/details/136359640 |
【二】深入理解Socket本质和BIO | https://zhenghuisheng.blog.csdn.net/article/details/136549478 |
【三】深入理解NIO的基本原理和底层实现 | https://zhenghuisheng.blog.csdn.net/article/details/138451491 |
【四】深入理解反应堆模式的种类和具体实现 | https://zhenghuisheng.blog.csdn.net/article/details/140113199 |
【五】深入理解直接内存与零拷贝 | https://zhenghuisheng.blog.csdn.net/article/details/140721001 |
深入理解直接内存与零拷贝
- 一,Nio直接内存与零拷贝
- 1,堆内存和直接内存
- 1.1,直接内存比堆内存快的原因
- 1.2,直接内存使用的缺陷
- 2,零拷贝
- 2.1,Linux DMA
- 2.2,传统的数据传输
- 2.3,零拷贝-mmap内存映射
- 2.4,零拷贝-sendfile
- 2.5,零拷贝-splice
- 3,java中使用的零拷贝
一,Nio直接内存与零拷贝
在该系列的第一片文章中,讲解了tcp的基本原理以及实现,在tcp的三次握手中,需要客户端先发起请求给服务端,然后客户端先发送一个 syn 等于1的标志,以及携带一个 seq_no 的序列号给服务端,从而完成第一次握手等等,然而根据tcp的特性,具有 网络重传 ,应答确认功能和封装报文 等功能时如何实现的呢,那么就是通过这个buffer缓冲区 实现的。
操作系统将tcp层以下的协议全部封装,然后通过调用操作系统的socket实现服务端与客户端之间的通信,因此需要通过socket去读取buffer缓冲区中的数据。这里的缓冲区指的是服务端内部之间的缓冲区,和前面提到的反应堆模式中的缓冲区不是同一个,这个缓冲区属于是业务缓冲区
1,堆内存和直接内存
通过上图可以得知,客户端在往对端发送数据时,需要先建立socket,然后通过buffer输入缓冲区和输出缓冲区,将数据发送给对端,发送数据的内部细节已由操作系统内部封装。
如在发送数据时,首选需要在用户态中,将数据加载到应用进程的缓冲区中,然后通过Socket建立连接,再调用操作系统的 write 相关的api,然后再将数据发送给内核态的 套接字发送缓冲区 里面,然后再通过一些tcp等协议栈,通过层层协议,通过网络、光缆等将数据发送给对端
1.1,直接内存比堆内存快的原因
不管是任何编程语言,都要遵循上面的这套规则,包括java也是。在java中jvm的整体架构如下,可以发现只有堆内存来存储对象,因此一般通过堆内存来存储buffer中的缓存数据
但是即使是先使用堆内存来存储buffer中的数据,在jvm内部也做了优化,在上面的这些结构中,加一个 DMA(Direct Memory Access) ,即直接内存的区域,如果是先用堆内存先存数据,也会将堆内存的数据复制(拷贝)到直接内存中,然后再通过直接内存区域中的数据,发送到内核的缓冲区,再发送到对端中。
jvm中使用直接内存的原因如下,首先是由于堆内存中会gc操作,那么在涉及对象复制或者空间碎片化管理等的时候,会将对象的位置进行移动,比如原先在100号位置,在调用write操作的时候,操作系统默认就会去读取堆内存上100号位置的数据,但是如果在读的时候发生了gc,导致堆内存上的数据发生了移动,那么原先100号的数据被挪到了50号,那么就会导致读取不到数据,或者读取不是该读取的数据 ,因此jvm为了优化这个堆内存读取buffer数据会出错的缺陷,专门开辟了一个新的直接内存,用于存储buffer数据
因此在做网络通信的时候,优先将存储buffer的内存改使用直接内存存储,堆存储需要一个数据拷贝到直接内存的时间,而直接内存不需要,因此使用直接内存是快于堆内存的。
1.2,直接内存使用的缺陷
在开发时直接使用直接内存确实爽,也可以加快整体效率,尤其时涉及到io密集型的操作时,可以显著的提升性能,但是使用直接内存也存在一些缺陷。
- 如需要开发者手动的分配和释放内存,并且内存的开辟和释放不受jvm控制,也就是说内部需要涉及到底层系统的资源分配
//定义一个1m的直接内存
ByteBuffer allocate = ByteBuffer.allocate(1024 * 1024);
//直接内存空间释放
allocate.clear();
-
其次就是如果内存不受jvm控制,那么调试会很困难,如一些内存泄漏无法保证,并且排查和调试这种问题也比较困难,一般的java监控工具都不能直接有效的监视直接内存的情况
-
最后就是这个系统的资源利用率,由于并不受jvm控制,因此会增加操作系统的资源消耗,从而影响其他进程
因此开发者在考虑使用这个直接内存时,需要权衡利弊,结合实际的应用场景选择。
2,零拷贝
2.1,Linux DMA
在最早期的数据拷贝的方式如下,需要先通过cpu的介入,通过cpu的调度将磁盘加载到内存中,如需要从输入设备读取数据,然后将其写入内存。但是cpu主要做的大量的运算操作,如果让cpu长期的出去干这种没有含金量的事情,让cpu长时间的等待,会影响整个机器的性能,并且有损cpu的性能和价值,因此原始数据拷贝的方式相对较慢
为了解决上面的缺陷,减少cpu的负担,并且提高数据的传输效率,因此在硬件层面,就增加一块硬件,专门用于处理这种直接代替cpu去读取数据,加载内存,这个就叫做Linux的直接内存DMA。如增加一个磁盘驱动器,专门用于处理这种数据的读取和加载,而cpu就不需要像之前一样去调用输入输出设备,只需要给这个DMA发送一个指令即可,然后剩下的就交给DMA。DMA目前已经适用于多种计算机的硬件设备,如 硬盘驱动器、声卡、网络设备等
2.2,传统的数据传输
Linux在引入了DMA之后,系统可以更加的高速的对数据进行读写,并且可以高效的处理一些音频视频的数据。
在传统的数据传输引入了DMA之后,其数据传输效率更加高效,接下来如看一段简单的代码表示数据的读取和传输,就是先调用操作系统的api读取数据,然后将最终读取到的数据通过Socket发送到对端
//读取数据
FileBuffer buffer = new File.read()
//将数据发送到对端
Socket.send(buffer)
上面这段代码虽然简单,但是在整个计算机内部执行的流程比较复杂,其主要流程如下:
-
如在执行第一条语句的时候,此时还在用户态,通过程序计数器执行到改行代码,然后调用操作系统的api,此时需要将用户态切换为内核态,然后先从磁盘中将数据通过DMA的方式拷贝到文件读取的buffer缓冲区中,然后将文件缓冲区的数据投通过cpu拷贝到执行这条语句的jvm进程中,此时操作系统将内核态切换为用户态
-
此时执行第二条语句,首先需要将进程中的数据通过CPU拷贝到Socket对应的buffer缓冲区中,又因为socket对应的send方法是操作系统层面的api,因此有需要从用户态切换到内核态,最后将socket中的buffer缓冲数据通过DMA拷贝的方式加入到网络设备对应的缓冲区中
总结来说就是需要4次用户态和内核态之间的上下文切换,4次数据的拷贝,分别是两次DMA拷贝,两次cpu拷贝。传统的数据传输虽然也是引入了DMA拷贝,但是依旧存在着两次CPU的拷贝,并且上下文切换也比较频繁
2.3,零拷贝-mmap内存映射
为了解决传统传输多次cpu拷贝带来的性能问题,在后面的优化中,采用了一种mmap内存映射的方式,就是提前对磁盘中数据的位置做一个映射,对应进程中的应用程序只需要通过这个地址直接去磁盘中拉取数据就行,从而不需要将数据先读取到buffer缓冲区中,同时也减少了一次cpu的拷贝
通过mmap的方式,减少了一次cpu的拷贝,因此只需要一次cpu拷贝,2次DMA拷贝,但是依旧需要4次上下文切换
2.4,零拷贝-sendfile
随着linux的不断升级,mmap的缺陷也在不断优化,在linux2.1的版本中,引入了sendfile的零拷贝方式,相对于mmap方式,sendfile同时在上下文切换和cpu拷贝时都做了优化。
从上下文切换来讲:
- 在调用操作系统的read方法之后,其数据不需要再返回到用户态中,因此在整个传输流程结束之后再切换为用户态,因此在上下文切换只需要两次,比上面的减少了两次,剩下的两次上下文切换时必不可少的
从拷贝的角度来看:
- 此时需要看该操作系统是否支持DMA拷贝,如果支持DMA拷贝,那么下图中的CPU拷贝可以直接不需要,就只需要两步:1、从磁盘中将数据通过DMA拷贝到文件缓冲区中,2、网络设备缓冲区直接读取文件缓冲区的数据 。此时只需要两次DMA拷贝,不需要CPU拷贝
- 如果该操作系统不支持使用DMA拷贝,那么就需要一次CPU拷贝,流程如下:1、从磁盘中将数据通过DMA拷贝到文件缓冲区中,2、文件缓冲区的数据通过CPU拷贝到socket中的buffer缓冲区,3、网络设备缓冲区直接读取文件缓冲区的数据 。此时需要两次DMA拷贝,1次cpu拷贝
总结就是只需要两次上下文切换,两次DMA拷贝,0次或者1次的CPU拷贝
2.5,零拷贝-splice
在linux2.6版本中,又迎来了第三次优化,由于sendfile还需要根据特定的操作系统以及对应的环境才能不用cpu的拷贝,为了解决这种出现概率性的问题,因此splice出现了
其原理和sendfile很像,只是在文件缓冲区和socket缓冲区中加了一个管道,这就相当于把这两个东西合二为一了,也就相对于磁盘拷贝出去的区域和网络设备缓冲区拷贝进来的区域是同一个区域,因此在完全解决了不需要cpu拷贝。
相对于sendfile,这种零拷贝方式不会局限于某个操作系统或者环境,能保证不需要cpu拷贝,只需要两次上下文切换和两次DMA切换
3,java中使用的零拷贝
在java中,目前暂时没有支持splice方式的零拷贝,即目前只支持mmap方式和sendfile方式。在java的各个中间件中,如kafka,nio等等这些地方用到了零拷贝。kaka在生产者端使用的是mmap,结合顺序写可以每秒处理百万级别的业务,生产端使用的是sendfile。nio中在读取数据时也使用mmap,在数据传输中也用了sendfile方式