面试高频——零拷贝一文解决
零拷贝(zero-copy)是一种目前只有在使用NIO
和Epoll
传输时才可使用的特性。它使你可以快速高效地将数据从文件系统移动到网络接口,而不需要将其从内核空间复制到用户空间,其在像FTP
或者HTTP
这样的协议中可以显著地提升性能。但是,并不是所有的操作系统都支持这一特性。特别地,它对于实现了数据加密或者压缩的文件系统是不可用的——只能传输文件的原始内容。反过来说,传输已被加密的文件则不是问题。
简单来讲:
零拷贝并不是说不复制文件,而是从操作系统的角度来看,文件直接在内核空间进行拷贝传输,不用在用户空间进行拷贝传输。
二、大马(DMA
)没来之前——I/O传输
故事场景:
老板(代表用户)、CPU(代表项目经理)。
上班的时候,老板经常让项目经理去楼下对面马路上买水买烟。大夏天的,室外温度三四十度,项目经理每次帮老板买东西都要先做电梯下楼,然后跑到马路对面买东西,累得一身汗。但是没办法呀,以后升职加薪还得要看老板呀,这活不能不干呀。
在程序中的体现:
老板(用户)给CPU(项目经理)发了一个请求,你去马路对面的商店(磁盘)给我买包烟(数据)。
用一副图表示这个过程:
从图中可以看出,在买烟的整个过程中,项目经理(CPU)全权参与,累死累活,到最后可能烟都没得一根抽。
项目经理(CPU)想想,天天这样干还得了,不行不行,得想个办法解决这个问题!当新来的实习生大马(DMA
)从他身边走过的时候,项目经理盯着大马的背影微微一笑......
三、大马(DMA
)来了之后——I/O传输
DMA
:Direct Memory Access
直接内存访问技术。
这天,项目经理把大马单独叫进了房间,语重心长地对大马说,“大马呀,你刚毕业来我们公司,对我们职场的一些人情世故还不是很了解,后面转正的时候可能会吃亏呀......”。
经过了项目经理半天语重心长地交谈之后,从那天开始,每天在烈日下都能看到大马不辞辛苦来回奔波的身影。而项目经理呢,就可以在大马买烟的时候尽情地摸鱼玩手机了。他只要算好时间在电梯旁等候大马回来,然后再亲手把烟交给老板。既不辛苦,功劳还到手,这才是真正的打工赚钱,简直是一举两得呀。
用一幅图表示这个过程:
即:在进行I/O磁盘数据传输的时候,数据搬运的工作全部交给DMA
控制器来做,CPU
不再参与任何与数据搬运的工作。
四、传统的文件传输
传统的I/O
工作方式
代码会调用:
read(file, tem_buf, len);
write(socket, tem_buf, len);
即读取文件和写入文件。
在计算机中的调用过程:
由上图可以看出,代码在调用read
和write
方法的时候,一共发生了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
来替代零拷贝技术。
参考文章: