一、mmap
mmap的优势在于通过把文件的某一块内容映射到用户空间上,用户可以直接向内核缓冲池读写这一块内容,这样一来就少了内核与用户空间的来回拷贝所以通常更快。
但 mmap方式只适用于更新、读写一块固定大小的文件区域而不能做像诸如不断的写内容进入文件导到文件增长这类的事
第一步:mmap系统调用使DMA引擎将文件内容复制到内核缓冲区中。然后与用户进程共享缓冲区,而无需在内核和用户内存空间之间执行任何复制。
第二步:写系统调用使内核将数据从原始内核缓冲区复制到与套接字关联的内核缓冲区中。
第三步:第三份复制发生在DMA引擎将数据从内核套接字缓冲区传递到协议引擎时。
特点
Mmap内存映射和普通标准IO操作的本质区别在于它并不需要将文件中的数据先拷贝至OS的内核IO缓冲区,而是可以直接将用户进程私有地址空间中的一块区域与文件对象建立映射关系,这样程序就好像可以直接从内存中完成对文件读/写操作一样。
只有当缺页中断发生时,直接将文件从磁盘拷贝至用户态的进程空间内,只进行了一次数据拷贝。对于容量 较大的文件来说(文件大小一般需要限制在1.5~2G以下),采用Mmap的方式其读/写的效率和性能都非常高
缺点:
1、虚拟内存增大。mmap
会导致虚拟内存增大,我们的APK、Dex、so
都是通过 mmap 读取。而目前大部分的应用还没支持 64 位,除去内核使用的地址空间,一般我们可以使用的虚拟内存空间只有 3GB 左右。如果 mmap
一个 1GB 的文件,应用很容易会出现虚拟内存不足所导致的 OOM。
2、磁盘延迟。mmap
通过缺页中断向磁盘发起真正的磁盘 I/O,所以如果我们当前的问题是在于磁盘 I/O 的高延迟,那么用mmap()
消除小小的系统调用开销是杯水车薪的
3、不能不断的写内容进入文件导到文件增长这类的事
4、只适用于更新、读写一块固定大小的文件区域
优势:
1、mmap 比较适合于对同一块区域频繁读写的情况,推荐也使用线程来操作。用户日志、数据上报都满足这种场景,另外需要跨进程同步的时候,mmap 也是一个不错的选择
使用Mmap的限制
a.Mmap
映射的内存空间释放的问题;由于映射的内存空间本身就不属于JVM的堆内存区(Java Heap),因此其不受JVM GC的控制,卸载这部分内存空间需要通过系统调用 unmap()方法来实现。
然而unmap()方法是FileChannelImpl类里实现的私有方法,无法直接显示调用。RocketMQ中的做法是,通过Java反射的方式调用“sun.misc”包下的Cleaner类的clean()方法来释放映射占用的内存空间;
b.MappedByteBuffer
内存映射大小限制;因为其占用的是虚拟内存(非JVM的堆内存),大小不受JVM的-Xmx参数限制,但其大小也受到OS虚拟内存大小的限制。一般来说,一次只能映射1.5~2G 的文件至用户态的虚拟内存空间,这也是为何RocketMQ默认设置单个CommitLog日志数据文件为1G的原因了
c.使用MappedByteBuffe
的其他问题;会存在内存占用率较高和文件关闭不确定性的问题;
二、sendfile
第一步:sendfile系统调用使DMA引擎将文件内容复制到内核缓冲区中。然后,数据被内核复制到与套接字关联的内核缓冲区中
第二步:第三份复制发生在DMA引擎将数据从内核套接字缓冲区传递到网络
sendfile(socket,file,len);
自内核版本号2.1,引进了sendfile2.4之后,sendfile实现了更简单的方式,不同之处在于,文件到达内核缓冲区后,不必再将数据全部复制到socket buffer缓冲区,而只将记录数据位置和长度相关的数据保存到socket buffer,而数据实际由DMA模块直接发送给协议相关引擎,再次降低了复制操作。
总结:
-
mmap 适合小数据量读写,sendFile 适合大文件传输。
-
mmap 需要 4 次上下文切换,3 次数据拷贝;sendFile 需要 3 次上下文切换,最少 2 次数据拷贝。
-
sendFile 可以利用 DMA 方式,减少 CPU 拷贝,mmap 则不能(必须从内核拷贝到 Socket 缓冲区)。
-
mmap将磁盘文件映射到内存,支持读和写,对内存的操作会反映在磁盘文件上。
-
sendfile 是将读到内核空间的数据,转到socket buffer,进行网络发送;
kafka推送消息用到了sendfile,落盘技术用到了mmap
RocketMQ 在消费消息时,使用了 mmap。kafka 使用了 sendFile。