Bootstrap

【得物技术】零拷贝

本文说的零拷贝都是基于网络传输。

什么是零拷贝

零拷贝并不是不需要拷贝,而是减少不必要的拷贝次数。

传统IO流程

通常我们需要访问硬盘数据的时候,用户进程需要借助内核来访问硬盘的数据;用户通过调用系统方法,如read()、write()等方法通知内核,让内核做相应的事情。

read();

传统读取数据的流程:

在没有DMA之前的拷贝流程如上图所示:

以上流程中,涉及到数据的拷贝都需要CPU来完成,CPU是非常珍贵的资源,CPU在拷贝数据的时候,无法做其他的事情,如果传输的数据非常大,那么CPU一直在拷贝数据,无法执行其他工作,代价非常大。

DMA

本质上,DMA技术就是计算机主板上一块独立的芯片,当计算机需要在内存和 I/O 设备进行数据传输的时候,不再需要CPU来执行耗时的IO操作,而是通过DMA控制器来完成,流程如下。

上图可知,数据拷贝由DMA完成,CPU不需要在执行一些耗时的IO操作。

下图可以更形象的表达文件传输的过程:

步骤说明如下:

以上传统的IO数据拷贝在性能上有很大的提升空间,

由上图看出,在文件传输的案例中,我们将数据拷贝到用户数据缓冲区,用户进程没有经过任何数据处理,将文件直接发送出去。因此,这一个步骤是多余的,可以省略。

实现零拷贝

零拷贝的实现主要是针对上下文切换和拷贝的次数进行优化,通过减少上下文切换和减少数据拷贝的次数来达到优化的目的。

实现方式一:mmap(..) + write(..)

什么是mmap

mmap全称Memory Mapped Files,是一种内存文件映射的方法,将一个文件或者其他对象映射到进程的地址空间,实现文件磁盘地址和进程虚拟地址空间中的一段虚拟地址的一一映射关系,映射关系生成之后,用户进程可以通过指针操作内存中的文件数据,系统会自动将操作后的数据写入到磁盘中,而不需要调用read(),write()等系统调用来操作数据。

实现过程

使用mmap()函数替换read()函数,mmap将内核缓冲区中的数据映射到用户空间,用户空间与内核之间就不需要进行数据的拷贝,他们可以进行数据共享。

如图可以看出,数据不再拷贝到用户缓冲区

mmap减少了一次数据的拷贝,性能有所提升,但还是存在4次用户态和内核态的切换,并不是最理想的零拷贝。

如何减少上下文切换?

用户进程没有权限直接操作磁盘的数据,内核拥有上帝的权限,所以用户进程可以通过调用系统函数(如read,wirte)将任务交给内核来完成。

一次系统调用会发生两次上下文切换,先从用户态切换到内核态执行任务,任务执行完成后,从内核态切换到用户态,用户进程继续执行逻辑。

上下文的切回需要耗费时间,每次上下文切换耗费几纳秒到几微妙,看起来时间很短,但在并发下会成倍放大。

因此,我们需要减少上下文切换的次数,要减少上下文切换的次数,就需要减少系统函数调用的次数

实现方式二:sendfile 函数

Linux 2.1版本后提供了一个专门发送文件的系统调用函数sendfile(),函数如下:

ssize_t sendfile(int out_fd, int in_fd, off_t *offset, size_t count);

参数说明:

out_fd : 目的地端的文件描述符

in_fd:源端的文件描述符

offset: 源端的偏移量

count:复制的长度

返回实际复制数据的长度

sendfile函数用于代替readwrite两个函数,这样就可以减少一次系统调用,减少两次上下文切换的开销。

$ ethtool -k eth0 | grep scatter-gather
scatter-gather: on

以上过程只涉及到一次系统函数调用,2次上下文切换,2次DMA数据拷贝,不需要CPU拷贝数据,实现了真正的零拷贝。

mmap 和 sendfile 对比

  • 都是通过调用操作系统提供的API函数来实现

  • mmap为文件内存映射,用户进程对映射内存中的数据支持读和写操作,最终结果会反应在磁盘上

  • sendfile将数据读取到内核缓冲区之后,网卡通过SG-DMA控制器将数据复制过来

  • mmap实现零拷贝涉及两次系统函数调用,产生4次上下文切换,三次数据拷贝,不属于真正意义上的零拷贝

  • sendfile只有一次系统函数调用,产生2次上下文切换,2次必要的数据拷贝,实现了真正意义上的零拷贝

  • mmap优化更多的是在写请求上,sendfile更多是优化读请求

内核缓冲区(PageCache)

PageCache为磁盘的高速缓冲区,由于在磁盘中找数据是非常耗时的操作,所以将磁盘中的部分数据缓存到PageCache中,将读写磁盘的操作转换到内存中,提高读写的效率。

PageCache内存空间相比于磁盘来说小很多,所以我们不可能把所有的数据放到磁盘中,那么我们需要将什么数据读取到内存中,读取多大?

PageCache使用了预读功能,假如我们需要读取32kb的数据,但是加载到内存中的数据不只是32kb,它会以页为单位(每页64kb)来读取数据,所以不仅会读取0-32kb的数据,还会读取32-64kb的数据,这样32-64kb部分的数据读取的代价非常小,如果在内存淘汰前被进程使用到,收益非常大。

所以PageCache有两个主要的好处:

  • 缓存最近被访问的数据

  • 预读功能

说白了PageCache的诞生就是为了提高磁盘的读写性能。

总结

讨论

答:先说答案:异步IO + 直接IO这个情况我们应该想办法绕过PageCache,大文件不应该使用PageCache直接IO会直接绕过PageCacheIO的读取是阻塞的,可以考虑使用异步IO去代替

欢迎大家讨论。

文/木匠

关注得物技术,携手走向技术的云端