基础知识-零拷贝技术

高级I/O函数

主要介绍一下Linux下的一些高级I/O函数,可以用于服务器的效率优化,这里介绍的API都是“零拷贝技术”的具体实现,可以直接直接跳到下一部分的对零拷贝技术的浅析

readv函数和writev函数

readv将数据从文件描述符读取到分散的内存块中,即分散读;writev函数将多块分散的内存数据一并写入文件描述符,即集中写。

#include ssize_t readv(int fd, const struct iovec* vector, int count);ssize_t writev(int fd, const struct iovec* vector, int count);

其中fd时被操作的目标文件描述符,vector是iovec结构数组(一个iovec描述一块内存区),count是vector数组的长度,即有多少块内存数据需要从fd读出or写到fd。

使用举例:

Web服务器中,当期解析完一个HTTP请求后,如果目标文档存在且客户具有读取该文档的权限,那么服务器需要返回一个HTTP应答来传输文档。HTTP应答包含1个状态行、多个头部字段、1个空行和文档的内容。前3部分可能被Web服务器放置在一块内存中,而文档内容通常被读入到另一块单独的内存中,通过writev函数就不需要把两部分内容拼接到一起再发送,而是直接同时写出。

sendfile函数

用于在两个文件描述符之间直接传递数据(完全在内核中操作),从而避免了内核缓冲区和用户缓冲区之间的数据拷贝,效率很高,也被称为

零拷贝,即从实际的文件中将内容直接拷贝到socket。

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

其中in_fd时待读出内容的文件描述符,out_fd是待写入内容的文件描述符,offset指定从读入文件流的哪个位置开始读(为空则使用默认的位置),count指定在两个文件描述符之间传输的字节数。

需要注意,in_fd必须是一个类似于mmap函数的文件描述符,即必须指向真实的文件,而不能是socket和管道,而out_fd则必须是一个socket,因此sendfile几乎是专门为在网络上传输文件而设计的。

使用sendfile就不需要为目标文件分配任何用户空间的缓存,也没有执行读取文件的操作,但是同样实现了文件的发送,效率显然高得多。

mmap函数和munmap函数

前者用于申请一段内存空间,可以作为进程间通信的共享内存,也可以将文件映射在其中。后者则为对应的释放空间的操作

#include void * mmap(void * start, size_t length, int prot, int flags, int fd, off_t offset);int munmap(void * start, size_t length);

start参数允许用户使用特定的地址作为这段内存的起始地址,如果被设置成NULL,则自动分配;

length指定内存段的长度;

prot用于设置内存段的访问权限,由后面的几个值按位或得出:

  • PROT_READ,内存段可读

  • PROT_WRITE,内存段可写

  • PROT_EXEC,内存段可执行

  • PROT_NONE,内存段不能被访问

flags参数控制内存段内容被修改后程序的行为,由以下几个值按位或得出:

  • MAP_SHARED

    在进程间共享这段内存,对该内存端的修改将反映到被映射的文件中

  • MAP_PRIVATE

    内存段为调用进程所私有。对其的修改不会反映到被映射的文件中。与上个选项互斥

  • MAP_ANONYMOUS

    这段内存不是从文件映射而来,内容全部初始化为0

  • MAP_FIXED

    内存段必须位于start参数指定的地址处,start必须是内存页面大小(4096字节)的整数倍

  • MAP_HUGETLB

    按照“大内存页面”来分配内存空间,“大内存页面”可以通过/proc/meminfo文件查看

fd参数是被映射文件对应的文件描述符;

offset参数设置从文件的何处开始映射;

splice函数

用于在两个文件描述符之间移动数据,也是零拷贝操作

#include ssize_t splice(int fd_in, loff_t * off_in, int fd_out, loff_t * off_out, size_t len, unsigned int flags);

fd_in是待输入数据的文件描述符,但是如果是管道的话那么off_in则必须为NULL(因为off_in表示从输入数据流的何处开始读取数据,此时为NULL,表示从当前偏移位置读入)。fd_out和off_out的含义大致与前面相同;

len参数指定移动数据的长度;

flags参数控制数据如何移动,从下面的值按位或得到:

  • SPLICE_F_MOVE(存在BUG)

    如果合适的话,按整夜内存移动数据

  • SPLICE_F_NONBLOCK

    非阻塞的splice操作,实际效果还是会受文件描述符本身的阻塞状态的影响

  • SPLICE_F_MORE

    给内核的一个提示,后续的splice调用将读取更多数据

  • SPLICE_F_GIFT

    对splice没有任何效果

使用splice函数时,fd_in和fd_out必须至少有一个是管道文件描述符。

