今天就来深入了解一下零拷贝技术吧~四次数据拷贝,四次上下文切换很多应用程序在面对客户端请求时,可以相当于下面的系统调用:File.read(file,buf,len);Socket.send(socket,buf,len);比如消息中间件Kafka就是应用场景。从磁盘读取一批消息后,写入网卡(NIC,Networkinterfacecontroller)进行发送。在没有任何优化技术的情况下,操作系统会进行4次数据拷贝和4次上下文切换,如下图所示:如果没有优化,读取磁盘数据然后通过网卡传输的场景性能是相对较差:4份:CPU负责将数据从磁盘移动到内核空间的PageCache;CPU负责将数据从内核空间的Socket缓冲区移动到网络;CPU负责将数据从内核空间的PageCache移动到用户空间的缓冲区;CPU负责将数据从用户空间缓冲区移动到内核空间套接字缓冲区。4上下文切换:调用read系统时:从用户态切换到内核态;read系统调用完成后:从内核态切换到用户态;write系统调用时:从用户态切换到内核态;write系统调用后:切换回内核态用户态。我们不禁抱怨:CPU一直负责在内存中拷贝数据还可以,因为效率还可以,但如果要负责内存、磁盘、网络的数据拷贝在整个过程中,因为磁盘和网卡的速度要高得多,所以无法接受。比内存小,内存远小于CPU;4个副本太多了,4个上下文切换太频繁了。有DMA参与的四次数据复制DMA技术很容易理解。本质上,DMA技术就是我们在主板上放了一个独立的芯片。在内存和I/O设备之间传输数据时,我们不再通过CPU来控制数据传输,而是直接通过DMA控制器(DMAController,简称DMAC)来控制。这个芯片,我们可以把它想象成一个协处理器(Co-Processor)。DMAC最有价值的地方就是当我们要传输的数据特别大特别快,或者要传输的数据很小特别慢的时候。比如我们在使用千兆网卡或者硬盘传输大量数据的时候,如果用CPU来承载的话,肯定会忙不过来,所以可以选择DMAC。当数据传输很慢时,DMAC可以等待数据到达,然后发送信号给CPU进行处理,而不是让CPU在那里等待。请注意这里的“协会”一词。DMAC是在“协助”CPU完成相应的数据传输工作。在DMAC控制数据传输的过程中,我们仍然需要CPU来控制,但是具体数据的拷贝已经不再由CPU来完成了。本来,计算机各部件之间的数据拷贝(流)必须经过CPU,如下图所示:网卡。CPU作为DMA的控制器,如下图所示:但DMA也有其局限性。DMA只能用于设备之间交换数据时的数据拷贝,而设备内部的数据拷贝也需要CPU来完成。比如CPU需要负责内核空间和用户空间之间的数据。copy(内存里面的copy),如下图:上图中的readbuffer也就是pagecache,socketbuffer也就是Socketbuffer。零拷贝技术什么是零拷贝技术?零拷贝技术是指当计算机执行操作时,CPU不需要先将数据从某个内存复制到另一个特定区域。可以看出,零拷贝的特点是CPU不负责将内存中的数据写入其他组件,CPU只起到管理作用。但是要注意,零拷贝并不是说不进行拷贝,而是CPU不再负责数据拷贝的处理。如果数据本身不在内存中,首先要通过某种方式将其复制到内存中(CPU不需要参与这个过程),因为数据只有在内存中才能传输,并且可以由CPU直接读取和计算。零拷贝技术的具体实现方式有很多,例如:sendfilemmapspliceDirectDirectI/O不同的零拷贝技术适用于不同的应用场景。下面依次分析sendfile、mmap、DirectI/O。但是,出于总结的目的,我们在这里对以下技术进行前瞻性总结。DMA技术回顾:DMA负责内存与其他部件之间的数据拷贝,CPU只需要负责管理即可,不需要负责数据拷贝的整个过程;使用pagecache的零拷贝:sendfile:once代替read/write系统调用,通过使用DMA技术,传递文件描述符,实现零copymmap:只代替read系统调用,将内核空间地址映射到用户空间地址,以及写操作直接作用于内核空间。通过DMA技术和地址映射技术,用户空间和内核空间不需要拷贝数据,实现零拷贝DirectI/Owithoutpagecache:直接在磁盘上进行读写操作,不使用pagecache机制,通常结合与用户空间使用用户缓存。通过DMA技术直接与磁盘/网卡交互,实现零拷贝sendfilesnedfile的应用场景:用户从磁盘中读取一些文件数据,然后通过网络传输,不需要任何计算和处理。此场景的典型应用是消息队列。在传统I/O下,如第一节所示,上述应用场景中的一次数据传输需要四个CPU-only副本和四个上下文切换,如本文第一节所述。sendfile主要使用了两种技术:DMA技术;传输文件描述符而不是数据副本。下面依次说明这两种技术的作用。使用DMA技术,sendfile依靠DMA技术将CPU完全负责的4次拷贝和4次上下文切换减少为2次,如下图所示:CPU全权参与。DMA负责磁盘到内核空间的Pagecache(readbuffer)的数据拷贝和内核空间的socketbuffer到网卡的数据拷贝。传递文件描述符代替数据拷贝传递文件描述符可以代替数据拷贝,原因有二:页面缓存和套接字缓冲区在内核空间;数据传输过程前后没有写操作。用传递文件描述符代替内核中的数据复制注意:只有支持SG-DMA(分散-聚集直接内存访问)技术的网卡才能通过传递文件描述符避免内核空间中的CPU复制。也就是说这个优化取决于Linux系统的物理网卡是否支持(Linux在2.4内核版本中引入了DMA的scatter/gather——scatter/gather功能,只要确保Linux版本高于2.4即可)。一次系统调用,而不是两次系统调用因为sendfile只对应一次系统调用,传统的文件操作需要两次系统调用,read和write。正因为如此,sendfile可以将用户态和内核态的上下文切换从4次减少到2次。sendfile系统调用只需要两次上下文切换另一方面,我们需要了解sendfile系统调用的局限性。如果应用程序需要写入从磁盘读取的数据,例如解密或加密,那么sendfile系统调用就完全没用了。这是因为用户线程根本无法通过sendfile系统调用获取传输的数据。mmapmmap技术在本文[1]中单独开发,请循序渐进阅读。DirectI/ODirectI/O代表直接I/O。其名称中的“direct”一词用于区分使用页面缓存机制的缓存I/O。缓存文件I/O:用户空间读写文件,不直接与磁盘交互,而是一层缓存,即页缓存;directfileI/O:用户空间读取的文件直接与磁盘交互,没有中间的pagecache层。“Direct”在这里还有另外一层语义:在所有其他技术中,数据都需要在内核空间至少存储一份,但在DirectI/O技术中,数据绕过内核,直接存储在用户空间。DirectI/O方式如下图所示:DirectI/O示意图此时,用户空间通过DMA直接向磁盘和网卡拷贝数据。DirectI/O的读写很有特点:写操作:由于不使用pagecache,所以写文件。如果返回成功,数据就真正放到了磁盘上(不管磁盘自带的缓存是什么);读操作:因为没有使用pagecache,所以每次读操作实际上都是从磁盘中读取,而不是从文件系统的缓存中读取。事实上,即使是DirectI/O也可能仍然需要使用操作系统的fsync系统调用。为什么?这是因为虽然文件的数据本身没有使用任何缓存,但是文件的元数据还是需要缓存的,包括VFS中的inode缓存和dentry缓存。在某些操作系统中,DirectI/O模式下的write系统调用可以保证文件数据写入磁盘,但文件元数据不一定写入磁盘。如果在这样的操作系统上,还需要一个fsync系统调用来确保文件元数据也被刷新。否则可能会导致文件异常、元数据不一致等问题。MySQL的O_DIRECTvs.O_DIRECT_NO_FSYNC配置是一个特例。DirectI/O的优缺点:优点:Linux中的DirectI/O技术省略了bufferedI/O技术中操作系统内核缓冲区的使用,数据直接在应用程序地址空间和磁盘之间传输,从而使得Self-caching应用程序可以省去复杂的系统级缓存结构,实现程序自身定义的数据读写管理,从而减少系统级管理对应用程序访问数据的影响。与其他零拷贝技术一样,避免了将数据从内核空间复制到用户空间。如果传输的数据量很大,直接I/O方式进行数据传输,不需要操作系统内核地址空间来进行数据拷贝操作。参与,这将大大提高性能。缺点:由于设备间的数据传输是通过DMA来完成的,所以用户空间的数据缓冲内存页必须是pagepinned(pagelocked),这是为了防止其物理页框地址被交换到磁盘或移动到新的address导致DMA拷贝数据找不到指定地址的内存页,从而导致pagefault,而pagelocking的开销不小于CPU拷贝,所以为了避免频繁的pagelockingsystemcalls,应用程序必须为数据缓冲分配并注册一个持久内存池。如果访问的数据不在应用程序缓存中,那么每次都会直接从磁盘加载数据,这种直接加载会很慢。在应用层引入直接I/O需要应用层自己管理,这就引入了额外的系统复杂度。谁将使用直接I/O?一篇IBM文章[2]指出,自缓存应用程序可以选择使用直接I/O。自缓存应用对于一些应用,它会拥有自己的数据缓存机制,比如它会在应用地址空间缓存数据,这样的应用根本不需要使用操作系统内核中的缓存内存,这样的应用被称为自缓存应用程序(self-cachingapplications)。例如,应用程序维护一个缓存空间。当有读操作时,首先读取应用层缓存的数据。如果没有,则通过DirectI/O直接通过磁盘I/O读取数据。还是应用了缓存,但是应用觉得自己实现一个缓存比操作系统的缓存效率更高。数据库管理系统是此类应用程序的代表。自缓存应用程序倾向于使用数据的逻辑表示而不是物理表示;当系统内存不足时,自缓存应用程序会导致此类数据的逻辑缓存被换出,而不是磁盘上的实际数据被换出。自缓存应用程序完全了解它所操作的数据的语义,因此它可以使用更有效的缓存替换算法。一个自缓存应用可能会在多台主机之间共享一块内存,所以自缓存应用需要提供一种机制可以有效的使用户地址空间中的缓存数据失效,从而保证应用地址空间缓存数据一致性。另一方面,目前Linux上依赖文件以O_DIRECT方式打开的异步IO库,经常一起使用。如何使用直接I/O?用户应用程序需要在用户空间实现一个缓冲区,读写操作应该尽可能通过这个缓冲区来提供。如果有性能方面的考虑,尽量避免基于DirectI/O的频繁读写操作。典型案例KakfaKafka是一个消息队列,涉及到磁盘I/O的操作主要有两个:Provider向Kakfa发送消息,Kakfa负责将消息持久化到日志中;Consumer从Kakfa拉取消息,Kafka负责从磁盘读取一批日志消息,通过网卡发送。Kakfa服务器在接收Provider消息并持久化的场景下使用了mmap机制,可以提供基于顺序磁盘I/O的高效持久化能力。使用的Java类是java.nio.MappedByteBuffer。sendfile机制用于Kakfa服务器向Consumer发送消息的场景。这种机制有两个主要优点:sendfile避免了从内核空间到用户空间CPU负责整个过程的数据移动;sendfile是基于PageCache实现的,所以如果有多个consumer同时消费一个topic的消息时,由于消息一直缓存在pagecache中,所以只用一个磁盘I/O就可以服务多个consumer。使用mmap持久化接收到的数据,使用sendfile从持久化介质中读取数据然后发送到外部是一种常见的组合。但是注意不能使用sendfile来持久化数据,而使用mmap来实现CPU全程不参与数据处理的数据拷贝。MySQLMySQL的具体实现要比Kakfa复杂的多,因为本身支持SQL查询的数据库比消息队列对要复杂的多。总结DMA技术的引入,使得内存和磁盘、网卡等其他部件复制数据,CPU只需要发送一个控制信号,复制数据的过程就由DMA完成。Linux零拷贝技术的实现策略有很多,但是按照策略可以分为以下几种:减少甚至避免用户空间和内核空间之间的数据拷贝:在某些场景下,用户进程不需要数据被访问和处理,因此可以完全避免LinuxPageCache和用户进程缓冲区之间的数据传递,数据拷贝可以完全在内核中进行,甚至可以避免在内核中进行巧妙的方式。数据拷贝。这种实现一般是通过增加新的系统调用来实现的,比如Linux中的mmap()、sendfile()、splice()等。DirectI/Obypassingthekernel:允许用户态进程绕过内核,直接向硬件传输数据。内核只负责传输过程中的一些管理和辅助工作。这种方法其实和第一种有点类似,也是尽量避免用户空间和内核空间之间的数据传输,但是第一种方法是在内核态完成数据传输过程,而这种方法直接绕过了Kernel和硬件通信,效果类似但原理完全不同。内核缓冲区和用户缓冲区之间的传输优化:这种方法侧重于优化用户进程缓冲区和操作系统页面缓存之间的CPU副本。这种方式延续了过去传统的通讯方式,但更加灵活。
