面试高频——零拷贝一文解决


一、写在最前

零拷贝(zero-copy)是一种目前只有在使用NIOEpoll传输时才可使用的特性。它使你可以快速高效地将数据从文件系统移动到网络接口,而不需要将其从内核空间复制到用户空间,其在像FTP或者HTTP这样的协议中可以显著地提升性能。但是,并不是所有的操作系统都支持这一特性。特别地,它对于实现了数据加密或者压缩的文件系统是不可用的——只能传输文件的原始内容。反过来说,传输已被加密的文件则不是问题。

简单来讲:

零拷贝并不是说不复制文件,而是从操作系统的角度来看,文件直接在内核空间进行拷贝传输,不用在用户空间进行拷贝传输。

二、大马(DMA)没来之前——I/O传输

故事场景:

老板(代表用户)、CPU(代表项目经理)。

上班的时候,老板经常让项目经理去楼下对面马路上买水买烟。大夏天的,室外温度三四十度,项目经理每次帮老板买东西都要先做电梯下楼,然后跑到马路对面买东西,累得一身汗。但是没办法呀,以后升职加薪还得要看老板呀,这活不能不干呀。

在程序中的体现:

老板(用户)给CPU(项目经理)发了一个请求,你去马路对面的商店(磁盘)给我买包烟(数据)。

用一副图表示这个过程:

从图中可以看出,在买烟的整个过程中,项目经理(CPU)全权参与,累死累活,到最后可能烟都没得一根抽。

项目经理(CPU)想想,天天这样干还得了,不行不行,得想个办法解决这个问题!当新来的实习生大马(DMA)从他身边走过的时候,项目经理盯着大马的背影微微一笑......

三、大马(DMA)来了之后——I/O传输

DMADirect Memory Access 直接内存访问技术。

这天,项目经理把大马单独叫进了房间,语重心长地对大马说,“大马呀,你刚毕业来我们公司,对我们职场的一些人情世故还不是很了解,后面转正的时候可能会吃亏呀......”。

经过了项目经理半天语重心长地交谈之后,从那天开始,每天在烈日下都能看到大马不辞辛苦来回奔波的身影。而项目经理呢,就可以在大马买烟的时候尽情地摸鱼玩手机了。他只要算好时间在电梯旁等候大马回来,然后再亲手把烟交给老板。既不辛苦,功劳还到手,这才是真正的打工赚钱,简直是一举两得呀。

用一幅图表示这个过程:

即:在进行I/O磁盘数据传输的时候,数据搬运的工作全部交给DMA控制器来做,CPU不再参与任何与数据搬运的工作。

四、传统的文件传输

传统的I/O工作方式

代码会调用:

read(file, tem_buf, len);
write(socket, tem_buf, len);

即读取文件和写入文件。

在计算机中的调用过程:

由上图可以看出,代码在调用readwrite方法的时候,一共发生了4次的CPU上下文切换和4次的数据拷贝。

只是简单传输一份数据,居然发生了4次的数据拷贝,多了很多不必要的开销,严重影响程序的性能。

优化方法:

要想优化程序传输数据的性能,首先想到的就是减少数据在传输过程中搬运的次数。就像你在商店里买一包纸,售货员还需要先给老板过目,老板过目之后还需要给老板娘过目。但是,我只是想简单买一包纸而已,售货员完全可以直接把纸给我就好了,不需要把这包纸一层一层不必要的传递。

所以,要想提高文件的传输性能,就需要减少cpu在用户态与内核态之间的来回切换,和数据在传输过程中不必要的拷贝次数。

五、read + write变换——层层减少数据拷贝次数

read + write ——> mmap + write

代码调用:

buf = mmap(file, len)
write(sockfd, buf, len)

核心:用户态进程与内核态进程共用一个缓冲区

如图,通过 mmap 代替 read ,数据拷贝次数减少一次。但是,系统还是两次调用,所以cpu还是需要进行4次上下文的切换。

现在买纸不需要老板的过目了,买纸的效率快了点,但是能不能再快呢,往下看。

read + write ——> sendfile

