有关zero-copy,mmap,direct-memory的一切

  本文专门解释关于高性能IO的基层技术,关于zero-copy,mmap,direct-memory,这些都是Linux/Windows/Mac OS都支持的底层api,但我会用Java作为主语言。
打比方有需求是从文件error.txt中读取文字,然后做一些业务修改操作再回写到socket把文件传输给同伴。Java写这个逻辑是很简单的用 InputStream 流 1chunk,1chunk读入到一个buffer,然后把内容修改完,写入 OutputStream 流,写回文件,然后发到socket buffer从网卡上发出去。

  虽然需求做完了,但是如果这个需求是大规模业务,这样做是显然不合适的或者说做的不够好,对于OS开销相当大,延迟会越来越高随着使用人数的增多。

  我们看下为啥开销相当大:


正常IO流读写.png
  1. JVM 通过native方法到c语言读文件api再通过glibc调用系统函数read()
  2. OS会从用户态陷入内核态,向disk请求调用读取error.txt这个文件数据,之后DMA会把文件数据读入到内存的内核地址空间的buffer中。
  3. OS内核再把上面buffer的文件数据通过上下文切换copy到用户态的buffer,之后我们写的程序就可以读到了
  4. 我们程序读到文件内容做了某些修改后调用native方法到c语言的写文件api再通过glibc调用系统函数write()
  5. OS再次上下文切换到用户态把文件数据copy到内核态socket描述符的buffer,最后数据刷出到socket。
  6. 刷到socket后,OS上下文切换回用户态,返回JVM的程序成功。

从图和表述看,这个过程就很复杂,2次OS上下文切换,4次昂贵的文件数据拷贝。


改进版本一: sendfile()/transferTo() 实现zero-copy

  zero-copy可以节省2次文件数据拷贝开销,这个技术以来各个系统自身的实现,每个都不同。一般*nix like的OS会提供 sendfile()方法来实现(PS:对应Java的api是NIO的transferTo()函数)。

sendfile() 系统调用:

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

transferTo() Java Nio函数:

public abstract long transferTo(long position,
                                long count,
                                WritableByteChannel target)
                                throws IOException;

transferTo() 将数据从的文件传输到指定的ByteChannel。使用此函数,相当于之前java的InputStream的read()和OutputStream的write()可以替换为一个单一的 transferTo()函数。下图展示了使用 transferTo() 方法时的数据路径:

用 transferTo() 方法的数据拷贝.png


使用了sendfile() / transferTo()函数时的OS执行步骤:

sendfile()/transferTo() 实现zero-copy.png
  1. 发出sendfile系统调用,导致用户空间到内核空间的上下文切换(第一次上下文切换)。通过DMA将磁盘文件中的内容拷贝到内核空间缓冲区中(第一次拷贝: hard driver ——> kernel buffer)。
  2. 然后再将数据从内核空间缓冲区拷贝到内核中与socket相关的缓冲区中(第二次拷贝: kernel buffer ——> socket buffer)。
  3. sendfile系统调用返回,导致内核空间到用户空间的上下文切换(第二次上下文切换)。通过DMA引擎将内核空间socket缓冲区中的数据传递到协议引擎(第三次拷贝: socket buffer ——> protocol engine)。
通过sendfile实现的零拷贝I/O只使用了2次用户空间与内核空间的上下文切换,以及3次数据的拷贝(1次CPU执行的,2次DMA执行的)。 你可能会说操作系统仍然需要在内核内存空间中复制数据(kernel buffer —>socket buffer)。 是的,但从操作系统的角度来看,这已经是零拷贝,因为没有数据从内核空间复制到用户空间。 内核需要复制的原因是因为通用硬件DMA访问需要连续的内存空间(因此需要缓冲区)。



改进版本二: sendfile()+DMA scatter-and-gather实现zero-copy

sendfile()+DMA scatter-and-gather实现zero-copy.png
  1. 发出sendfile系统调用,导致用户空间到内核空间的上下文切换(第一次上下文切换)。通过DMA引擎将磁盘文件中的内容拷贝到内核空间缓冲区中(第一次拷贝: hard drive ——> kernel buffer)。
  2. 没有数据拷贝到socket缓冲区。取而代之的是只有相应的描述符信息会被拷贝到相应的socket缓冲区当中。该描述符包含了两方面的信息:a)kernel buffer的内存地址;b)kernel buffer的偏移量。
  3. sendfile系统调用返回,导致内核空间到用户空间的上下文切换(第二次上下文切换)。DMA gather copy根据socket缓冲区中描述符提供的位置和偏移量信息直接将内核空间缓冲区中的数据拷贝到协议引擎上(第二次拷贝: kernel buffer ——> protocol engine),这样就避免了最后一次CPU数据拷贝。
