前言

‘零拷贝’这个词大家应该不陌生了,也算是大厂面试中的一个高频考点,玩过 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 去完成。

因为是使用了内存映射的关系,所以零拷贝技术「无法对数据内容做更改」

分类: 微服务

0 条评论

发表回复

Avatar placeholder

您的电子邮箱地址不会被公开。 必填项已用*标注

此站点使用Akismet来减少垃圾评论。了解我们如何处理您的评论数据