代码调用:

#include 
ssize_t sendfile(int out_fd, int in_fd, off_t *offset, size_t count);

核心:程序只需要调用一次

如图,系统程序只需要调用一次,首当其冲的就是减少两次cpu的上下文切换。同样,数据不需要再拷贝到用户态,数据拷贝次数减少一次。

现在,买纸也不需要经过上层的老板娘了,直接找售货员就可以了。

但是,还能不能再快呢,可以,继续往下看。

六、网卡SG-DMA技术的出现

有需求就会有发展,为了更快的进行数据传输,从 Linux 内核 2.4 版本开始起,网卡 SG-DMA 技术出现了。

核心:需要设备硬件能跟上

如图,在原来的基础上,没有在内存层面去进行数据的拷贝。即,在数据传输的过程中,数据全是通过DMA来进行搬运的,cpu不需要进行数据搬运了。这就是开头所说的零拷贝技术。零拷贝不是说不进行数据的拷贝,而是在操作系统的角度来看,不需要cpu来参与数据的拷贝。

相比于传统的数据传输,这里只需要进行2次上下文的切换,2次数据的拷贝,把文件传输的性能提升了一半。

七、大文件传输与小文件传输

首先明确一点,并不是所有文件传输都使用零拷贝。尤其是在传输大文件的时候。

什么?大文件还不用零拷贝,文件越大,传输的数据越多,不是更应该减少数据的拷贝次数吗?不是更应该使用零拷贝技术吗?疑问三连。

原因:PageCache的存在

首先来看之前上面出现的一张图

图中PageCache是用来做什么的呢?数据为什么要在这里做一次周转呢?

这里的PageCache相当于是一处缓存,给数据传输提供预读的功能

对照现实场景:

你有没有发现在超市柜台结账的时候,柜台旁边小货架上都会放一些小东西,口香糖、打火机、餐巾纸、棒棒糖之类的。

你可以把柜台旁边的小货架想象成PageCache,在街上闲逛的时候,突然肚子痛想去上厕所,你急忙跑到超市要买一包餐巾纸,这个时候你就可以直接在这个小货架(PageCache)上直接购买,而不是需要到超市里一顿乱找。即,在数据传输的时候,传输一些小文件的时候,你可以使用到零拷贝。

但是,当你要传输大文件的时候呢,同样是到超市购买东西。这次你需要购买10箱可乐,但是这时候有两个问题出现:

1.货架太小,放不下10箱可乐,所以你就需要多次从超市里面先把可乐搬到这个小货架,然后一次一次从这个小货架消费,共消费10箱。弊端:过程中明显有一步操作多余,你可以直接拿个小推车把10箱可乐付钱装箱运走,而不需要先一次一次搬到小货架上,这个过程可以省略,效率提高。

2.就算有个别超市有钱,小货架可以装很多的东西,10箱可乐也可以装。但是有个顾客过来,他想要买的是10箱雪碧,这样你10箱可乐搬过来没人买,白白浪费了这个搬运的过程。

所以,对于这种大文件的传输,使用直接I/O来进行。

什么是直接I/O,简单来说就是绕过PageCache,直接装好你要的数据拉走。

不需要 PageCache再次缓存,减少过程中不必要的额外性能损耗。在 MySQL 数据库中,可以通过参数设置开启直接I/O,默认是不开启。因此,在高并发的场景下,针对大文件的传输的方式,应该使用 直接I/O来替代零拷贝技术

 

参考文章:https://blog.csdn.net/qq_34827674/article/details/108756999?ops_request_misc=%257B%2522request%255Fid%2522%253A%2522165500471416782184693372%2522%252C%2522scm%2522%253A%252220140713.130102334.pc%255Fblog.%2522%257D&request_id=165500471416782184693372&biz_id=0&utm_medium=distribute.pc_search_result.none-task-blog-2~blog~first_rank_ecpm_v1~rank_v31_ecpm-1-108756999-null-null.article_score_rank_blog&utm_term=%E9%9B%B6%E6%8B%B7%E8%B4%9D&spm=1018.2226.3001.4450