举例:在写回射服务器的时候,通过splice函数将客户端的内容读入到pipefd[1]中,再使用splice函数从pipefd[0]中读取内容到客户端中,实现简单而高效的回射服务,因为没有使用revc和send操作,因此并未涉及用户空间和内核空间之间的数据拷贝,效率很高

tee函数

用于在两个管道文件描述符之间复制数据,也是零拷贝操作

其不消耗数据,源文件描述符上的数据仍然可以用于后续的读操作。

#include ssize_t tee(int fd_in, int fd_out, size_t len, unsigned int flags);

参数含义与splice相同,但是fd_in和fd_out都是管道文件描述符


零拷贝原理

传统操作系统的标准I/O是基于数据拷贝进行操作的,即I/O操作会导致数据在操作系统内核地址空间的缓冲区和应用程序地址空间定义的缓冲区之间进行传输,这样的缓冲区之间的操作实际上减少了磁盘I/O操作,因为数据在高速缓冲存储器中,就不需要再进行实际的物理磁盘I/O操作(注意这里所指的是物理磁盘,表示的是直接进行内存上的数据拷贝,而不是写入磁盘再从磁盘上读),但是这种内存之间的拷贝动作会导致极大的CPU开销。

零拷贝技术可以有效地改善数据传输的性能,当内核驱动程序处理I/O数据的时候可以减少甚至完全避免不必要CPU数据拷贝操作。

补充:Java中的零拷贝技术的方法是java.nio.channel.FileChannel的transferTo()以及transferFrom()方法,可以用这两个方法将bytes直接从调用它的channel传输到另一个writable byte channel中,中间不会让数据经过应用程序,提高数据转移的效率。它不像Linux的系统调用,有着好几个接口适用于不同的文件描述符情况,这两个方法可以实现所有种类的文件描述符的传输,即socket与普通的文件描述符都可以。而还有的readv和writev的相关功能的零拷贝技术,在Java的nio包中没有实现,而在Netty这个I/O框架库中有实现,通过ChannelBuffer接口的众多实现中的一个实现CompositeChannelBuffer类来使用此功能。

传统服务器进行数据传输的流程

一般来说,传输数据的时候,用户应用程序需要分配一块大小合适的缓冲区用来存放需要传输的数据。应用程序从文件中读出一块数据,填充这个缓冲区,然后把这块数据通过网络发送到接收端去。这样接口比较简单,只需要read()和write()这两个系统调用就可以完成这个数据传输操作,应用程序不会意识到这个过程中OS所作的数据拷贝操作。但是内核在某些情况下,数据拷贝工作会极大地降低数据传输的性能。

此外传统的数据传输过程中,数据至少发生了四次拷贝操作,如下图所示

基础知识-零拷贝技术

如果使用DMA操作,那么可以省去硬件设备与内核空间之间的拷贝的步骤,因为DMA操作可以解放与硬件通讯时的CPU消耗,即仍然需要访问数据两次,如下图所示

基础知识-零拷贝技术

什么是零拷贝技术

简单来说,零拷贝技术是一种避免CPU将数据从一块存储拷贝到另一块存储的技术,零拷贝技术可以消除数据在存储器之间不必要的中间拷贝次数,从而有效提高数据传输效率。可以概括如下

  • 避免数据拷贝

    包括内核缓冲区之间、内核与用户空间的拷贝,避开OS直接访问硬件存储,数据传输尽量让DMA来完成

  • 将多个操作结合在一起

    对于多个内存区域的访问,本来是需要调用多次的访问操作才可以,而readv和writev可以一次访问多个内存区域

零拷贝技术分类

一般包含以下几种:

  • 直接I/O

    应用程序直接访问硬件存储

  • 避免数据在OS 内核地址空间缓冲区和应用程序地址空间缓冲区之间拷贝

  • 对数据在Linux的页缓存和用户进程的缓冲区之间的传输过程进行优化

    侧重于灵活地处理数据在用户进程的缓冲区和OS页缓存之间的拷贝操作,主要利用CopyOnWrite技术

基础知识-零拷贝技术

  • 如果要传输的数据量比较大,那么直接I/O是可以提高性能,但是直接I/O的开销很大,不能利用缓存I/O的优势,读操作会造成磁盘的同步读,这个执行进程需要很长时间才能执行完成,写操作会造成应用程序关闭缓慢。因此应用程序使用直接IO进行数据传输通常和异步IO结合使用。

    目前执行的操作是,执行open()(打开文件)时指定O_DIRECT标识符。一般用于OS不需要对数据进行处理的情况

前面的两种主要为了避免应用程序地址空间和OS内核地址空间两者之间的缓冲区拷贝操作,通常适用于某些特殊情况,例如:传送的数据不需要经过OS内核的处理or不需要经过应用程序的处理。第三类的方法继承了传统的应用程序地址空间和OS内核地址空间之间数据传输的概念,即在传输效率上进行改善,而不是避免传输

具体方法

对于前面的第一种直接I/O没有什么好说的,第二种是比较常用的零拷贝技术,通常用的一些mmap()、sendfile()、splice()等都是这种零拷贝技术,下面的部分中具体介绍这些方法中如何实现“零拷贝”,对于第三种零拷贝技术并没有进行学习,所以这里只是简单提了下其中用到的技术。

1. mmap()/write()

利用mmap()代替read()。mmap()通过DMA拷贝数据到OS内核的缓冲区中,然后应用程序与OS共享这个缓冲区,于是OS内核和应用程序内核空间没有数据拷贝操作。如下图,需要消耗CPU的只有页缓存拷贝至socket缓冲区这一步操作,相比于原来所说传统的I/O已经减少了3步的CPU消耗。具体的拷贝次数,如果加上由DMA操作的磁盘到页缓存以及socket缓冲区到协议引擎中的过程,总共三次数据拷贝操作。

基础知识-零拷贝技术

基础知识-零拷贝技术

2.sendfile()

为了简化用户接口,保留原来mmap()、write()操作的优点,引入sendfile()系统调用。其不仅减少了数据拷贝操作,还减少了上下文切换,因为执行了sendfile后需要进入内核态将数拷贝到OS内核缓冲区,然后不返回用户态(原本的mmap/write是需要返回的),直接将此数据拷贝到socket缓冲区中,从而使用DMA将数据发送到协议引擎,因此减少了上下文的切换,但是也因此只使用于应用程序地址空间不需要对所访问数据进行处理的情况。

基础知识-零拷贝技术

上图这种情况并不是完美的sendfile(),因为可以看到其中存在了一次要消耗CPU的copy操作(DMA操作不消耗CPU),那么就不符合零拷贝这个名称了,那么更好的sendfile()如何实现“零拷贝”操作呢?

如果需要不拷贝,那么就是要解决页缓存拷贝到socket缓冲区的那一步,这里的实现选择将带有文件位置和长度信息的缓冲区描述符添加到socket缓冲区中,于是可以不需要进行那唯一的一次拷贝,DMA引擎会直接将数据从OS缓冲区拷贝到协议引擎中去。具体的实现原理如下引用,这里的原理描述上很像是readv和writev的原理,应该就是这两个系统调用的实现的根本。

为了避免操作系统内核造成的数据副本,需要用到一个支持收集操作的网络接口,这也就是说,待传输的数据可以分散在存储的不同位置上,而不需要在连续存储中存放。这样一来,从文件中读出的数据就根本不需要被拷贝到 socket 缓冲区中去,而只是需要将缓冲区描述符传到网络协议栈中去,之后其在缓冲区中建立起数据包的相关结构,然后通过 DMA 收集拷贝功能将所有的数据结合成一个网络数据包

基础知识-零拷贝技术

总结一下上面的几次优化过程,即如何从原本的4步拷贝操作变为“零拷贝”:1、通过DMA操作解决了硬件读取到OS内核缓冲区、内核协议栈缓冲区到网络接口卡的两步拷贝操作;2、通过直接mmap()映射减少了内核缓冲区到用户空间缓冲区的拷贝;3、最后在sendfile()中运用不让用户空间缓冲区操作数据以及readv、writev用到的支持收集操作的socket接口的技术,将最后的一次拷贝操作也避免掉,从而实现真正的零拷贝

3. splice()

与mmap()和sendfile()类似。适用于确定数据传输路径的用户应用程序,简单来说,前面的sendfile()只能适用于传输给socket网络接口(因为网络接口实现了数据收集操作),而这里的splice用于两个文件描述符之间的传递数据,不过需要注意,splice()中的两个文件描述符参数必须有一个为管道文件描述符。

不过需要注意的是splice本身代表了一种机制,即零拷贝的一种机制,而sendfile()实际上也是它的一个实现,不过上面的splice()这个系统调用引入中限制了两个文件描述符必须有一个为管道文件描述,所以这个设计上存在了局限性

优化应用程序地址空间和OS内核之间的数据传输的零拷贝技术

前面说过这种方法的优点在于不用免去OS内核空间对数据的操作or用户空间对数据的操作,其保留了传统在用户应用程序地址空间和OS内核地址空间之间传递数据的技术,但是在传输上进行优化,这个优化主要就是CopyOnWrite机制,此外还有着共享缓存的机制来实现等等。


分享到:


相關文章: