java nio的底层原理说明之kafka-2.6.0补充
java nio通常和java的零拷贝放在一起来说,但这么说却存在着很是模糊的地方.对所谓的零拷贝而言,如果从操作系统的层面来说,这是没有意义的,因为永远不可能出现零次拷贝.若只从应用层面来看,零拷贝也只是一种概念,在我们能看到的io效率提升的背后,是否真的节省了操作步骤,事实可能与我们想的不一样.
1 基本概念说明
与io最为相关的就是计算机的存储,这里主要明确一下这些不同的存储概念的定义.
1.1 内存
按照传统的计算机专业说法,内存一般指虚拟内存,对于unix操作系统来说就是地址空间,对于windows来说虚拟内存由专门的设定,这里不考虑windows系统,只针对unix系统.
1.2 主存
相对于内存,这个是具体的物理内存,存储具体的数据,若掉电数据可能会丢失.
1.3 页表
无论对于内存亦或是主存,它们都被分页了,由页表来记录它们之间的映射关系,可以实现两个不同的内存地址映射到统一主存上.
即使是现代计算机,以上术语也适用,这里就按照这些概念展开说明.
2 用户数据和网卡之间的io流程说明
这里以一个用户本地的队列数据和网卡之间的数据移动为例,来说明io的流程.
假设这里用户从本地队列中拿出一个请求发送到本地的网卡上,这个过程可以被描述为下列过程
1 队列中的用户数据被拷贝到用户空间中的缓存内
2 发起系统调用,内核把用户空间缓存内的数据拷贝到自身的缓存中
3 在内核区内,数据从缓存区拷贝到socket缓存区
4 内核把socket缓存内的数据拷贝到网卡上
接收rpc结果也是一样的,顺序则相反,整个过程如下图所示
实线代表读数据,虚线代表写数据.可见,不可避免的发生了4次拷贝,这些拷贝相比较于“零拷贝”,确实多了一些.
3 rpc的拷贝过程中用户空间和内核空间的交互分析
以读数据为例,这里开看一下用户空间和内核空间之间的交互.
//用户发起请求
//内核查看了缓冲区后,此时就有如下三种场景
用户: 我要读数据
场景一
内核: 请稍等,正在查看中… …
用户: 我等着
//过了2分钟
内核: 数据已给您了
用户: 再见
场景二
内核: 没有数据,您还是回去吧
用户: 好的,我还会再来的
//过了1分钟
用户: 我要读数据
内核: 没有数据,您还是回去吧
用户: 好的,我还会再来的
//过了1分钟
用户: 我要读数据
内核: 数据已给您了
用户: 再见
场景三
内核: 我会给您发邮件,勿回,再见
用户: 再见
第一种场景就是bio,用户被内核阻塞了,因为数据没到缓存区,尽管此时数据可能已到了socket缓存区.
第二种场景是nio,用户没有被内核阻塞,但是数据也没到缓存区,用户定时来向内核要数据.
第三种场景是aio,用户不仅没被内核阻塞,还得到了内核的主动服务.内核通过发送信号来告知用户数据已放入其缓存区内,用户得到通知后就可以拿数据了.
第一种场景使用户不满意,第三种场景目前的linux系统内核不支持,所以java nio就是第二种场景.下面就详细分析这个java nio.
4 java nio解析
这里主要针对网络部分进行解析,文件部分比较简单,道理都是一样的.
相信不熟悉与操作系统相关的一些系统调用的读者会对java nio感到比较迷惑,诸如channel管道到底是干什么用的?还有buffer,既然有了管道,这个buffer还来做什么?selector好像与io没太大的关系,能不使用吗?
这些问题确实有些迷惑,但是也是很有价值的.
首先就要弄清一个最基本的问题,java nio为什么会有这些类?
这一切都要从如何减少rpc过程中的拷贝次数说起.
4.1 减少数据拷贝次数的方法
通过前面rpc过程中的数据拷贝分析,可以看出
1 如果内核和用户共同使用一个缓存区,那么用户和内核之间的拷贝就没有了.
2 如果内核内部的socket缓存区也合并,这里面的拷贝也没有了,就剩下用户数据和网卡的两次拷贝了.
1 文件内存映射
文件内存映射就是第一种假设的解决方案,可以认为就是利用页表的功能,把不同的内存地址映射到同一个范围的主存上,使得内核缓存区和用户缓存区重合了,减少一次数据拷贝.
具体做法是借助把硬盘上的文件完整的映射到内存中,再通过内存的转译,把另一个不同的内存地址映射到此文件的内存地址上,最后再把内存地址映射到主存上,达到了合并缓存和减少一次拷贝的效果.
2 socket缓存另类合并
这里说的另类,并不是像文件内存映射那样在内存层面合并了,而是把其socket缓存中存储的数据地址信息保存在了普通的缓存中,socket缓存的数据依然在,这样在读普通缓存时就根据这个地址信息把socket缓存一并读取了,当然socket缓存区内的数据拿到了.在写数据时,先写普通缓存区再写socket缓存区的顺序依然没有改变,只是此时写普通缓存区时只写上了socket缓存区的地址,完事就开始写socket缓存区,这些都是在内存层面上的操作.
最终通过间接的合并,读写都实现了又减少一次的拷贝.
4.2 java nio网络中的类和减少拷贝次数方法之间的对应
1 chennal
用户数据到缓存区的拷贝以及缓存区到网卡的拷贝,都需要使用channel,那么channel的定位是什么?
假定操作系统已经为我们合并了缓存区,包括用户和内核的,以及内核socket缓存区.这时channel的定位就明确了,那就是封装了在此场景下用户数据或网卡与合并后的缓存之间读写数据的方法(这些方法是系统调用方法).
因为合并后的缓存区不可能再使用老式的读写数据的方法,所以channel其实就是新式读写数据的方法的包装.
2 buffer
这个很明显了,就是合并后的缓存区的子集,当然也是其一部分.所以结合channel,才能把数据从用户那里或网卡上写到合并后的缓存区中.
文件的buffer就是普通缓存区,网络的buffer就是socket缓存区.
3 selector
这个就有些绕弯了.
回到减少拷贝次数方法的分析中,在socket缓存的另类合并场景下,每次内核查看普通缓存区时,都会知道socket缓存区的地址信息是否已准备好,若准备好了,就可以一并读取socket缓存区的数据了.
那么此时如果内核能够事先得到通知,说socket缓存区的地址信息已经放到普通缓存区了,那么内核就能有的放矢,减少轮询普通缓存区的次数,从而提高效率.
selector就是包装了这个事先通知功能,使得内核每次查看普通缓存区都有收获,一并读取socket缓存区数据,进而配合channel把数据从socket缓存区写入用户数据或是网卡上.
4.3 小结
可见,nio网络部分的buffer都是socket缓存区的子集.对应的是,文件部分的buffer都是普通缓存区的子集,这里的道理和网络类似,不再详细阐述.
1 从合并的缓存区读数据流程
selector通知内核socket缓存区数据已到,内核查看普通缓存区进而通过地址信息一并读取socket缓存区(buffer)数据,调用channel把数据写到用户数据或网卡上.
2 向合并的缓存区写数据流程
selector通知内核socket缓存区(buffer)已准备好,内核借助Chennal向socket缓存区(buffer)写数据,按照写顺序事先已在普通缓存区存留了一份其地址信息.
5 总结
用户数据,内核,缓存区,socket缓存区和网卡,这些都是客观存在的io组成部分,java nio通过封装底层系统调用,通过减少拷贝次数的方式,完成了io过程.
这个过程是高效的,但是具体的操作并没有减少,而是增多了.