read write、mmap、directBuffer、sendfile傻傻分不清楚?

当讨论到文件数据、网络数据的读写时,我们可以看到有多种解决方案,read write、mmap、directBuffer、sendfile这些大家都多多少少看到过,但可能不够清晰,本文旨在综合讨论这几种方案的原理,并做出对比

pageCache以及DMA名词解释:

由于后续内容多次涉及到pageCache以及DMA,这里先做下名词解释

pageCache:

       操作系统会为每个文件单独维护一个pageCache,其本质是内核中的一段内存,用户进程对于文件的大多数读写操作会直接作用到pageCache上,相当于一次纯内存操作,操作系统会在适当的时候将pageCache中的内容写到磁盘上(当然我们可以手工fsync控制回写),这样可以大大减少磁盘的访问次数,从而提高读写性能

DMA:

       在没有DMA时,所有的IO操作都由CPU亲力亲为,相对于CPU的风驰电掣来说(ns级),IO的速度真可谓龟速慢爬了(ms级),发起IO操作后,CPU只能一直忙等直到IO完成,这就造成CPU资源的极大浪费。DMA的引入就是为了将CPU从IO操作中解放出来,简单来说,DMA是一组芯片,专门负责和与硬件IO设备进行数据交互,控制数据传输。有了DMA后,CPU只需告诉DMA,要传啥数据,从哪来,到哪去,就可以放心离开了,实际传输工作由DMA完成,现在的硬件设备中都加上了DMA芯片,使得CPU很少需要再关心数据传输工作了

read write系统调用:

read write是最常用的IO操作,包括文件IO,以及网络IO,先看文件IO步骤

用户程序发起read系统调用:

1.操作系统上下文切换到对应的read中断处理程序,并从用户态切换到内核态(权限升级)

2.DMA将文件数据从磁盘读到pageCache

3.CPU将数据从pageCache拷贝到用户缓冲区

4.上下文切换回用户程序,切换到用户态,read返回

(read操作返回前,用户程序会一直处于阻塞状态,如果操作系统发现pageCache中有要读取的数据页,第2步就不需要了,相当于只有一次纯内存拷贝操作,如果pageCache未命中,就需要等待第2步完成,相对来说,磁盘操作比内存慢好几个数量级,如果能省去第2步,还是相当幸福的,怎么才能提高pageCache命中率呢?连续读取!pageCache缓存是以文件页page为单位的,一个page默认4k,如果read的数据连续分布,就会持续命中一个page,或者下一个page

ps:内存速度在10ns级别  固态盘150us级别 机械磁盘5ms级别  )

用户程序发起write系统调用:

1.操作系统上下文切换到对应的write中断处理程序,并且从用户态切换到内核态(权限升级)

2.CPU将数据从用户缓冲区拷贝到pageCache

3.上下文切换回应用程序,并切回用户态,write返回(到这里write就结束了)

4.DMA将数据从pageCache写到磁盘中

(write操作只需要将数据写到pageCache就可以返回了,只有一次内存拷贝,操作系统会异步的将数据写到磁盘中,写到磁盘的动作叫刷盘;操作系统对外提供了fsync系统调用(force sync),如果用户程序调用完write后觉得不保险(因为此时数据还在内存中),可以继续调用fsync强制操作系统将数据刷盘,那就需要阻塞到第4步结束,上面已经说过,磁盘操作很慢,这是安全的代价...)

看起来挺费劲的,又是上下文切换,又是中断调用,用户程序为啥不能直接操作文件呢?因为不安全!用户程序的行为是未知的,只有操作系统才是安全的,所以操作系统设置了权限控制用户程序无权直接访问磁盘数据,必须由操作系统来中转控制,所以就有了用户态与内核态的上下文切换,以及数据在用户空间与内核空间的来回拷贝。

read/write+fsync,数据的传输路径大致如下图,写是从上到下,读是从下到上,与磁盘的数据交互由DMA完成内存间的数据拷贝由CPU完成

再来看网络IO:用户程序无权直接读写磁盘,同样无权操作网卡,所以网络IO同样需要上下文切换,以及数据的来回拷贝

write操作只需要将数据写到本机的socket send发送缓冲区就可以返回了,操作系统会异步的将数据通过网络传到对端,但是如果send缓冲区满了,write就需要等待缓冲空出空闲空间来,这个就是写IO操作的真正耗时
read 操作只负责将数据从本地操作系统内核的接收缓冲中取出来就了事了,但是如果缓冲是空的,就需要等待数据从对端通过internet传过来,这个就是读IO操作的真正耗时,相对于内存拷贝来说,这个过程是漫长的。我们在谈论阻塞IO与非阻塞IO时,对于read操作来说,其实就是指用户线程是否需要阻塞等待数据从对端传到本地,BIO中这一步由用户线程来等,NIO和多路复用中这一步不需要用户线程关心,由专门的IO线程来等;

对比read write操作的耗时,write操作等待send缓冲区空闲的几率并不大,除非网络操作非常频繁,send缓冲区一直打满;而read操作等待数据从对端传来,对于一次等待网络响应动作来说,至少要发生一次,所以正常情况下,write比read要快很多

mmap系统调用:

mmap调用将文件地址与用户进程的虚拟地址建立映射关系,本质上是将文件对应的pageCache内存地址与用户进程的虚拟地址建立映射关系,有了这层映射关系,用户进程读写自己的地址空间(普通的内存访问),就相当于直接读写pageCache(也就是读写文件),就不需要再调用read write fsync了,相当于跨越了传统的权限控制屏障,进而为每次的读写省去一次数据在用户空间与pageCache的拷贝,以及上下文切换(当然了,mmap的设计目的中,除了简化文件的访问流程外,还有一个就是提供了进程间数据共享的手段,这个不在本文的讨论范围中)

mmap系统调用步骤:

1.从发出mmap调用的进程地址空间中划出一段连续的虚拟地址空间

2.建立页表,维护虚拟地址空间与文件地址的映射关系

3.进程发起对这块虚拟地址空间的访问,发现文件数据并不在内存页中,引发缺页异常

4.操作系统将磁盘数据拷贝到pageCache中

5.进程通过普通的内存读写即可访问到pageCache,也就是读写文件

可以看到,映射建立后,数据不需要在用户空间与pageCache中来回拷贝了,因为用户程序可以像访问普通内存一样访问到pageCache了

mmap与read write文件IO操作对比:

比read write操作多出来的步骤:1.建立映射关系 2.处理缺页中断

比read write操作节省的步骤:每次读写操作可以省去 1.一次数据在用户空间与pageCache间的拷贝  2.一次上下文切换

用户空间与pageCache间的数据拷贝,是纯内存操作,在现在的计算机中,内存操作速度已经很快了,而多出来的操作中,如果文件较大,建立映射关系要建立多个页表项,并且缺页中断会引发上下文切换,这二者的开销倒是不小。

但要注意,对于一个文件来说,映射关系只要建立一次即可,一个页也只会触发一次缺页中断。所以在文件映射完成后,如果需要对该文件进行大量的读写,那mmap就很有优势,因为节省了大量的数据拷贝和内存开销,比如消息队列的场景中,会大量读写消息日志,这就非常适合采用mmap

那我们就知道了,mmap的适用场景是读写大文件,如果文件很小,使用mmap带来的额外开销比节省的开销大,所以使用普通的read write效果更好

directBuffer(堆外内存):

当我们说到directBuffer时,一定是在java程序中,directBuffer叫堆外内存,是相对于jvm的堆内存来说的,jdk中提供了两种分配buffer的方法(buffer用于IO读写缓冲)

ByteBuffer buf = ByteBuffer.allocate(1024);

ByteBuffer buf = ByteBuffer.allocateDirect(1024);

第一种buffer就是普通的堆内存,受到GC的管理

第二种buffer是堆外内存,不受GC管理,由操作系统自行管理

二者的区别在于将buf中的数据进行IO传输时,前者会创建一个临时的directBuffer出来,把数据copy进去,再交给pageCache,而后者因为就是directBuffer,直接copy到pageCache即可,所以就少了一次copy

所以问题就在于,为啥数据要先copy到directBuffer,不能直接从堆buffer中往pageCache中copy吗?

答案是不能,数据copy是基于内存地址来的堆buffer受到GC的管控,在执行GC时,JVM实际上会做一些整理内存的工作,使得该对象在内存中的实际地址发生变化,那copy的过程就不安全了,所以只能通过一个不受GC影响的directBuffer来中转一道

mmap与directBuffer的对比:

在java程序中,mmap以及directBuffer都可以借助jdk实现,上面已经说明了这两种方式相对于普通read write的优势,mmap只在读写大文件时优势比较明显,那我们就将讨论范围放在大文件的读写上,当我们使用java程序进行大文件数据传输时,mmap和directBuffer谁更胜一筹呢?

这二者的对比,我建议你看看京东的“详解JMQ4的存储设计”一文中的"高性能IO"部分,其结论就是directBuffer更快,在多种测试场景中,directBuffer的性能大约可以达到mmap的两倍,文章里从几个方面分析了directBuffer更快的缘由,你可以仔细品品

sendfile系统调用:

在应用程序需要操作传输的数据内容时,我们可以从read write、mmap、directBuffer这几个方案中权衡,如果应用程序不需要操作传输的数据内容,比如仅仅是将某个文件发送到网络,就可以考虑sendfile

sendfile的唯一使用场景是:用户程序将文件数据发送到网络,并且用户程序无需操作文件内容。

注意,有两个重点,1.数据是要发送到网络的 2.用户程序不需要操作文件内容

在这个使用场景中,如果我们使用传统的read write系统调用,也是可以完成的,只不过效率上与sendfile差距很大,我们知道,read write需要多次数据拷贝,而sendfile称为零拷贝,那么,sendfile真的不需要数据拷贝吗

使用read write将文件数据发送到网络,其顺序就是先文件read再网络write,综合起来,一共四次上下文切换,五次数据拷贝(两次与IO设备的交互由DMA操作,三次内存拷贝由CPU操作)

如果使用sendfile发送文件数据,步骤如下:

1.应用程序发出sendfile系统调用,操作系统上下文切换到中断处理程序,切换到内核态

2.DMA将数据从磁盘拷贝到pageCache

3.sendfile调用返回,切换回应用程序,切换回用户态

4.DMA根据Socket的描述符信息,将数据从pageCache直接拷贝到网卡

综合起来,一共两次上下文切换,两次数据拷贝(都是由DMA来操作的,CPU无需参与)

比read write方式节省两次上下文切换,以及三次CPU的拷贝,我们说零拷贝,其实就是不需要CPU参与拷贝,而不是不拷贝,我们都知道,CPU资源很宝贵,所以当你遇到将文件数据发送到网络这种场景,sendfile是你应该优先考虑的

PS:如果有不足之处,欢迎指出;如果解决了你的疑惑,就点个赞吧o(* ̄︶ ̄*)o

猜你喜欢

转载自blog.csdn.net/wb_snail/article/details/106050632
今日推荐