零拷贝——如何高效的通过网络传输文件

传统文件传输方式

假如要传输320MB的文件,那么你可能会在应用程序分配32KB的内存空间,然后调用read函数从文件中读出32字节,最后调用write函数通过网络发生出去,其流程如下图所示。
在这里插入图片描述

这种方式非常低效,主要有如下的原因:
1、至少经历了4万次用户态与内核态的上下文切换。因为每处理32KB的消息,就需要一次read调用和一次write调用,每次系统调用都得先从用户态切换到内核态,等内核完成任务后,再从内核态切换回用户态。上下文切换的成本并不小,虽然一次切换仅消耗几十纳秒到几微秒,但高并发服务会放大这类时间的消耗。
2、4万次内存拷贝,对320MB文件拷贝的字节数也翻了4倍,到了 1280MB。因为每发送32KB的数据,都发生了4次内存拷贝,第一次从磁盘拷贝到内核的PageCache中,第二次从内核的PageCache拷贝到用户缓冲区中,第三次从用户缓冲区拷贝到内核的socket缓冲区中,最会一次从socket缓冲区拷贝到网卡的环形缓冲区中。

零拷贝技术

linux内核2.1开始引入一个叫sendfile系统调用,这个系统调用可以在内核态内把数据从内核缓冲区直接复制到套接字(SOCKET)缓冲区内,从而可以减少上下文的切换和不必要数据的复制

#include<sys/sendfile.h>
ssize_t senfile(int out_fd,int in_fd,off_t* offset,size_t count);
out_fd是写出的文件描述符,而且必须是一个socket
in_fd是读取内容的文件描述符,必须是一个真实的文件, 不能是管道或socket
offset是开始读的位置
count是将要读取的字节数

有了sendfile这个系统调用后, 传输文件变为:

1. 应用程序开始读文件的操作
2. 应用程序发起系统调用, 从用户态切换到内核态(第一次上下文切换)
3. 内核态中通过DMA把数据从硬盘文件读取到内核PageCache
4. 在内核态中把数据从内核PageCache复制到socket的缓冲区
5. 内核态中再通过DMA把数据从socket的缓冲区拷贝到网卡的环形缓冲区上
6. 从内核态切换到用户态(第二次上下文切换)

流程图如下:
在这里插入图片描述
从而使得320MB文件的传输,上下文切换变成2次,总共需要拷贝960MB。
在内核2.4以后的版本中, linux内核对socket缓冲区描述符做了优化。把数据直接从PageCache复制到网卡的环形缓冲区中(这个技术叫SG-DMA(The Scatter-Gather Direct Memory Access),注意网卡也需支持)。从而避免了从"PageCache"拷贝到"socket缓冲区"的这一次拷贝。从而传输文件的流程继续简化为

1. 应用程序开始读文件的操作
2. 应用程序发起系统调用, 从用户态切换到内核态(第一次上下文切换)
3. 内核态中通过DMA把数据从硬盘文件读取到内核PageCache
4. 内核态中通过DMA把数据从PageCache拷贝网卡的环形缓冲区上
5. 从内核态切换到用户态(第二次上下文切换)

流程图如下:
在这里插入图片描述
从而使得320MB文件的传输,上下文切换变成2次,总共需要拷贝640MB。

零拷贝技术不但通过减少上下文切换的次数与文件拷贝的次数,还使得我们不用管理一次发生的字节数。我们知道socket的发送缓冲区大小是动态变化的,如果应用程序的缓冲区如果设置的过小,那么上下文切换次数将会增多,但是如果设置的过大,我们调用write函数的时候,又不能一次全部写完。而零拷贝技术由于传输文件都在内核中进行,因此内核可以最大化的利用发送缓冲区的大小,从而最大化的利用网络。

通过如上的技术,零拷贝可以在很大的程度上提高网络传输速度,通常可以提高一倍以上。

大文件传输

那么有没有什么场景不适合使用零拷贝技术呢?当系统中存在多个应用程序读写小文件并且没有做资源隔离时,如果某个应用需要传输大文件,那么传输大文件时不应该使用零拷贝技术。主要原因如下:
从上面的图片可以看出,内核从磁盘读取文件时,都会先读取到PageCache中,根据时间局部性原理(刚被访问的数据在短时间内再次被访问的概率很高)与空间局部性原理(刚被访问的数据的后面的数据在很短的时间内再次被访问的概率很高),为此内核会使用LRU算法缓存PageCache的数据,同时读取磁盘的时候预读一些数据。那么对于大文件,由于动辄几个GB,第一次访问数据的时候,再次被访问的概率比较小。如果读取文件都进过PageCache那么内存将很快被PageCache占用完,那么将会触发LRU淘汰算法,从而导致其他小文件的缓存被淘汰出内存,导致缓存命中低效。为此在这个场景中应该使用直接IO,自然也不应使用零拷贝技术。

直接IO

直接 IO 的应用场景并不多,主要有两种:
1、应用程序已经实现了磁盘文件的缓存,不需要PageCache再次缓存,引发额外的性能消耗。比如 MySQL 等数据库就使用直接 IO;
2、高并发下传输大文件,我们上文提到过,大文件难以命中PageCache缓存,又带来额外的内存拷贝,同时还挤占了小文件使用PageCache时需要的内存,因此,这时应该使用直接 IO。
直接 IO 也有一定的缺点。除了缓存外,内核(IO 调度算法)会试图缓存尽量多的连续IO在PageCache中,最后合并成一个更大的IO发给磁盘,这样可以减少磁盘的寻址操作;另外,内核也会预读后续的 IO 放在PageCache中,减少磁盘操作。直接 IO 绕过了PageCache,所以无法享受这些性能提升。

  • 0
    点赞
  • 4
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值