Java零拷贝二步曲——Linux 中的零拷贝技术

假如如今有这样一个需求:须要将磁盘中的一个文件经过网络传输到另外一个设备上,咱们看看各类实现方式中数据流的传递过程。node

传统read和write数据传输

在"Java零拷贝一步曲"中咱们也展现过这个过程,这个过程产生的系统消耗是:web

  • 4次数据copy
  • 4次应用程序与内核之间上下文切换

image

Linux 中的直接 I/O

若是应用程序能够直接访问网络接口存储,那么在应用程序访问数据以前存储总线就不须要被遍历,数据传输所引发的开销将会是最小的。应用程序或者运行在用户模式下的库函数能够直接访问硬件设备的存储,操做系统内核除了进行必要的虚拟存储配置工做以外,不参与数据传输过程当中的其它任何事情。直接 I/O 使得数据能够直接在应用程序和外围设备之间进行传输,彻底不须要操做系统内核页缓存的支持。
图 1. 使用直接 I/O 的数据传输
缓存

image

这个过程产生的系统消耗是:

  • 2次数据copy
  • 4次应用程序与内核之间上下文切换

针对数据传输不须要通过应用程序地址空间的零拷贝技术

利用 mmap()

在 Linux 中,减小拷贝次数的一种方法是调用 mmap() 来代替调用 read,好比:bash

tmp_buf = mmap(file, len); 
write(socket, tmp_buf, len);
复制代码

首先,应用程序调用了 mmap() 以后,数据会先经过 DMA 拷贝到操做系统内核的缓冲区中去。接着,应用程序跟操做系统共享这个缓冲区,这样,操做系统内核和应用程序存储空间就不须要再进行任何的数据拷贝操做。应用程序调用了 write() 以后,操做系统内核将数据从原来的内核缓冲区中拷贝到与 socket 相关的内核缓冲区中。接下来,数据从内核 socket 缓冲区拷贝到协议引擎中去,这是第三次数据拷贝操做。服务器

图 2. 利用 mmap() 代替 read()
网络

image

这个过程产生的系统消耗是:

  • 3次数据copy
  • 4次应用程序与内核之间上下文切换

经过使用 mmap() 来代替 read(), 已经能够减半操做系统须要进行数据拷贝的次数。当大量数据须要传输的时候,这样作就会有一个比较好的效率。可是,这种改进也是须要代价的,使用 mma()p 实际上是存在潜在的问题的。当对文件进行了内存映射,而后调用 write() 系统调用,若是此时其余的进程截断了这个文件,那么 write() 系统调用将会被总线错误信号 SIGBUS 中断,由于此时正在执行的是一个错误的存储访问。这个信号将会致使进程被杀死,解决这个问题能够经过如下这两种方法:异步

  1. 为 SIGBUS 安装一个新的信号处理器,这样,write() 系统调用在它被中断以前就返回已经写入的字节数目,errno 会被设置成 success。可是这种方法也有其缺点,它不能反映出产生这个问题的根源所在,由于 BIGBUS 信号只是显示某进程发生了一些很严重的错误。
  2. 第二种方法是经过文件租借锁来解决这个问题的,这种方法相对来讲更好一些。咱们能够经过内核对文件加读或者写的租借锁,当另一个进程尝试对用户正在进行传输的文件进行截断的时候,内核会发送给用户一个实时信号:RT_SIGNAL_LEASE 信号,这个信号会告诉用户内核破坏了用户加在那个文件上的写或者读租借锁,那么 write() 系统调用则会被中断,而且进程会被 SIGBUS 信号杀死,返回值则是中断前写的字节数,errno 也会被设置为 success。文件租借锁须要在对文件进行内存映射以前设置。

使用 mmap 是 POSIX 兼容的,可是使用 mmap 并不必定能得到理想的数据传输性能。数据传输的过程当中仍然须要一次 CPU 拷贝操做,并且映射操做也是一个开销很大的虚拟存储操做,这种操做须要经过更改页表以及冲刷 TLB (使得 TLB 的内容无效)来维持存储的一致性。可是,由于映射一般适用于较大范围,因此对于相同长度的数据来讲,映射所带来的开销远远低于 CPU 拷贝所带来的开销。socket

sendfile()

为了简化用户接口,同时还要继续保留 mmap()/write() 技术的优势:减小 CPU 的拷贝次数,Linux 在版本 2.1 中引入了 sendfile() 这个系统调用。函数

sendfile() 不只减小了数据拷贝操做,它也减小了上下文切换。首先:sendfile() 系统调用利用 DMA 引擎将文件中的数据拷贝到操做系统内核缓冲区中,而后数据被拷贝到与 socket 相关的内核缓冲区中去。接下来,DMA 引擎将数据从内核 socket 缓冲区中拷贝到协议引擎中去。若是在用户调用 sendfile () 系统调用进行数据传输的过程当中有其余进程截断了该文件,那么 sendfile () 系统调用会简单地返回给用户应用程序中断前所传输的字节数,errno 会被设置为 success。若是在调用 sendfile() 以前操做系统对文件加上了租借锁,那么 sendfile() 的操做和返回状态将会和 mmap()/write () 同样。性能

图 3. 利用 sendfile () 进行数据传输

image

这个过程产生的系统消耗是:

  • 3次数据copy
  • 2次应用程序与内核之间上下文切换
    它与mmap的区别就是少了一次应用程序与内核之间上下文切换

sendfile() 系统调用不须要将数据拷贝或者映射到应用程序地址空间中去,因此 sendfile() 只是适用于应用程序地址空间不须要对所访问数据进行处理的状况。相对于 mmap() 方法来讲,由于 sendfile 传输的数据没有越过用户应用程序 / 操做系统内核的边界线,因此 sendfile () 也极大地减小了存储管理的开销。可是,sendfile () 也有不少局限性,以下所列:

  • sendfile() 局限于基于文件服务的网络应用程序,好比 web 服务器。听说,在 Linux 内核中实现 sendfile() 只是为了在其余平台上使用 sendfile() 的 Apache 程序。
  • 因为网络传输具备异步性,很难在 sendfile () 系统调用的接收端进行配对的实现方式,因此数据传输的接收端通常没有用到这种技术。
  • 基于性能的考虑来讲,sendfile () 仍然须要有一次从文件到 socket 缓冲区的 CPU 拷贝操做,这就致使页缓存有可能会被传输的数据所污染。

带有 DMA 收集拷贝功能的 sendfile()

上小节介绍的 sendfile() 技术在进行数据传输仍然还须要一次多余的数据拷贝操做,经过引入一点硬件上的帮助,这仅有的一次数据拷贝操做也能够避免。为了不操做系统内核形成的数据副本,须要用到一个支持收集操做的网络接口,这也就是说,待传输的数据能够分散在存储的不一样位置上,而不须要在连续存储中存放。这样一来,从文件中读出的数据就根本不须要被拷贝到 socket 缓冲区中去,而只是须要将缓冲区描述符传到网络协议栈中去,以后其在缓冲区中创建起数据包的相关结构,而后经过 DMA 收集拷贝功能将全部的数据结合成一个网络数据包。网卡的 DMA 引擎会在一次操做中从多个位置读取包头和数据。Linux 2.4 版本中的 socket 缓冲区就能够知足这种条件,这也就是用于 Linux 中的众所周知的零拷贝技术,这种方法不但减小了由于屡次上下文切换所带来开销,同时也减小了处理器形成的数据副本的个数。对于用户应用程序来讲,代码没有任何改变。首先,sendfile() 系统调用利用 DMA 引擎将文件内容拷贝到内核缓冲区去;而后,将带有文件位置和长度信息的缓冲区描述符添加到 socket 缓冲区中去,此过程不须要将数据从操做系统内核缓冲区拷贝到 socket 缓冲区中,DMA 引擎会将数据直接从内核缓冲区拷贝到协议引擎中去,这样就避免了最后一次数据拷贝。

图 4. 带有 DMA 收集拷贝功能的 sendfile

image

这个过程产生的系统消耗是:

  • 1次数据copy
  • 2次应用程序与内核之间上下文切换

经过这种方法,CPU 在数据传输的过程当中不但避免了数据拷贝操做,理论上,CPU 也永远不会跟传输的数据有任何关联,这对于 CPU 的性能来讲起到了积极的做用:首先,高速缓冲存储器没有受到污染;其次,高速缓冲存储器的一致性不须要维护,高速缓冲存储器在 DMA 进行数据传输前或者传输后不须要被刷新。然而实际上,后者实现起来很是困难。源缓冲区有多是页缓存的一部分,这也就是说通常的读操做能够访问它,并且该访问也能够是经过传统方式进行的。只要存储区域能够被 CPU 访问到,那么高速缓冲存储器的一致性就须要经过 DMA 传输以前冲刷新高速缓冲存储器来维护。并且,这种数据收集拷贝功能的实现是须要硬件以及设备驱动程序支持的。

splice()