带有DMA收集拷贝功能的sendfile实现的I/O只使用了2次用户空间与内核空间的上下文切换,以及2次数据的拷贝,而且这2次的数据拷贝都是非CPU拷贝。这样一来我们就实现了最理想的零拷贝I/O传输了,不需要任何一次的CPU拷贝,以及最少的上下文切换。


改进版本三,通过mmap实现zero-copy

  传统I/O用户空间缓冲区中存有数据,因此应用程序能够对此数据进行修改等操作;而sendfile零拷贝消除了所有内核空间缓冲区与用户空间缓冲区之间的数据拷贝过程,因此sendfile零拷贝I/O的实现是完成在内核空间中完成的,这对于应用程序来说就无法对数据进行操作了。为了解决这个问题,Linux提供了mmap零拷贝来实现我们的需求。

mmap.png

mmap(内存映射)是一个比sendfile昂贵但优于传统I/O的方法。
我们的程序发起一次系统调用,将一个文件(或者文件的一部分)映射到虚拟地址空间的一部分,注意这时候没有分配和映射到具体的物理内存空间,而是到第一次加载这个文件的时候,通过MMU把之前虚拟地址换算成物理地址,把文件加载进物理内存。

mmap实现zero-copy.png

具体分析消耗步骤如下:

  1. 发出mmap系统调用,导致用户空间到内核空间的上下文切换(第一次上下文切换)。通过DMA引擎将磁盘文件中的内容拷贝到内核空间缓冲区中(第一次拷贝: hard drive ——> kernel buffer)。
  2. mmap系统调用返回,导致内核空间到用户空间的上下文切换(第二次上下文切换)。接着用户空间和内核空间共享这个缓冲区,而不需要将数据从内核空间拷贝到用户空间。因为用户空间和内核空间共享了这个缓冲区数据,所以用户空间就可以像在操作自己缓冲区中数据一般操作这个由内核空间共享的缓冲区数据。
  3. 发出write系统调用,导致用户空间到内核空间的上下文切换(第三次上下文切换)。将数据从内核空间缓冲区拷贝到内核空间socket相关联的缓冲区(第二次拷贝: kernel buffer ——> socket buffer)。
  4. write系统调用返回,导致内核空间到用户空间的上下文切换(第四次上下文切换)。通过DMA引擎将内核空间socket缓冲区中的数据传递到协议引擎(第三次拷贝: socket buffer ——> protocol engine)

通过mmap实现的零拷贝I/O进行了4次用户空间与内核空间的上下文切换,以及3次数据拷贝。其中3次数据拷贝中包括了2次DMA拷贝和1次CPU拷贝。明显,它与传统I/O相比仅仅少了1次内核空间缓冲区和用户空间缓冲区之间的CPU拷贝。这样的好处是,我们可以将整个文件或者整个文件的一部分映射到内存当中,用户直接对内存中对文件进行操作,然后是由操作系统来进行相关的页面请求并将内存的修改写入到文件当中。我们的应用程序只需要处理内存的数据,这样可以实现非常迅速的I/O操作。



说了这么多,那么Java NIO中对零拷贝的使用有哪些呢?

NIO DirectByteBuffer
Java NIO引入了用于通道的缓冲区的ByteBuffer。 ByteBuffer有三个主要的实现:

HeapByteBuffer
在调用ByteBuffer.allocate()时使用。 它被称为堆,因为它保存在JVM的堆空间中,因此您可以获得所有优势,如GC支持和缓存优化。 但是,它不是页面对齐的,这意味着如果您需要通过JNI与本地代码交谈,JVM将不得不复制到对齐的缓冲区空间。

DirectByteBuffer
在调用ByteBuffer.allocateDirect()时使用。 JVM将使用malloc()在堆空间之外分配内存空间。 因为它不是由JVM管理的,所以你的内存空间是页面对齐的,不受GC影响,这使得它成为处理本地代码的完美选择。 然而,你要C程序员一样,自己管理这个内存,必须自己分配和释放内存来防止内存泄漏。

MappedByteBuffer
在调用FileChannel.map()时使用。 与DirectByteBuffer类似,这也是JVM堆外部的情况。 它基本上作为OS mmap()系统调用的包装函数,以便代码直接操作映射的物理内存数据。


总结

零拷贝是操作系统底层的一种实现,我们在网络编程中,利用操作系统这一特性,可以大大提高数据传输的效率。这也是目前网络编程框架中都会采用的方式,理解好零拷贝,有助于我们进一步学习Netty等网络通信框架的底层原理。

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

推荐阅读更多精彩内容