dma 对文件读写的影响_从使用NIO读写文件说起——基于源码透彻拆解文件内存映射...

652aed9516357dd808e0ed7501f27a00.png

点击上方蓝字关注我吧!

本篇文章大概7800字,阅读时间大约10分钟

前面总结梳理了操作系统的内存管理一般套路,下面就看看NIO的一种I/O优化技术,即文件内存映射,它也是操作系统零拷贝的一种实现方案,可以参考JDK以及Netty,Kafka对操作系统零拷贝的封装,JDK封装的操作系统的零拷贝就把该机制作为了sendFile之后的一种策略,所以Netty也使用了该机制。

本文从NIO的MappedByteBuffer使用和实现源码入手,结合Linux0.11源码对它的实现进行了深入拆解。

2bd475485d6e629c8070e7af64eb08dd.gif

现代操作系统都使用了虚拟内存技术,首先程序里的变量的内存地址都是逻辑地址,经过重定位(段表)后转换为进程空间内的虚拟地址,也叫线性地址,然后通过页表,转换为物理内存的物理地址,之后才能被操作系统及CPU使用。这使得虚拟内存空间可远远大于实际可用的硬件内存空间,还能实现进程的数据隔离。

与此同时,为了改进这种转换效率,为了实现零拷贝等,诞生了一种内存映射技术,我们都知道操作系统外设的控制器不能通过DMA去直接访问用户空间,但可以将多个虚拟内存的地址提前指向同一个物理内存地址(在内核里),后续,DMA就可以通过填充对内核与进程用户空间同时可见的缓冲区,实现一种数据的零拷贝。这样就省去了内核与用户空间的实际数据拷贝动作。

以上,有个条件:即内核缓冲区与用户空间缓冲区必须使用相同的页对齐,缓冲区的大小还必须是磁盘控制器块大小(通常为512字节扇区)的倍数。这里结合操作系统的内存管理套路看,操作系统把物理内存地址划分为了页,一般为了区分程序页,而叫页框,这些页框本质就是固定大小的字节组。且内存页的大小总是磁盘块大小的倍数,通常为2次幂,这样可简化寻址操作,使用位运算快速计算。所以物理内存典型的内存页规格要么为1024、2048,要么就是4096等字节。如下是一个示意图:

f270c9bec56be6e05e2b6375e7a25075.png

下面,再看NIO提供的文件内存映射工具——MappedByteBuffer类,仍然以ByteBuffer为例,前面分析的DirectByteBuffer(从使用NIO读写文件说起——拆解堆外内存分配过程)还继承了一个MappedByteBuffer类,如下:

af44681e87548873711009349da20b88.png

MappedByteBuffer,这个命名也非常给力,顾名思义就是一个有映射内存机制的buffer类,它本身也是一个直接缓冲区,即分配的堆外内存,它包含了一个文件的内存映射区域(memory-mapped region of a file),这个工具类的对象可以通过NIO的FileChannel#map方法创建,如下创建了一个文件内存映射对象,它能读能写,我设置的是读写模式都支持,并且从0开始映射文件的5个字节:

MappedByteBuffer mappedByteBuffer = fileChannel.map(FileChannel.MapMode.READ_WRITE, 0, 5);

如下是map的声明:

    public abstract MappedByteBuffer map(MapMode mode,                                         long position, long size)        throws IOException;

以上,得到的MappedByteBuffer对象以及它关联的文件内存映射区域会被GC管理,当MappedByteBuffer对象本身被回收时,其映射的区域也就被删除了,对用户完全透明。

理解文件的内存映射(memory-mapped region of a file):它是允许程序能直接在内存中访问文件的一种技术,即一个磁盘上的文件会将其内容映射到内存里,而且不是将整个文件都读到内存中,一般来说只有文件中实际读取或者写入的部分才会映射到内存中,后续只需在内存中操作这块映射区域即可,无需每次都和磁盘交互,最终更新的数据都会写到文件里,涉及的物理页请求调度以及内存上的修改,写入到磁盘等都是操作系统控制的,Java程序只需和内存交互即可,其它不用管。如此一来,它比常规的基于流或者基于Channel的文件I/O要快的多。因为这块内存映射文件区域本身就是堆外内存,而且操作期间也不需要和磁盘交互。

如下面的示例代码:

aaeb8527bdf0039b5d224f010c919b4d.png