splice() 是  Linux  中与 mmap() 和  sendfile() 相似的一种方法。它也能够用于用户应用程序地址空间和操做系统地址空间之间的数据传输。splice() 适用于能够肯定数据传输路径的用户应用程序,它不须要利用用户地址空间的缓冲区进行显式的数据传输操做。那么,当数据只是从一个地方传送到另外一个地方,过程当中所传输的数据不须要通过用户应用程序的处理的时候,spice() 就成为了一种比较好的选择。splice() 能够在操做系统地址空间中整块地移动数据,从而减小大多数数据拷贝操做。并且,splice() 进行数据传输能够经过异步的方式来进行,用户应用程序能够先从系统调用返回,而操做系统内核进程会控制数据传输过程继续进行下去。splice() 能够被当作是相似于基于流的管道的实现,管道可使得两个文件描述符相互链接,splice 的调用者则能够控制两个设备(或者协议栈)在操做系统内核中的相互链接。

splice() 系统调用和 sendfile() 很是相似,用户应用程序必须拥有两个已经打开的文件描述符,一个用于表示输入设备,一个用于表示输出设备。与 sendfile() 不一样的是,splice() 容许任意两个文件之间互相链接,而并不仅是文件到 socket 进行数据传输。对于从一个文件描述符发送数据到 socket 这种特例来讲,一直都是使用 sendfile() 这个系统调用,而 splice 一直以来就只是一种机制,它并不只限于 sendfile() 的功能。也就是说,sendfile() 只是 splice() 的一个子集,在 Linux 2.6.23 中,sendfile() 这种机制的实现已经没有了,可是这个 API 以及相应的功能还存在,只不过 API 以及相应的功能是利用了 splice() 这种机制来实现的。

在数据传输的过程当中,splice() 机制交替地发送相关的文件描述符的读写操做,而且能够将读缓冲区从新用于写操做。它也利用了一种简单的流控制,经过预先定义的水印( watermark )来阻塞写请求。有实验代表,利用这种方法将数据从一个磁盘传输到另外一个磁盘会增长 30% 到 70% 的吞吐量,数据传输的过程当中, CPU 的负载也会减小一半。

Linux 2.6.17 内核引入了 splice() 系统调用,可是,这个概念在此以前 ] 其实已经存在了很长一段时间了。1988 年,Larry McVoy 提出了这个概念,它被当作是一种改进服务器端系统的 I/O 性能的一种技术,尽管在以后的若干年中常常被说起,可是 splice 系统调用历来没有在主流的 Linux 操做系统内核中实现过,一直到 Linux 2.6.17 版本的出现。splice 系统调用须要用到四个参数,其中两个是文件描述符,一个表示文件长度,还有一个用于控制如何进行数据拷贝。splice 系统调用能够同步实现,也可使用异步方式来实现。在使用异步方式的时候,用户应用程序会经过信号 SIGIO 来获知数据传输已经终止。splice() 系统调用的接口以下所示:

long splice(int fdin, int fdout, size_t len, unsigned int flags);
复制代码

调用 splice() 系统调用会致使操做系统内核从数据源 fdin 移动最多 len 个字节的数据到 fdout 中去,这个数据的移动过程只是通过操做系统内核空间,须要最少的拷贝次数。使用 splice() 系统调用须要这两个文件描述符中的一个必须是用来表示一个管道设备的。不难看出,这种设计具备局限性,Linux 的后续版本针对这一问题将会有所改进。参数 flags 用于表示拷贝操做的执行方法,当前的 flags 有以下这些取值:

  • SPLICE_F_NONBLOCK:splice 操做不会被阻塞。然而,若是文件描述符没有被设置为不可被阻塞方式的 I/O ,那么调用 splice 有可能仍然被阻塞。
  • SPLICE_F_MORE:告知操做系统内核下一个 splice 系统调用将会有更多的数据传来。
  • SPLICE_F_MOVE:若是输出是文件,这个值则会使得操做系统内核尝试从输入管道缓冲区直接将数据读入到输出地址空间,这个数据传输过程没有任何数据拷贝操做发生。

Splice() 系统调用利用了 Linux 提出的管道缓冲区( pipe buffer )机制,这就是为何这个系统调用的两个文件描述符参数中至少有一个必需要指代管道设备的缘由。为了支持 splice 这种机制,Linux 在用于设备和文件系统的 file_operations 结构中增长了下边这两个定义:

ssize_t (*splice_write)(struct inode *pipe, strucuct file *out, size_t len, unsigned int flags); 
ssize_t (*splice_read)(struct inode *in, strucuct file *pipe, size_t len, unsigned int flags);
复制代码

这两个新的操做能够根据 flags 的设定在 pipe 和 in 或者 out 之间移动 len 个字节。Linux 文件系统已经实现了具备上述功能而且可使用的操做,并且还实现了一个 generic_splice_sendpage() 函数用于和 socket 之间的接合。

相关文章
相关标签/搜索