我理解的零拷贝-原文连接
java
最近作的业务涉及到的 I/O 操做比较多,对于Linux上的 I/O 操做的优化 Zero Copy 早有耳闻,今天打算由上而下(从应用层到底层,固然并不会涉及到内核的细节)的研究一下这个问题。linux
为了更好的描述 zero copy ,本文将以网络服务器的简单过程所涉及的内容展开,该过程经过网络将存储在服务端的文件中的数据提供给客户端。整个过程主要是网络的 I/O 操做,数据至少被复制了4次,而且几乎已经执行了许多用户/内核上下文切换。 以下图所示,通过了下面四个步骤:缓存
步骤一:操做系统发生 read 系统调用读取磁盘中的文件内容并将其存储到内核地址空间缓冲区中。服务器
第二步:将数据从内核缓冲区复制到用户缓冲区,read 系统调用返回。调用的返回致使了从内核返回到用户模式的上下文切换,如今,数据存储在用户地址空间缓冲区中,它能够再次开始向下移动。网络
第三步:write 系统调用致使从用户模式到内核模式的上下文切换,执行第三个复制,将数据再次放入内核地址空间缓冲区中。可是这一次,数据被放入一个不一样的缓冲区,这个缓冲区是与套接字相关联的。架构
第四步:写系统调用返回,建立第四个上下文切换。并将数据写入网络 I/O 中,网络传输中的服务端的操做逻辑到此结束。jvm
从上图中咱们知道,整个网络传输过程当中数据被复制了多达4次之多,也进行了屡次从用户态到内核态的切换。那么有没有可能减小数据的复制次数,提升网络 I/O 的效率呢?答案是确定的。socket
那么到底什么是零拷贝呢?就是将数据直接从内核态的缓冲区中直接拷贝到 Socket 的缓冲区中,没有通过用户态的缓冲区,之因此被叫作零拷贝是相对于用户态来讲的。以下图所示: 性能
总的来讲,从操做系统的角度来看是零拷贝,由于数据不是在内核缓冲区之间复制的。当使用零拷贝时,除了复制避免以外,还用其余性能优点,例如更少的上下文切换、更少的 CPU 数据缓存污染和没有 CPU 校验和计算。优化
NIO 中的 FileChannel 拥有 transferTo 和 transferFrom 两个方法,可直接把 FileChannel 中的数据拷贝到另一个 Channel,或直接把另一个 Channel 中的数据拷贝到 FileChannel。该接口常被用于高效的网络/文件的数据传输和大文件拷贝。在操做系统支持的状况下,经过该方法传输数据并不须要将源数据从内核态拷贝到用户态,再从用户态拷贝到目标通道的内核态,同时也避免了两次用户态和内核态间的上下文切换,也即便用了“零拷贝”。
/** * disk-nic零拷贝 */
class ZeroCopyServer {
ServerSocketChannel listener = null;
public static void main(String[] args) {
ZerocopyServer dns = new ZerocopyServer();
dns.mySetup();
dns.readData();
}
protected void mySetup() {
InetSocketAddress listenAddr = new InetSocketAddress(9026);
try {
listener = ServerSocketChannel.open();
ServerSocket ss = listener.socket();
ss.setReuseAddress(true);
ss.bind(listenAddr);
System.out.println("监听的端口:" + listenAddr.toString());
} catch (IOException e) {
System.out.println("端口绑定失败 : " + listenAddr.toString() + " 端口可能已经被使用,出错缘由: " + e.getMessage());
e.printStackTrace();
}
}
private void readData() {
ByteBuffer dst = ByteBuffer.allocate(4096);
try {
while (true) {
SocketChannel conn = listener.accept();
System.out.println("建立的链接: " + conn);
conn.configureBlocking(true);
int nread = 0;
while (nread != -1) {
try {
nread = conn.read(dst);
} catch (IOException e) {
e.printStackTrace();
nread = -1;
}
dst.rewind();
}
}
} catch (IOException e) {
e.printStackTrace();
}
}
}
复制代码
对于 I/O 操做的优化也能够参考零拷贝的思路来对咱们的系统进行优化,最近了解到 kafka 之因此能够可以承载高吞吐量跟它强依赖底层操做系统的 page cache 有很大关系,因此在使用 Kafka 并非 jvm 的内存越大越好,跟零拷贝的减小数据在内核态与用户态之间的拷贝,上下文切换有殊途同归的操做,对 kafka 还不甚了解不敢多说了……
为了弥补这种性能差别,现代操做系统在愈来愈注重使用内存对磁盘进行 cache。现代操做系统主动将全部空闲内存用做 disk caching ,代价是在内存回收时性能会有所下降。全部对磁盘的读写操做都会经过这个统一的 cache。若是不使用直接 I/O,该功能不能轻易关闭。所以即便进程维护了 in-process cache,该数据也可能会被复制到操做系统的 pagecache 中,事实上全部内容都被存储了两份。
此外,Kafka 创建在 JVM 之上,任何了解 Java 内存使用的人都知道两点:
受这些因素影响,相比于维护 in-memory cache 或者其余结构,使用文件系统和 pagecache 显得更有优点--咱们能够经过自动访问全部空闲内存将可用缓存的容量至少翻倍,而且经过存储紧凑的字节结构而不是独立的对象,有望将缓存容量再翻一番。 这样使得32GB的机器缓存容量能够达到28-30GB,而且不会产生额外的 GC 负担。此外,即便服务从新启动,缓存依旧可用,而 in-process cache 则须要在内存中重建(重建一个10GB的缓存可能须要10分钟),不然进程就要从 cold cache 的状态开始(这意味着进程最初的性能表现十分糟糕)。 这同时也极大的简化了代码,由于全部保持 cache 和文件系统之间一致性的逻辑如今都被放到了 OS 中,这样作比一次性的进程内缓存更准确、更高效。若是你的磁盘使用更倾向于顺序读取,那么 read-ahead 能够有效的使用每次从磁盘中读取到的有用数据预先填充 cache。
这里给出了一个很是简单的设计:相比于维护尽量多的 in-memory cache,而且在空间不足的时候匆忙将数据 flush 到文件系统,咱们把这个过程倒过来。全部数据一开始就被写入到文件系统的持久化日志中,而不用在 cache 空间不足的时候 flush 到磁盘。实际上,这代表数据被转移到了内核的 pagecache 中。
如上图所示,从宏观上来看,操做系统的体系架构分为用户态和内核态。内核从本质上看是一种软件——控制计算机的硬件资源,并提供上层应用程序运行的环境。用户态即上层应用程序的活动空间,应用程序的执行必须依托于内核提供的资源,包括 CPU 资源、存储资源、I/O 资源等。为了使上层应用可以访问到这些资源,内核必须为上层应用提供访问的接口:即系统调用。