循环10万次get一个字节,结果如下:

791e8a27c9f20a2f63e2feebf6dc35d0.png

再看用普通I/O流的方式修改该文件:

250b69ccc23f855b8ff3aaea50d478e4.png

循环10万次read一个字节,速度差了近70倍左右:

648dbaf72c64ed64be2da4fdf7c9f9e0.png

再看用NIO的文件Channel的方式,我使用了堆外内存分配buffer:

cb140506187cd3daf33c3bae09ffa603.png

循环10万次read一个字节到堆外分配的buffer,结果如下。虽然比普通I/O流快多了,但是和MappedByteBuffer比还是慢了5倍左右:

a99585fc2f14f9e79f501144c7782e76.png

以上,虽然它的I/O速度很快,但是该工具类的资源释放比较头疼,Java没有给出类似closed的方法,官方说法是依赖GC什么时候回收MappedByteBuffer对象,会顺带清理,这不同于堆外内存回收,文件映射区域往往比较大,有堆外的OOM风险,所以为了保险,可以手动清理之,具体做法,可以参考Netty封装JDK的unsafe工具包清理堆外内存的做法,一般思路就是结合安全管理器(学会安全管理器的用法,你也可以魔改底层依赖库!)+反射拿到控制权,然后修改需要用的属性为public,访问后调用相关清理API即可。具体参考:从使用NIO读写文件说起——拆解堆外内存分配过程

也可以参考JDK是怎么用的(JDK以及Netty,Kafka对操作系统零拷贝的封装),在FileChannelImpl.java#transferTo内(只看写的过程):

5f364ecfb3be38b27944b7daeaf51a09.png

第二优先实现零拷贝的方法,是调用的transferToTrustedChannel:

e875c070dda3999f95dc03c69107ad8f.png

红线处的map方法调用的就是FileChannel的map,具体实现就在FileChannelImpl类,通过它创建MappedByteBuffer工具类对象,如下核心调用了一个mapInternal方法,也可以学习JDK的这种命名规则,内部的封装加个internal后缀,Netty这种框架一般会习惯叫doMap,或者map0(品Netty源码,学习良好的编程习惯(1)):

558de73d323ebd869849a60bb7cde43d.png

下面是mapInternal的核心实现片段,红色1处,addr就是一个long类型的变量,用来保存映射区域的首地址,该区域的分配和首地址获取通过map0方法实现,果不其然,JDK也是这种命名习惯,最后实在没名字可选了,就后面加个0。。。另外要知道mapInternal方法是线程安全的,JDK给核心逻辑加了synchronized锁:

5fd65f3caee96edcdf10744086289d26.png

以上,在红色2处,可知一旦map期间发生了OOM,就手动调用一次System.gc,很多人或者文章说不要用这个方法,但是JDK它老人家也悄悄用了。。。

我个人理解,只要透彻掌握了某个机制(API)的原理和各种可能的副作用,为什么不敢用呢?!JDK就敢用,因为这是他们写的。。。同理还有JDK的Unsafe工具包,Netty就敢用,用得还挺好。说明不敢用或者不建议用,还是对自身能力的不自信啊。。。我承认自己菜就完事儿了。。。

言归正传,这样设计的目的注释也写了,是再次尝试分配内存映射区域,强迫GC后,第二次分配失败才真正的抛出异常。另外,System.gc还有一种写法,如下:

java.lang.System.gc()是java.lang.Runtime.getRuntime().gc()的简写

e774f84574d8807d49f069c21c7b0634.png

下面看看JVM的权威之一,开发过JVM的中国人——R大是怎么说的(PS.如果对JVM有任何疑问和有对网上文章的JVM的知识点的怀疑,那么去知乎看R大的相关回答,甚至给他提问,一般就是标准答案),如下,有个精神小伙儿问了这个System.gc到底会不会执行GC,为啥不建议手动用:

3ec4f9db71967f47b57dc0ede6570c53.png

R大一顿吐槽,小伙儿没理解到位,一句话——JVM规范是规范,和具体实现是tnd的两码事!类比TCP/IP标准,具体的协议栈实现不同厂商也是不一样的。我们一般都是用的HotSpot虚拟机,该机子(大部分JVM)是如下实现的System.gc:默认会立即执行GC,并且GC完成后该方法才返回,并且有参数可以配置它的调用行为。所以JDK这里调用System.gc没毛病啊。。。确实是有必要调用的时候,JDK调用了,只不过建议大家不用和写C/C++代码那样,经常性的析构内存,毕竟Java的GC频繁还是影响性能的,而且Java会自动触发GC,不需要用户关心就能实现最优解,你主动触发GC也保证不了GC的最佳时机,这也是Java语言设计的目的之一。所以这里也澄清了一个误区——可以用,大部分JVM都会立即GC,且只有必要的时候,比如可能OOM的时候用一下子。

