一篇文章让你10分钟就能玩懂“零拷贝和NIO”
Posted 一口Linux
tags:
篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了一篇文章让你10分钟就能玩懂“零拷贝和NIO”相关的知识,希望对你有一定的参考价值。
前言
相信大家应该对“零拷贝”这个词并不陌生了,这也算是大厂面试中的一个高频考点,玩过 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很重要的三个属性:position (指针当前位置),capacity (总容量),limit (读/写边界位置)
-
- selector(选择器):selector是一个特殊的组件,用于采集各个通道的状态(或者说事件)。我们先将通道注册到选择器,并设置好关心的事件,然后就可以通过调用select()方法,静静地等待事件发生。
通道有如下4个事件可供我们监听:
- Read:有数据可读
- Connect:连接成功
- Write:可以写入数据了
- Accept:有可以接受的连接
首先我们需要注册当这几个事件到来的时候所对应的处理器。然后在合适的时机告诉事件选择器:我对这个事件感兴趣。
也就是说,在选择器上注册了这四个事件的处理器,用来处理 channel 的事件,当 channel 某个事件真的准备就绪了,可以进行下一步的动作时,再告诉服务端来处理相应的数据,把相应的任务分配给服务端,这样就能更好的利用 cpu 的资源。
前面我们说的零拷贝,就是在这时数据处理时发生的。
NIO 和 IO 有什么区别?
- NIO是以缓冲区(块) 的方式处理数据,IO是以流的形式去写入和读出的。
- NIO 又是基于这种流的形式,采用了通道和缓冲区的形式来进行处理数据的
- NIO 的通道是可以双向的,但是 IO 中的流只能是单向的
- NIO 是以选择器的轮询机制来触发的, IO是收到信息即触发。所以它们读写触发方式不同
- NIO 的缓冲区可以进行分片,可以建立直接缓冲区、间接缓冲区和只读缓冲区,直接缓冲区是为加快 I/O 速度,而以一种特殊的方式分配其内存的缓冲区
总结
从传统 IO 模型 到 NIO 零拷贝模型我们可以看出,一个新技术的产生到崛起肯定是因为它能满足之前技术满足不了的需求,或者性能有很高的提升。
传统 IO 传输需要进行四次的数据内容拷贝,包括用户态和内核态的切换,数据载体(磁盘、网卡)和内核态的切换,整个过程是阻塞的,浪费了很多资源。
而 NIO 是通过通道,选择器等核心模块,将整个 IO 处理过程变为异步的方式,只有其数据任务真正就绪了,才会让 cpu 去做处理,大量的提高了性能,亦节省了资源。
零拷贝是使用了内存映射,做到了内核态和用户态数据的零拷贝。而不是让内核态和用户态之间的数据再通过拷贝的方式传输。而因为使用了内存映射的关系,所以零拷贝技术无法对数据内容做更改。
它的拷贝方式使用了 DMA 技术,目的就是为了解决 CPU 拷贝数据的方式,让拷贝数据不再占用 CPU 的资源,有 DMA 去完成。
以上是关于一篇文章让你10分钟就能玩懂“零拷贝和NIO”的主要内容,如果未能解决你的问题,请参考以下文章