妹妹 10 分钟就玩懂了零拷贝和 NIO,也太强了
>微信公众号:moon 聊技术<br>
本文约 3500 字,完整阅读大概会花费你 10 分钟左右的时间<br>
[如果你觉得文章对你有帮助,欢迎关注,点赞,转发]
前言
'零拷贝'这个词大家应该不陌生了,也算是大厂面试中的一个高频考点,玩过 NETTY 的朋友应该对此相当熟悉了,NETTY 的高并发很大程度上都是因为 NIO,而 NIO 的核心就是零拷贝技术了,今天就让你十分钟玩懂零拷贝。
传统的 IO 模型是怎么样的?
我们来看一张图,让我们看看一个文件从磁盘传输到网卡究竟要经历什么样的磨难:
第一步:将文件通过 DMA 技术从磁盘中拷贝到内核缓冲区
第二步:将文件从内核缓冲区拷贝到用户进程缓冲区域中
第三步:将文件从用户进程缓冲区中拷贝到 socket 缓冲区中
第四步:将 socket 缓冲区中的文件通过 DMA 技术拷贝到网卡
这种数据存储的区域整体我们把它叫做非直接缓冲区。
我们发现,居然有四步数据拷贝的过程!!并且整个数据的传输过程都是需要 CPU 去执行的。
这个过程也太繁琐了,我就想传输一些数据,干嘛要传到用户这里,还要我自己再走一遍后续的流程,写到 socket 缓冲区再发出去,你不能帮我实现吗?
怎么去优化传统 IO 的流程呢?
我们继续看上面的流程图理一下,看看哪些步骤是可以去掉的
我们发现在整个过程中,数据从磁盘读出来到发送给网卡,文件内容都是*不会发生改变*的,但是我却要经历 4 次文件内容的拷贝才真正能将文件传输到网卡。
那么以最简单的的方式来说,能不能直接将磁盘中的数据传输到网卡呢?
当然不可以,这个原因也很简单,因为网卡和磁盘都是外部设备,所以一定要有一个中间的缓冲区域来取存储数据,做一个转发的作用。
那么我们看上图中能做缓冲的有两个区域,一个是 socket 缓冲区,一个是内核缓冲区,那么用哪一个?
这个问题应该很好选择了,socket 肯定不可以,socket 和我操作系统无瓜,那么只有用内核缓冲区来做缓冲区。
那么能不能通过内核缓冲区直接给网卡发送数据呢?
看样子是可以的,那么我们来看看,socket 缓冲区的作用是什么?
socket 缓冲区的作用
每个 socket 被创建后,都会分配两个缓冲区,输入缓冲区和输出缓冲区。
write()/send() 并不立即向网络中传输数据,而是先将数据写入缓冲区中,再由 TCP 协议将数据从缓冲区发送到目标机器。一旦将数据写入到缓冲区,函数就可以成功返回,不管它们有没有到达目标机器,也不管它们何时被发送到网络,这些都是 TCP 协议负责的事情。
所以 socket 就是用来传输网络数据的,看来没它还不行。
但是我们换个思路,是不是说,只需要告诉 socket 要传输哪些数据就可以了?然后文件内容就可以直接用内核缓冲区的就好了。
零拷贝(zero copy)是怎么做到性能提升的
当你读懂了上面的内容,基本上已经能摸到零拷贝的核心脉络了,其实零拷贝就是使用内存映射来消除数据拷贝次数的,然后使用 DMA 技术来减少 CPU 的工作时间。
就只从拷贝次数的性能来看,可以讲性能提高至少百分之五十以上。
DMA
上文中经常提到一个很重要的词汇 - DMA ,它在整个零拷贝的流程当中是有很大的占比的,能帮助 CPU 做大量的工作,我们来介绍一下这个神奇的技术。
DMA 就是直接存储器访问,DMA (Direct Memory Access,直接存储器访问) 是所有现代电脑的重要特色,它允许不同速度的硬件装置来沟通,而不需要依赖于 CPU 的大量中断负载。否则,CPU 需要从来源把每一片段的资料复制到暂存器,然后把它们再次写回到新的地方。在这个时间中,CPU 对于其他的工作来说就无法使用。
原理:DMA 传输将数据从一个地址空间复制到另外一个地址空间。当 CPU 初始化这个传输动作,传输动作本身是由 DMA 控制器来实行和完成。
零拷贝整体流程图
看到这里的话相信你对零拷贝已经有了深刻的理解,那么 NIO 到底是什么的?既然说了十分钟让你玩懂 NIO 和零拷贝,那 NIO 必不可少。
为什么需要 NIO ?
所有的系统 I/O 都分为两个阶段:
1.等待就绪
2.读写操作
需要说明的是等待就绪的阻塞是不使用 CPU 的,是在“空等”;而真正的读写操作的阻塞是使用 CPU 的,真正在”干活”,而且这个过程非常快,属于 memory copy,带宽通常在 1GB/s 级别以上,可以理解为基本不耗时。
我们先来看看传统 IO 是怎么做的
在传统的 socket IO 中,需要为每个连接创建一个线程。
一个线程对应一个连接,只处理一个连接的事情,这就是传统的 socket IO。
当并发的连接数量非常巨大时,线程所占用的栈内存和 CPU 线程切换的开销就会非常大。
在这种情境下还可能会出现线程数量小于连接数量的情况,所以每个线程进行 I O 操作时就不能阻塞,如果阻塞的话,有些连接就得不到处理。
如上图,假设有三条线程在管理三条连接,如果此时有第四个任务插入,那么就只能等待前面任务执行完成。
其操作就像是一条流水线一样,是串行阻塞的,故传统 IO 我们也称为 BIO。
传统 IO 也不知道什么时候该处理数据,所以只能一直傻等。
为了解决这些问题,NIO 就出现了。
NIO 是 怎么解决这些问题的?
我们先来介绍一下 NIO 的核心组件
channel(通道)
- 一个 channel(通道)代表和某一实体的连接,这个实体可以是文件、网络套接字等。也就是说,通道是 Java NIO 提供的一座桥梁,用于我们的程序和操作系统底层 I/O 服务进行交互
buffer(缓冲区)
- 你可以把它理解为存储数据的地方,buffer 很重要的三个属性
capacity (总容量),position (指针当前位置),limit (读/写边界位置)
selectors(选择器)
- selector(选择器)是一个特殊的组件,用于采集各个通道的状态(或者说事件)。我们先将通道注册到选择器,并设置好关心的事件,然后就可以通过调用 select()方法,静静地等待事件发生。
通道有如下 4 个事件可供我们监听:
Accept:有可以接受的连接
Connect:连接成功
Read:有数据可读
Write:可以写入数据了
我们首先需要注册当这几个事件到来的时候所对应的处理器。然后在合适的时机告诉事件选择器:我对这个事件感兴趣。
也就是说,在选择器上注册了这四个事件的处理器,用来处理 channel 的事件,当 channel 某个事件真的准备就绪了,可以进行下一步的动作时,再告诉服务端来处理相应的数据,把相应的任务分配给服务端,这样就能更好的利用 cpu 的资源。
前面我们说的零拷贝,就是在这时数据处理时发生的。
NIO 和 IO 有什么区别?
1.NIO 是以缓冲区(块) 的方式处理数据,IO 是以流的形式去写入和读出的。
2.NIO 又是基于这种流的形式,采用了通道和缓冲区的形式来进行处理数据的
3.还有一点就是 NIO 的通道是可以双向的,但是 IO 中的流只能是单向的
4.还有就是 NIO 的缓冲区还可以进行分片,可以建立只读缓冲区、直接缓冲区和间接缓冲区,直接缓冲区是为加快 I/O 速度,而以一种特殊的方式分配其内存的缓冲区
5.读写触发方式不同,NIO 是以选择器的轮询机制来触发的, IO 是收到信息即触发。
总结
从传统 IO 模型 到 NIO 零拷贝模型我们可以看出,一个新技术的产生到崛起肯定是因为其能满足之前技术满足不了的需求,或者相对于之前技术的性能有很高的提升。
传统 IO 传输需要进行四次的数据内容拷贝,包括内核态和用户态的切换,内核态和数据载体(磁盘、网卡)的切换,整个过程是阻塞的,过程浪费了很多资源。
而 NIO 是通过选择器,通道等核心模块,将整个 IO 处理过程变为异步的方式,只有其数据任务真正就绪了,才会让 cpu 去做处理,大量的节省了资源,提高了性能。
零拷贝就是让用户态和内核态之间的数据不再通过拷贝的方式传输,使用了内存映射,做到了内核态和用户态数据的零拷贝。
其拷贝方式使用了 DMA 技术,其目的就是为了解决 CPU 拷贝数据的方式,让拷贝数据这种累活不再占用 CPU 的资源,有 DMA 去完成。
因为是使用了内存映射的关系,所以零拷贝技术无法对数据内容做更改。
版权声明: 本文为 InfoQ 作者【moon聊技术】的原创文章。
原文链接:【http://xie.infoq.cn/article/a8c98c1f10ad33ef900afe218】。文章转载请联系作者。
评论