读懂零拷贝是什么

zero copy实现高效的数据传输

许多web应用系统都会向用户提供大量的静态内容,这也就是说会有大量地从磁盘读取文件数据,并把读取后的数据写回到响应套接字中。这个活动似乎看起来几乎不涉及到CPU计算,但是它却有点低效:系统内核从磁盘读取数据,并借由内核空间-用户空间的切换把数据推送给应用系统,之后应用系统又借由内核空间-用户空间的切换把数据写出到套接字。从实际上来看,在把数据从磁盘文件传输到套接字的过程中,应用系统其实是一个无效的中间媒介。

数据每次在用户空间-内核空间移动时,它都需要被拷贝,而这样就消耗了cpu的周期和内存的带宽。不过幸运地是,你可以通过zero copy技术来消除这些无效的拷贝操作。利用zero copy的系统可以直接请求内核把数据直接从磁盘文件复制到套接字,而无需经由应用系统。零拷贝(zero copy)极大地提供了应用性能,并减少了在内核空间和用户空间的切换次数。

Java类库对于Linux和UNIX上的零拷贝支持是通过 java.nio.channels.FileChannel类的transferTo()方法来实现的。你可以使用transferTo()方法直接把一个channel中的字节数据传输到另一个可写的字节channel中,而无需数据流经应用系统。本文首先将展示一下使用传统的拷贝语义来完成文件传输时所产生的消耗,之后再展示一下使用transferTo()的零拷贝技术是如何实现更高性能的。

数据传输: 传统的做法

设想一下这样一个场景: 读取一个文件,并通过网络把文件中的数据传输到另一个程序中。这个操作的核心就是代码示例1中的俩个调用。

代码示例1:把文件中的字节复制到套接字

  File.read(fileDesc, buf, len);
  Socket.send(socket, buf, len);

虽然代码示例1比较简单,但是其内部的拷贝操作却需要在用户空间和内核空间进行四次上下文切换,并且数据需要被复制四次。图1展示了在系统内部数据是如何从文件中被移动到套接字中的。

传统数据拷贝方法

Figure 1. Traditional data copying approach

传统的上下文切换

Figure 2. Traditional context switches

上面的图中涉及到的步骤有:

  • 1、read()调用导致上下文从用户模式切换到内核模式。其内部,sys_read()会从一个文件中读取数据。第一次的拷贝操作是由DMA(direct memory access)引擎执行的,它会从磁盘中读取文件内容,并把它们存储到内核地址空间缓存中。

  • 2、大量数据从read buffer中拷贝到用户空间地址缓存中,read()调用结束并返回。read()调用返回之后会导致另一个上下文切换---从内核模式切换到用户模式。此时,数据被存储在了用户地址空间缓存中。

  • 3、之后,send()套接字调用又导致了一次上下文切换-从用户模式到内核模式。执行第三次拷贝操作,此数据被再次放入内核地址空间。这次,数据被放入了一个不同的buffer中,此buffer和一个目地套接字相关。
  • 4、send()系统调用返回,第四次上下文切换发生。当DMA引擎把内核中的数据传递到协议引擎时,发生了第四次拷贝。

内核buffer这个中间媒介的使用似乎看起来是低效的。但是,内核buffer当初作为一个中间媒介被引入这个过程却是为了提供性能的。当应用系统请求的数据不超过内核缓存所能容纳的大小的时候,在读操作的一端,使用内核这个中间媒介使得内核buffer可以起到“readahead cache”的作用。这在所请求数据远小于内核buffer的情况下,可以极大地性能。在写操作的一端,内核这个中间媒介可以实现异步写入。

不幸的是,如果请求的数据远比内核缓存大的情况下,这种方法本身也可能导致性能瓶颈。数据在被最终传送应用系统之前,在磁盘、内核buffer、用户buffer之间进行了多次拷贝操作。

通过消除这些冗余的数据拷贝,零拷贝可以极大地提高性能。

数据传输: 零拷贝方法

如果你重新检查一下上面一个传统的场景,你将发现第二次和第三次的数据拷贝其实是不必要的。应用系统其实就是在缓存数据,并把缓存的数据
写入到套接字。反之,数据可以被直接地从read buffer中传输到套接字buffer中。transferTo()方法可以帮助你做到这一点。

示例代码2: transferTo()方法

