零拷贝(Zero-Copy)用于在数据读写过程中减少不需要的CPU拷贝,CPU就那么几个,减少它的负担自然可以提高处理效率。数据传输有本地的文件拷贝和通过socket进行文件传输两种,两者区别不大,只是文件数据最终的去向仍然是本地磁盘还是网卡的区别,这里以socket文件为例介绍传统IO演化至零拷贝的过程。
介绍零拷贝之前,可以先看一下传统IO,借此熟悉一些相关概念,先上图:
首先要知道操作系统已经隔离了两块运行空间,即用户空间和内核空间。可以理解为用户程序是跑在用户空间的,而操作系统的内核代码是跑在内核空间的,把这两个隔离是为了用户程序的故障不影响操作系统。其实现代操作系统已经对数据的拷贝做了优化,之前把数据从底层硬件拷贝到内核空间也是CPU来的,现在CPU只需要通知一下DMA(Direct Memory Access,直接内存存取),拷贝工作就交给DMA了,这样CPU就解放出来做其他事去了,所以现代操作系统底层硬件和内核空间之间的数据拷贝CPU参与的很少可以不予考虑,都是DMA来的,但是内核空间和用户空间之间的活都是CPU亲自上的。
从上图可以看出,传统IO是这么几个步骤:
1.线程在用户空间发起read()读文件,线程从用户态切换为内核态
2.DMA将磁盘数据拷贝到内核缓存后,CPU又将数据从内核缓存拷贝至用户缓存,这时线程又从内核态切换为用户态
3.这时候知道了数据应该往哪里写,CPU将数据从用户缓存拷贝至socket缓存,线程又从用户态切换到内核态
4.最后DMA将数据从内核缓存拷贝到网卡,read()调用结束返回,线程又从内核态切换到用户态
整个过程线程上下文切换了四次,一共有四次拷贝,2次CPU来的,2次DMA来的。观察图不经会想,为啥数据要在用户空间走一趟呢,能不能在内核空间直接从内核缓存到socket缓存呢,答案是可以的,这就是第一种零拷贝技术的原理,即mmap+write,先上图:
mmap即内存映射,mmap()是由unix/linux操作系统来调用的,它可以将内核缓存中的一块区域与用户缓存中的一块区域形成映射关系,即共享内存,不过在用户缓存中的这块映射区域是堆外内存。建立映射关系后,理解起来就是往其中任意一头写另外一头也写进去了,这样是为了省掉一次CPU拷贝,传统IO要把数据从内核缓存拷贝到用户缓存才能写,现在直接在用户缓存写,有了映射关系,对应的那块内核缓存也有了。mmap+write实现的零拷贝流程是这样的:
1.用户进程要读一个磁盘文件,告诉内核进程发起mmap()函数调用,来来来把你的内核缓存和我的一块用户缓存建立下映射关系,我要读这个磁盘文件了。
2.内核进程乖乖调用了mmap()函数,将一块内核缓存和用户缓存中的一块堆外内存建立的映射关系。并且告诉DMA将这个文件中的数据拷贝到了这块内核缓存中。到这里mmap()函数就调用结束了,任务完成。严格的说到这里为止都不算IO过程,因此也没有统计线程的上下文切换次数。
3.这才开始IO,因为磁盘文件已经被DMA拷贝到内核缓存中去了,又被映射到了这块堆外内存,所以就直接在用户缓存里就读到了,线程没有上下文切换,然后准备写进一块socket缓存里去了,线程发起了write()调用,状态由用户态切换为内核态,这时候内核基于CPU拷贝将数据从那块映射着的内核缓存拷贝到socket缓存,CPU也就拷贝了这一次。
4.然后又是DMA将数据从socket缓存拷贝到网卡,最后write()函数调用返回,线程从内核态切换到用户态。
整个过程线程切换了两次,一共有三次拷贝,其中2次DMA拷贝,1次CPU拷贝。到这里CPU已经轻松不少了,就拷贝了一次嘛,可以不是说好的零拷贝的嘛,怎么还有一次拷贝,然后sendfile()函数就登场了,它是实实在在的实现了零拷贝,先上图:
sendfile()也是操作系统来调用的,用户线程只能通过特定的方法发起调用,比如java.nio包下的FileChannel,它的transferTo()方法可以发起sendfile()函数的调用。sendfile()函数实现零拷贝的过程是这样的:
1.用户线程发起sendfile()函数调用,与mmap()函数不同的是,不单单告诉内核去哪里读数据,往哪里写数据也一起告诉内核了。这时候就已经开始算IO了,线程从用户态切换到了内核态。
2.知道了从哪里读数据,依然是DMA去磁盘里把数据拷贝到内核缓存中去,由于同时也知道了应该往哪里写数据,那就接着干活呗。
3.先把数据描述信息从内核缓存复制到指定的socket缓存,然后DMA又来了,这个时候socket缓存中的数据描述信息就起作用了,这些描述信息主要是数据的位置信息等。DMA Gather通过这些数据描述信息将数据从内核缓存拷贝到网卡。
4.sendfile()函数调用结束,线程从内核态切换到了用户态,CPU一次拷贝都没有!零!
这就是真正的零拷贝,整个过程用户线程切换了两次,只有两次拷贝,但都是DMA来的。
关于第三种零拷贝方式,这是Linux2.4对sendfile做了改进之后的零拷贝。其实linux 2.1 内核开始就引入了sendfile()函数,当时的零拷贝是这样的。
可以看出整个过程用户线程切换了两次,有三次拷贝,两次DMA来的,还是有一次CPU拷贝。这种零拷贝方式和mmap+write方式有点类似,但是这也算零拷贝演进过程中的一环。
sendfile()函数的man page里面有这句话: In Linux kernels before 2.6.33, out_fd must refer to a socket. Since Linux 2.6.33 it can be any file. 也就是说Linux2.6.33之前sendfile()只能用于文件到socket的传输。而Linux2.6.33之后可以用于两个文件描述符之间和文件到socket之间的传输。
全部0条评论
快来发表一下你的评论吧 !