
传统的I/O操作读取文件并通过Socket发送,需要经过4次上下文切换、2次CPU数据拷贝和2次DMA控制器数据拷贝,如下图: ![]() 操作系统层面的减少数据拷贝次数主要是指用户空间和内核空间的数据拷贝,因为只有他们的拷贝是大量消耗CPU时间片的,而DMA控制器拷贝数据CPU参与的工作较少,只是辅助作用。 现实中对零拷贝的概念有广义和狭义之分,广义上是指只要减少了数据拷贝的次数就称之为零拷贝;狭义上是指真正的零拷贝,比如上例中避免2和3的CPU拷贝。 下面我们逐一看看他们的设计思想和实现方案 mmap内存映射既然是内存映射,首先来了解解下虚拟内存和物理内存的映射关系,虚拟内存是操作系统为了方便操作而对物理内存做的抽象,他们之间是靠页表(Page Table)进行关联的,关系如下 ![]() 每个进程都有自己的PageTable,进程的虚拟内存地址通过PageTable对应于物理内存,内存分配具有惰性,它的过程一般是这样的:进程创建后新建与进程对应的PageTable,当进程需要内存时会通过PageTable寻找物理内存,如果没有找到对应的页帧就会发生缺页中断,从而创建PageTable与物理内存的对应关系。虚拟内存不仅可以对物理内存进行扩展,还可以更方便地灵活分配,并对编程提供更友好的操作。 内存映射(mmap)是指用户空间和内核空间的虚拟内存地址同时映射到同一块物理内存,用户态进程可以直接操作物理内存,避免用户空间和内核空间之间的数据拷贝。 ![]() 它的具体执行流程是这样的 ![]()
sendfilesendfile是在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 gatherLinux2.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); 它的执行流程如下 ![]()
teetee与splice类同,但fd_in和fd_out都必须是管道。 写在最后 ![]() |