零拷贝
普通流程
网络IO读写流程应用进程的每一次写操作,都会把数据写到用户空间的缓冲区中,再由 CPU 将数据拷贝到系统内核的缓冲区中,之后再由 DMA 将这份数据拷贝到网卡中,最后由网卡发送出去。这里我们可以看到,一次写操作数据要拷贝两次才能通过网卡发送出去,而用户进程的读操作则是将整个流程反过来,数据同样会拷贝两次才能让应用程序读取到数据。应用进程的一次完整的读写操作,都需要在用户空间与内核空间中来回拷贝,并且每一次拷贝,都需要 CPU 进行一次上下文切换(由用户进程切换到系统内核,或由系统内核切换到用户进程),这样是不是很浪费 CPU 和性能呢?那有没有什么方式,可以减少进程间的数据拷贝,提高数据传输的效率呢?
所谓的零拷贝,就是取消用户空间与内核空间之间的数据拷贝操作,应用进程每一次的读写操作,都可以通过一种方式,让应用进程向用户空间写入或者读取数据,就如同直接向内核空间写入或者读取数据一样,再通过 DMA 将内核中的数据拷贝到网卡,或将网卡中的数据 copy 到内核。
零拷贝有两种解决方式,分别是 mmap+write 方式和 sendfile 方式,mmap+write方式的核心原理就是通过虚拟内存来解决的。
mmap(Memory-mapped file)
mmap(Memory-mapped file)是一种在内存和文件之间建立映射关系的技术。它允许将文件的一部分或整个文件映射到进程的地址空间,使得进程可以像访问内存一样直接读写文件数据。 使用mmap技术进行文件映射,可以带来一些优势和灵活性: 1. 零拷贝操作:mmap技术避免了数据在内核空间和用户空间之间的拷贝,从而提高了文件I/O的效率。当进程访问映射区域时,数据直接在内存中进行读写,减少了不必要的数据复制操作。 2. 文件缓存:映射文件的数据可以利用操作系统的文件缓存机制。如果多个进程都映射了同一个文件,它们可以共享同一个缓存,减少了磁盘I/O的次数,提高了整体性能。 3. 随机访问:通过映射文件到内存,进程可以直接访问文件的任意部分,而无需按照顺序逐步读取。这使得随机访问文件变得更加高效。 4. 内存映射:映射文件到内存后,可以将文件视为一块连续的内存区域进行操作。这使得对文件的读写操作变得更加灵活和方便。
sendfile
数据根本不经过用户态,直接从内核缓冲区进入到 Socket Buffer,同时,由于和用户态完全无关,就减少了一次上下文切换。
mmap 和 sendFile 的区别
- mmap 适合小数据量读写,sendFile 适合大文件传输。
- mmap 需要 4 次上下文切换,3 次数据拷贝;sendFile 需要 2 次上下文切换,最少 2 次数据拷贝。
- sendFile 可以利用 DMA 方式,减少 CPU 拷贝,mmap 则不能(必须从内核拷贝到 Socket 缓冲区)。
- sendfile因为没有经过用户内存,所以不适合对传输数据有变化场景。
在这个选择上:rocketMQ 在消费消息时,使用了 mmap。kafka在Index文件读取,通常使用mmap方式读取;Segment文件读取,通常使用sendfile系统调用来实现零拷贝读取和发送。
几种I/O操作对比
- 传统I/O 硬盘—>内核缓冲区—>用户缓冲区—>内核socket缓冲区—>协议引擎
- mmap 硬盘—>内核缓冲区映射到用户缓冲区—>内核socket缓冲区—>协议引擎
- sendfile 硬盘—>内核缓冲区—>内核socket缓冲区—>协议引擎
- sendfile( DMA 收集拷贝) 硬盘—>内核缓冲区—>协议引擎
JVM的拷贝
对于Java程序来说:因为使用了JVM,其实就更复杂了。因为JVM相对于操作系统就是用户进程,但是JVM里面又划分了一个JVM的堆,所以其实所谓的堆外空间的IO才是上面讲的用户进程地址空间拷贝到内核缓存。
jvm堆内的数据的IO呢?参照这篇文章下面作者给读者的评论,是说:
正常IO是两次拷贝,但是由于Java 中堆的GC机制,在IO操作时会导致地址变化移动(但是C调用IO时要求地址是不能发生变化,否则出错),所以会拷贝到堆外内存,从而存在堆内与堆外多一次的复制。
也就是因为jvm因为通过调用native方法,从而使用的C语言封装的系统调用,从而必须要满足C语言调用IO的限制(要求IO时地址不能发生变化),所以jvm因为堆内内存可能发生gc,导致数据使用的地址变化,从而在进行IO的时候,需要有堆内堆外拷贝的过程,拷贝到堆外内存之后再通过C语言的系统调用进行用户进程和操作系统内核缓存数据的交互。
也因此解释了,C语言没有垃圾回收,也没有内存整理,需要用户手动free或者delete内存的占用;而Java用了gc,方便了用户,牺牲了IO性能。因为按照上面的理解,Java程序在发生IO时,比C语言的进程更多一次数据拷贝。