上一篇推文《百万并发「零拷贝」技术系列之初探门径》中的示例告诉我们:传统的I/O操作读取文件并通过Socket发送,需要经过4次上下文切换、2次CPU数据拷贝和2次DMA控制器数据拷贝,如下图
从中也可以看得出提高性能可以从减少数据拷贝和上下文切换的次数着手,在Linux操作系统层面上有4种实现方案:内存映射mmap、sendfile、splice、tee,这些实现中或多多少的减少数据拷贝次数或减少上下文切换次数。
操作系统层面的减少数据拷贝次数主要是指用户空间和内核空间的数据拷贝,因为只有他们的拷贝是大量消耗CPU时间片的,而DMA控制器拷贝数据CPU参与的工作较少,只是辅助作用。
现实中对零拷贝的概念有广义和狭义之分,广义上是指只要减少了数据拷贝的次数就称之为零拷贝;狭义上是指真正的零拷贝,比如上例中避免2和3的CPU拷贝。
下面我们逐一看看他们的设计思想和实现方案
mmap内存映射
既然是内存映射,首先来了解解下虚拟内存和物理内存的映射关系,虚拟内存是操作系统为了方便操作而对物理内存做的抽象,他们之间是靠页表(Page Table)进行关联的,关系如下
每个进程都有自己的PageTable,进程的虚拟内存地址通过PageTable对应于物理内存,内存分配具有惰性,它的过程一般是这样的:进程创建后新建与进程对应的PageTable,当进程需要内存时会通过PageTable寻找物理内存,如果没有找到对应的页帧就会发生缺页中断,从而创建PageTable与物理内存的对应关系。虚拟内存不仅可以对物理内存进行扩展,还可以更方便地灵活分配,并对编程提供更友好的操作。
内存映射(mmap)是指用户空间和内核空间的虚拟内存地址同时映射到同一块物理内存,用户态进程可以直接操作物理内存,避免用户空间和内核空间之间的数据拷贝。
它的具体执行流程是这样的
总结
sendfile
sendfile是在linux2.1引入的,它只需要2次上下文切换和1次内核CPU拷贝、2次DMA拷贝,函数原型
ssize_t sendfile(int out_fd, int in_fd, off_t *offset, size_t count);
out_fd为文件描述符,in_fd为网络缓冲区描述符,offset偏移量(默认NULL),count文件大小。
它的内部执行流程是这样的
总结
sendfile+DMA gather
Linux2.4对sendfile进行了优化,为DMA控制器引入了gather功能,就是在不拷贝数据到网络缓冲区,而是将待发送数据的内存地址和偏移量等描述信息存在网络缓冲区,DMA根据描述信息从内核的读缓冲区截取数据并发送。它的流程是如下
总结
但那时的sendfile有个致命的缺陷,如果你查看Sendfild手册,你会发现如下描述
in_fd不仅仅不能是socket,而且在2.6.33之前Sendfile的out_fd必须是socket,因此sendfile几乎成了专为网络传输而设计的,限制了其使用范围比较狭窄。2.6.33之后out_fd才可以是任何file,于是乎出现了splice。
splice
鉴于Sendfile的缺点,在Linux2.6.17中引入了Splice,它在读缓冲区和网络操作缓冲区之间建立管道避免CPU拷贝:先将文件读入到内核缓冲区,然后再与内核网络缓冲区建立管道。它的函数原型
ssize_t splice(int fd_in, loff_t *off_in, int fd_out, loff_t *off_out, size_t len, unsigned int flags);
它的执行流程如下
总结
tee
tee与splice类同,但fd_in和fd_out都必须是管道。
写在最后
各种I/O方案总结对比如上,下一篇将带你感受下Java的零拷贝方案及实现,敬请关注。
End
版权归@码农神说所有,转载须经授权,翻版必究