本文转自:https://www.ibm.com/developerworks/cn/java/j-zerocopy/ 写的不错,搬运过来java
本文解释了如何经过一种称为零拷贝 的方法来提升运行于 Linux® 和 UNIX® 平台上的 I/O 密集型 Java™ 应用程序的性能。零拷贝不只消除了中间缓冲区之间的冗余数据拷贝,还减小了用户空间和内核空间之间的上下文切换次数。缓存
不少 Web 应用程序都会提供大量的静态内容,其数量多到至关于读完整个磁盘的数据再将一样的数据写回响应套接字(socket)。此动做看似只需较少的 CPU 活动,但它的效率很是低:首先内核读出全盘数据,而后将数据跨越内核用户推到应用程序,而后应用程序再次跨越内核用户将数据推回,写出到套接字。应用程序实际上在这里担当了一个不怎么高效的中介角色,将磁盘文件的数据转入套接字。服务器
数据每遍历用户内核一次,就要被拷贝一次,这会消耗 CPU 周期和内存带宽。幸运的是,您能够经过一个叫 零拷贝— 很贴切 — 的技巧来消除这些拷贝。使用零拷贝的应用程序要求内核直接将数据从磁盘文件拷贝到套接字,而无需经过应用程序。零拷贝不只大大地提升了应用程序的性能,并且还减小了内核与用户模式间的上下文切换。网络
Java 类库经过 java.nio.channels.FileChannel
中的 transferTo()
方法来在 Linux 和 UNIX 系统上支持零拷贝。可使用transferTo()
方法直接将字节从它被调用的通道上传输到另一个可写字节通道上,数据无需流经应用程序。本文首先展现了经过传统拷贝语义进行的简单文件传输引起的开销,而后展现了使用 transferTo()
零拷贝技巧如何提升性能。异步
考虑一下从一个文件中读出数据并将数据传输到网络上另外一程序的场景(这个场景表述出了不少服务器应用程序的行为,包括提供静态内容的 Web 应用程序、FTP 服务器、邮件服务器等)。操做的核心在清单 1 的两个调用中:socket
File.read(fileDesc, buf, len); Socket.send(socket, buf, len);
清单 1 的概念很简单,但实际上,拷贝的操做须要四次用户模式和内核模式间的上下文切换,并且在操做完成前数据被复制了四次。图 1 展现了数据是如何在内部从文件移动到套接字的:性能
图 2 展现了上下文切换:操作系统
这里涉及的步骤有:3d
read()
调用(参见 图 2)引起了一次从用户模式到内核模式的上下文切换。在内部,发出 sys_read()
(或等效内容)以从文件中读取数据。直接内存存取(direct memory access,DMA)引擎执行了第一次拷贝(参见 图 1),它从磁盘中读取文件内容,而后将它们存储到一个内核地址空间缓存区中。read()
调用返回。该调用的返回引起了内核模式到用户模式的上下文切换(又一次上下文切换)。如今数据被储存在用户地址空间缓冲区。send()
套接字调用引起了从用户模式到内核模式的上下文切换。数据被第三次拷贝,并被再次放置在内核地址空间缓冲区。可是这一次放置的缓冲区不一样,该缓冲区与目标套接字相关联。send()
系统调用返回,结果致使了第四次的上下文切换。DMA 引擎将数据从内核缓冲区传到协议引擎,第四次拷贝独立地、异步地发生 。使用中间内核缓冲区(而不是直接将数据传输到用户缓冲区)看起来可能有点效率低下。可是之因此引入中间内核缓冲区的目的是想提升性能。在读取方面使用中间内核缓冲区,能够容许内核缓冲区在应用程序不须要内核缓冲区内的所有数据时,充当 “预读高速缓存(readahead cache)” 的角色。这在所需数据量小于内核缓冲区大小时极大地提升了性能。在写入方面的中间缓冲区则可让写入过程异步完成。code
不幸的是,若是所需数据量远大于内核缓冲区大小的话,这个方法自己可能成为一个性能瓶颈。数据在被最终传入到应用程序前,在磁盘、内核缓冲区和用户缓冲区中被拷贝了屡次。
零拷贝经过消除这些冗余的数据拷贝而提升了性能。
再次检查 传统场景,您就会注意到第二次和第三次拷贝根本就是多余的。应用程序只是起到缓存数据并将其传回到套接字的做用而以,别无他用。数据能够直接从读取缓冲区传输到套接字缓冲区。transferTo()
方法就可以让您实现这个操做。清单 2 展现了 transferTo()
的方法签名:
transferTo()
方法public void transferTo(long position, long count, WritableByteChannel target);
transferTo()
方法将数据从文件通道传输到了给定的可写字节通道。在内部,它依赖底层操做系统对零拷贝的支持;在 UNIX 和各类 Linux 系统中,此调用被传递到 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()
和 socket.send()
调用动做能够替换为一个单一的 transferTo()
调用,如清单 4 所示:
transferTo()
将数据从磁盘文件拷贝到套接字transferTo(position, count, writableChannel);
图 3 展现了使用 transferTo()
方法时的数据路径:
transferTo()
方法的数据拷贝图 4 展现了使用 transferTo()
方法时的上下文切换:
transferTo()
方法的上下文切换使用 清单 4 所示的 transferTo()
方法时的步骤有:
transferTo()
方法引起 DMA 引擎将文件内容拷贝到一个读取缓冲区。而后由内核将数据拷贝到与输出套接字相关联的内核缓冲区。改进的地方:咱们将上下文切换的次数从四次减小到了两次,将数据复制的次数从四次减小到了三次(其中只有一次涉及到了 CPU)。可是这个代码还没有达到咱们的零拷贝要求。若是底层网络接口卡支持收集操做 的话,那么咱们就能够进一步减小内核的数据复制。在 Linux 内核 2.4 及后期版本中,套接字缓冲区描述符就作了相应调整,以知足该需求。这种方法不只能够减小多个上下文切换,还能够消除须要涉及 CPU 的重复的数据拷贝。对于用户方面,用法仍是同样的,可是内部操做已经发生了改变:
transferTo()
方法引起 DMA 引擎将文件内容拷贝到内核缓冲区。图 5 展现告终合使用 transferTo()
方法和收集操做的数据拷贝:
transferTo()
和收集操做时的数据拷贝