言归正传,下面简单看下这个map0,它是一个native方法:

private native long map0(int prot, long position, long length, boolean isSync)    throws IOException;

它是MappedByteBuffer实现的精髓,最终它会调用如下JVM里的实现方法,代码比较长只看核心: 

350bc91cbec024062790f0b1f57f24b9.png

无非就是用C代码调用了操作系统(以Linux为例)的mmap系统调用,如下是核心片段: 

498437f3ea5cca0b16f404eb1de0e777.png

一句话,本质就是Linux的mmap,然后将分配好的内存映射区域的首地址返回给Java即可。理解了这个,也就知道了JDK的文件内存映射没什么新鲜东西,关于mmap API的各种用法,乃至于C的源码级拆解,网上有的是,不属于Java范畴,这里不多说了。

84dd6c1412382cea20ad872f1f665f0b.png

大概梳理总结下mmap基本原理吧,遇到类似面试也可以说下。如下都在一个图里了,左边的进程空间是代表虚拟地址,然后将一个文件或者其它对象的物理地址映射到进程虚拟地址。此后进程空间的代码可以用指针去操作这段内存,而操作系统内核会自动回写脏页(程序的页,此处指文件)到磁盘,即完成了对文件的操作而不必再调用read和write等系统调用,反之一样。并且多个进程都可以这样映射到内核的一块区域,从而可以实现不同进程间的文件共享:

4479a697658d712ea457ea96d7f07fa9.png

这样解释其实等于没说,也就是停留在背答案的阶段。。。不如看代码吧,看下面一段C语言demo,即绕过mmap系统调用,通过C语言直接实现一个内存共享的程序。

首先,思考这个基本原理,肯定需要获得一个空闲的物理页框吧,即内存页,因为现代操作系统的内存都是段页结合的管理模型,内存的最小使用粒度是【页】框,而进程的虚拟内存也是以页为单位切割的,两者大小一样。回忆这个图:

530812c961404a9ddeb7d6af6e3e8404.png

所以mmap的实现也是这样,提供的补充映射区域的大小必须是物理内存页大小的整倍数(32位系统中一般一页=4kb)。

下面是Linux0.11早期内核源码里的get_free_page()函数的实现,这都是Linus大神写的。。。膜拜一下,它可以用来获取一个空闲内存页,返回的是内存页的首地址,找不到就返回0,大神在内部直接用asm关键字调用了汇编指令,高效完成。具体意思百度都有,也可以参考《Linux内核完全剖析》这本神书,不多说了:

88524eed9fd3932cef3ac1bb1b709969.png

使用方式如下,一般可以使用一个hash表,保存已经拿到的空闲物理页,key是共享内存页的ID,value是页的首地址,即调用系统的get_free_page()函数获得了一页空闲的物理内存页,返回起始地址给phyAddress变量,并存储到hash表,如果phyAddress为0,说明获取失败,那么就主动调用C语言的malloc函数,申请一页新的物理内存(其实此时还没有分配物理内存,先在用户空间分配虚拟地址,运行时才映射对应的物理页),也就是说先去看看有没有废弃的不被使用的内存页,如果没有才申请新的。

f82f1387b2429b1a0644c989712362fa.png

拿到了空闲的物理页,下一步就是获取当前进程空间的一页空闲的虚拟地址区域,这需要对进程的PCB以及进程空间内存布局有个大概了解(参考https://www.cnblogs.com/HKUI/articles/9080214.html,http://www.360doc.com/content/14/0628/15/7821691_390500628.shtml

https://blog.csdn.net/qq_26768741/article/details/54375524):

以上得到如下结论:执行程序时,系统首先在内核空间中创建一个进程,为这个进程申请PCB(进程控制块),即内核空间会存储PCB(进程控制块),它的结构体是task struct,负责管理进程的所有资源,也是进程存在的唯一标志,它里面有个成员mm_struct,描述了这个进程的虚拟内存空间,本质也是指向了一个结构体,可以说,mm_struct是对进程整个用户空间的描述

下面就是mm_struct结构体表述的进程用户空间内存布局:

475e959d1da325846f92465546e5a126.png

如下是一个感性的认识,高地址在进程空间最上面布局,下面是栈,向下增长内存,意思就是栈顶在低地址,之后就是堆区了,堆区挨着数据段空间:

46e0ce601c2abf5b2c94a5818d7495e2.png

我们常说的:用户栈区,堆区,BSS段,数据段,代码段等都在这里了,本质还是虚拟内存,其中所谓的动态内存就是堆空间,栈空间,mmap映射区,在Linux里,操作系统自动分配的是栈空间内存,使用malloc这种函数分配的是用户态的堆内存空间(本质还是虚拟地址)。其中,代码段(text)主要存放代码指令和常量数据(比如字符串常量)这部分区域在程序运行前就已确定,并且通常只读, 某些架构也允许代码段为可写,即允许修改程序。再看数据段(data)存放全局或静态的已初始化的变量,属于静态内存分配而高地址的1G虚拟空间是内核专用,用户无法处理

堆和栈的起始地址默认是随机产生的,其目的是避免安全漏洞,但可以指定堆中申请的起始地址,通过一个叫brk的系统调用就能申请:

int brk(void *addr); //指定下次申请堆空间的起始地址为addr

以上使用brk,就能拿到一页空闲的虚拟地址区域,而PCB结构体里的mm_struct结构体里已经有个叫brk的字段,如下:

137778c4597d44c9ea880221aaea9209.png

brk这个long值,指向的是当前进程空间里data段的末尾。即malloc函数分配的堆内存,都是以brk作为起始地址的。所以可以拿到这个brk的值,然后尝试增加一页内存的大小,先看看返回的值是否大于了栈的起始地址,而一般来说,默认堆栈大小为8388608,堆栈最小为16384=2的14次,单位为字节。所以可以这样判断:

if(brk + PAGE_SIZE > start_stack - 16384){    说明选出的这一页空闲虚拟内存的地址,会覆盖进程的栈空间,代表分配失败} else {开始真的分配一页空闲虚拟内存}

其中的start_stack就是栈的起始地址,也被定义在了PCB。上面伪代码的完整demo如下,因为进程空间里的地址本质都是段内逻辑地址(此时还没有进行重定位,重定位是运行时进行),所以需要使用Linux的get_base函数拿到当前段的基址加上这个逻辑地址才是虚拟地址——即提前做了段表应该做的事情,这里需要拿数据段,而当前数据段的段号可通过ldt[2]获取,具体可以参考Linux的函数change_ldt的源码,它是用来修改局部描述符,即段表的,https://blog.csdn.net/weixin_30394251/article/details/96675008

5cbad0176af28f172fa4a8a6d664c9c7.png

成功拿到一页虚拟内存页后,调用Linux的函数put_page去修改页表——建立虚拟内存页和物理内存页的映射关系,即添加了一个页表项。这样就能把两者关联起来,即设置页表——包括页目录号和页号,然后就能拿到物理页号,所以总算明白了什么tnd的映射,其实就是用人工的方式在提前设置页表表项而已。回忆:

66332bab1721808d63e418d7c03e91dc.png

关于put_page的具体实现,可以参考:https://blog.csdn.net/linpeng12358/article/details/41450593,而通过虚拟地址,设置页目录号,业号是很简单的事情了,按照如下格式计算即可:

4cd343d81db275a7151b5f86cbaf1ad5.png

以上demo,还有一个重要函数的调用,即incre_mem_map,可以参考mem_map数据结构,即linux内核所有的物理内存页都用struct page结构来描述,这些对象以数组形式存放,而这个数组的地址就是mem_map,里面的一个字节代表一页内存,每个页面对应的字节值用来标记该页面被占用的次数,一旦映射到虚拟内存,该页对应的字节就需要加1处理,否则会出bug:

https://blog.csdn.net/sunlei0625/article/details/79276490

void incre_mem_map(unsigned long PhyAddr){    PhyAddr-=LOW_MEM;    PhyAddr>>=12;    mem_map[PhyAddr]++;}

以上大体思路就是这样,如果是实现多个进程的共享内存,即通信,那么可以再申请一个进程空间,然后找到一个空闲虚拟页,映射到一样的物理页框即可。

回到文件内存映射,Linux也是基于这个思路实现的,只不过工程上比这个复杂,考虑的点也多,实际上上面的demo仅仅是演示实现的思路,在实际操作系统里这样做肯定有漏洞,比如仅仅是映射了一页。。。一些内存安全使用等很多地方也没有考虑。。。具体水平有限。

实际上,mmap的实现是先在进程虚拟地址空间中寻找一段空闲的满足要求的连续的虚拟地址(不一定是一页),然后对文件的物理地址和进程的虚拟地址进行页表项的填充一一映射,这涉及到了文件系统,实现复杂不总结了,知道就是这么个原理即可。

具体的简单说,在初始化文件内存映射区域后,虚拟地址并没有任何数据关联到内存。故当进程首次访问映射区域时,必然会触发缺页中断,内核开始请求调页,将文件内容拷贝到物理内存对应的页框。也就是说真正的文件读取到内存,仍然是当进程发起读或写操作时。如果发起的写操作改变了文件内容,一定时间后内核会自动回写脏页到磁盘,也即完成了写入到文件的过程。也就是说修改过的脏页并不会立即更新回文件,而是有一段时间的延迟,可以调用msync()来强制同步, 这样所写的内容就能立即保存到文件里了。也就是说NIO也有一样的问题。同样NIO提供了一个类似的方法:

31270183482d2f91e0876889f8af1bf7.png

以上的force0方法是一个native方法,最终调用到如下C代码,封装的就是Linux的msync()函数:

d26ebc9a29911ef4bd7141d85e2a1e37.png

以上,通过文件内存映射,相当于将磁盘上的文件所在空间建立成一块虚拟内存,程序访问时可按访问内存的方式进行,省去了普通IO的一些环节,其实真正要读写操作时,会进行页面置换,这是无法避免的。总之文件内存映射是应用虚拟内存的技术来达到加速文件I/O的。

 78c4063d54a0c7c846bd867315eb6c74.png

以上,应该可以比较明白,清晰的理解这个东西了,我想,这应付面试足够了吧

下面言归正传,FileChannelImpl.java的map方法实现了文件内存映射,内部通过mapInternal方法封装了一些系统调用实现的:

558de73d323ebd869849a60bb7cde43d.png

之后,分配虚拟映射区域成功后,在Java层面会返回一个对象——Unmapper,如下是它的类结构,一个内部类,里面保存了虚拟映射区域的首地址(虚拟地址)address,区域大小size,源文件的文件描述符等信息:

ad628e078553c9fb5f6cd6dd69d31689.png

同时它还是一个Runnable,即红色代码2处,会在额外的线程里调用unmap方法,即尝试回收这块儿区域。暂且不表,继续看map方法剩余的关键代码,我的demo是从transferTo方法进入的,故只看写的逻辑,即黄线处代码——调用了NIO的一个工具类Util的静态方法newMappedByteBuffer来初始化MappedByteBuffer对象:

46e3c6a9c96f55da467a46bd95dfc662.png

以上,也能看出,在读虚拟映射区域时会使用只读缓冲区封装,可以参考NIO的安全保障:认识只读缓冲区。细节不再多说了。

下面看看JDK如何对文件内存映射区域进行回收的,回到transferTo方法,看其内部的transferToTrustedChannel,在拿到文件映射对象后dbb后(这jb啥名字啊)开始调用目标文件Channel的write方法进行写入操作:

4fcfc1e48b311918a5921b7b575aacd5.png

写完后,会在finally块里调用unmap方法,似曾相识,回忆从使用NIO读写文件说起——拆解堆外内存分配过程,即这里JDK仍然是依靠了Cleaner机制(虚引用+清理线程)对堆外内存手动进行强制清理,默认情况时只有GC发生开始清理堆外内存的引用的Java对象时,才开始顺带清理堆外内存:

69b60168f1128769d2f05791f42694e9.png

那么文件内存映射区域时什么时候和Cleaner机制关联的呢?回忆NIO的工具类Util的静态方法newMappedByteBuffer,在初始化MappedByteBuffer对象时,会通过反射将unmapper设置到堆外内存缓冲区对象里(DirectByteBuffer是MappedByteBuffer的子类):

51a2e6e286f652457089203e0847858e.png

如下是DirectByteBuffer的一个构造器,这也是为啥newMappedByteBuffer用反射初始化它,因为都是保护了,该构造器注释也写到,专门为mmap留的,内部就和DirectByteBuffer的创建回收过程一样了,只不过这个address存的是虚拟映射文件区域的首地址了。而且此时unmapper就和Cleaner机制关联,最终调用Cleaner的clean方法,就会触发unmapper这个Runnable的run(在清理线程里执行),从而实现清理虚拟映射文件区域。

4f401f7969078f349f694c0c0957a7ee.png

简单看下unmapper的run的逻辑,如下,JDK做的也很细致了,即关闭件描述,清理address,然后调用了unmap0清理虚拟文件映射区域:

61b16a077b528c75aba22c49fc489a59.png

unmap0本质是native方法:

// Removes an existing mappingprivate static native int unmap0(long address, long length);

最终调用到C语言的munmap函数执行相反的操作——删除特定映射地址区域的对象映射,而且一般说来进程在映射空间的对共享内容的改变并不直接写回到磁盘文件,往往在调用munmap或者msync()函数后才执行实际同步的操作。。 前面也提到了,NIO就是调用force方法:

9f9cccd3ceea107dea44f4173c52892d.png

关于munmap,可以参考https://www.cnblogs.com/sky-heaven/p/5691692.html

以上,实际项目里也可以模仿JDK的这种安全的用法。。。或者用Netty的那种Unsafe的清理方式也OK,不过需要反射+安全管理器去获取address。

小结:

1、System.gc也可以用起来,比如JDK分配mmap时,这种极端需要立即回收内存的场景不要惧怕它,它的实现和JVM规范是两码事。

2、理解操作系统级别的数据传输优化思路——DMA,mmap,sendFile等都是怎么回事即可,一个技术人的基本素养吧。

3、知道Netty,Kafka为啥会高性能,学习设计思想。

4、NIO的内存文件映射对程序的提速,只在处理大文件或非常频繁的文件读写操作时效果才明显。

5、实际文件的变化,不是实时的,有一定延迟,取决于GC时间,在Java里就是GC了MappedByteBuffer对象时,或者用户主动调用了MappedByteBuffer的force方法,不过可以手动清理,模仿JDK的做法吧。

6、为啥文件内存映射快?

最终还是落到了零拷贝上,即通过文件内存映射,相当于将磁盘上的文件所在空间建立成一块虚拟内存,程序访问时可按访问内存的方式进行,省去了普通I/O的一些环节,其实真正要读写操作时会进行页面置换,这是无法避免的。总之,文件内存映射是应用虚拟内存的技术来减少了数据的拷贝次数,用内存读写取代I/O读写,提高了文件读取效率。

另外以上还是表面的一种解释,更深层的:如果进程A和进程B都映射了区域C,当A第一次读取C时通过缺页中断从磁盘拷贝文件页到内存;当B再读C的相同页时,虽然也会产生缺页异常,但不再需要从磁盘中拷贝文件了,而是可以直接使用已经保存在共享内存中的文件数据。即减少了请求调页的次数,即缺页率降低了,进程的执行效率(有效工作时间多了)必然是提高的。

7、知道进程间通信的一种实现手段,就是通过共享内存实现,本质原理仍然是虚拟内存到物理内存的映射。

8、凡是需要用磁盘代替内存的时候,mmap都可以发挥其功效

9、文件内存映射建立后,即使文件描述符被关闭,这个映射的表项依然存在。因为映射的是磁盘的地址,不是文件本身,和文件描述符无关。所以还需要调用munmap函数,NIO里就是要么依赖GC,要么模仿JDK,手动调用MappedByteBuffer(需要强转为DirectByteBuffer)对象的cleaner的clean方法清理,即堆外内存的顶级接口里有cleaner方法就是干这个的,子类都能使用。或者模仿Netty,基于JDK的Unsafe清理堆外的内存。不过考虑实现代码量,还是模仿JDK的做法吧。

39bc6d96b98188a85f72586a82e104e1.png

10、传统I/O是操作系统把磁盘中的文件读入内核空间,然后再拷贝到用户空间中,用户才能使用,这和网络I/O一样的流程。这中间多了一个Buffer拷贝的过程,如果这个量够大的话,那么是很浪费时间的。

END

点亮在看,你最好看

点击此处写留言~

933bc731a24a6090d70badd1ea96d91c.png f09c4670596b328f3892d6e6eed79f65.gif

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值