深入理解netty零拷贝

首先来看一次网络I/O,使用传统的方式 (如图)发生了哪些系统调用和数据的copy

1.用户空间会发生一次系统调用,调用操作系统的read()方法请求磁盘上的数据
2.操作系统收到调用后,到磁盘去读数据,期间发生上下文的切换(由用户态到内核态的切换)
3.数据通过DMA(Direct Memory Access,直接内存存取)copy数据到kernel buffer(即内核缓冲区)
4.然后用户进程再将内核缓冲区的数据copy到user buffer(用户缓冲区),期间发生上下文切换
5.数据在用户缓冲区操作完毕后,又发生一次系统调用,调用write()去将数据copy回kernel buffer,期间再次上下文切换
6.将kernel buffer的数据copy到socket buffer
7.协议引擎从socket buffer读数据,并完成发送
8.调用完成,write()方法结果返回,期间发生上下文的切换(由内核态到用户态的切换)

深入理解netty零拷贝

由此过程可知,采用传统的调用方式,整个过程发生了三次内存copy(内存内部发生了三次,DMA两次)和四次上下文的切换(即内核态和用户态之间的切换),在频繁的I/O情况下,这种调用的效率明显是很低的,其实这些copy和切换其实是可以避免掉的,那么通过什么方式可以减少这些不必要的copy和切换呢?
答案是零拷贝

那么下面我们来看一下零拷贝的过程是怎样的,又是如何提升效率的(如图)
深入理解netty零拷贝

上图的调用过程大致分为:

1.用户空间发生一次系统调用,即调用sendfile()方法请求数据
2.操作系统收到调用后,到磁盘去读数据,期间发生上下文的切换(由用户态到内核态的切换)
3.数据通过DMA(Direct Memory Access,直接内存存取)copy数据到kernel buffer(即内核缓冲区)
4.数据直接从kernel buffer copy到了socket buffer
5.协议引擎从socket buffer读数据,并完成发送
6.sendfile()方法完成结果返回,期间发生上下文的切换(由内核态到用户态的切换)

可以看到上述方式,减少了两次copy(内核和用户之间的copy)和两次上下文的切换,效率提升不少
但这种方式依然不是最优的,在linux2.4之后又加了一些重要的改进,真正意义上的实现了零拷贝

下面我们来看一下做了什么改进
深入理解netty零拷贝

可以看到改进的点主要在数据从内核缓冲区到socket buffer的一次copy没有了,通过scatter/gather收集的操作去掉了这一次copy

下面来看下零拷贝的时序图
深入理解netty零拷贝

改进的点在数据从磁盘copy到kernel buffer时,这里会将一个kernel buffer数据的文件描述符写到socket buffer,注意这里不涉及数据本身的copy,只记录了文件描述符(指针而已),这个文件描述符记录了两个信息(一个是kernel buffer 数据的内存地址,一个是数据的长度),协议引擎这时在发送数据的时候,会从两个buffer中收集数据,即kernel buffer(存储数据本身),及socket buffer(数据的地址),从两个buffer中读取信息,就是一种gather操作(收集方式),在数据收集完成后将数据发送到远端。

这个时候我们可以看到整个过程其实只发生了DMA的copy,及两次上下文切换,真正意义上实现了内存的零拷贝
netty 的NIO采用就是这种零拷贝方式,也是netty之所以如此高效的很重要的原因。

ok,先介绍到这里,其中涉及很多操作系统的知识(ganther…DMA等)还需要去具体了解学习下。