public void transferTo(long position, long count, WritableByteChannel target);

transferTo()方法可以直接把数据从一个file channel中传输到一个给定的writable byte channel中。内部的实现取决于底层的操作系统对于
零拷贝的支持。在UNIX和各种Linux系统中,transferTo调用会被路由到sendfile()系统调用,就像示例3所展示的

示例代码3:sendfile()系统调用

#include <sys/socket.h>
ssize_t sendfile(int out_fd, int in_fd, off_t *offset, size_t count);

在代码示例1中的file.read()和sockect.send()操作可以被直接替换为单个的transferTo()调用。

示例代码4:使用transferTo()方法把数据从磁盘文件复制到套接字

transferTo(position, count, writableChannel);

使用transferTo时的数据路径

Figure 3. Data copy with transferTo()

使用transferTo方法时的上下文切换

Figure 4. Context switching with transferTo()

当你使用transferTo()方法时,会执行如下动作:

  • 1、transferTo()方法的执行,会让DMA引擎把文件内容拷贝到read buffer中,
    之后,内核会把数据从内核buffer拷贝到一个和输出套接字相关的内核buffer中

  • 2、DMA引擎把数据从内核套接字缓存传递到协议引擎

这是一个进步: 我们已经减少了上下文切换的次数。由原来的4次减少为2次,并减少了数据拷贝的次数从4次降低为3次。但是这还没有达到我们的零拷贝目标。我们可以进一步减少数据复制的次数,如果底层的网络接口支持聚合操作。从linux kernel 2.4以及其后的系统,套接字缓存描述符都做了修改以适应这种需求。 这种方法不仅减少了多次上下文切换而且也消除了涉及到CPU的数据拷贝操作。虽然用户端的使用还是像以前一样,但其内部的运行机制已经发生了改变:

  • 1、 transferTo()方法的调用,使得DMA引擎把文件内容复制到内核缓存。

  • 2、没有数据再被复制进套接字缓存。相反,只有相关位置和数据长度信息的描述符被追加进套接字缓存中。DMA引擎
    直接把套接字缓存中的数据传输到协议引擎中,也就因此消除了最后一个cpu拷贝操作。

transferTo和聚合操作的同时使用

Figure 5. Data copies when transferTo() and gather operations are used

创建一个文件服务器

现在,我们使用在客户端和服务器之间传输文件的相同示例,来实践零副本(示例代码请参见下载)。 TraditionalClient.java和TraditionalServer.java基于传统的复制语义,使用File.read()和Socket.send()。TraditionalServer.java是一个服务器程序,该程序在特定的端口上侦听客户端进行连接,然后一次从套接字读取4K字节的数据。 TraditionalClient.java连接到服务器,从文件中读取(使用File.read())4K字节数据,然后通过套接字将内容(使用socket.send())发送到服务器。

同样,TransferToServer.java和TransferToClient.java执行相同的功能,但改用transferTo()方法(进而使用sendfile()系统调用)将文件从服务器传输到客户端。

性能比较

我们在linux2.6上执行上面的示例程序,并测量使用传统方法和使用transferTo方法所消耗的时间对比。

表1:性能对比: 传统方法 VS 零拷贝

文件大小 传统的文件传输方法 (ms) transferTo方法 (ms)
7MB 156 45
21MB 337 128
63MB 843 387
98MB 1320 617
200MB 2124 1150
350MB 3631 1762
700MB 13498 4422
1GB 18399 8537

总结

我们上面展示了相较于使用传统方法,使用TransferTo()的性能优势。中间媒介的buffer拷贝---即使它们隐藏在内核层面,依旧产生了相当可观的消耗。

如果一个应用系统需要在channel间处理大量的数据拷贝的话,零拷贝技术可以带来极大地性能提升。

参考文献

Efficient data transfer through zero copy

©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 216,163评论 6 498
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 92,301评论 3 392
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 162,089评论 0 352
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 58,093评论 1 292
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 67,110评论 6 388
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 51,079评论 1 295
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 40,005评论 3 417
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 38,840评论 0 273
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 45,278评论 1 310
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 37,497评论 2 332
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 39,667评论 1 348
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 35,394评论 5 343
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 40,980评论 3 325
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 31,628评论 0 21
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,796评论 1 268
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 47,649评论 2 368
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 44,548评论 2 352

推荐阅读更多精彩内容