(理论篇)从基础文件IO说起虚拟内存,内存文件映射,零拷贝
为了快速构建项目,使用高性能框架是我的职责,但若不去深究底层的细节会让我失去对技术的热爱。
探究的过程是痛苦并激动的,痛苦在于完全理解甚至要十天半月甚至没有机会去应用,激动在于技术的相同性,新的框架不再是我焦虑。
每一个底层细节的攻克,就越发觉得自己对计算机一无所知,这可能就是对知识的敬畏。
新IO和传统IO-intsmaze
新IO和传统IO都是用于进行输入/输出。
新IO采用了内存映射的方式来处理输入/输出,新IO将文件或文件的一段区域映射到内存中,这样就可以像访问内存一样访问文件了,通过这种方式比传统的输入/输出要快的多。通过内存映射机制操作文件比使用常规方法和使用FileChannel读写高效的多。
传统IO操作-intsmaze
传统的文件IO操作中,调用操作系统提供的底层标准IO系统调用函数 read()、write() ,调用此函数的进程(在JAVA中即java进程)由当前的用户态切换到内核态,然后OS的内核代码负责将相应的文件数据读取到内核的IO缓冲区,然后再把数据从内核IO缓冲区拷贝到进程的私有地址空间中去,这样便完成了一次IO操作。
传统IO的优化-intsmaze
为了减少磁盘的IO操作,同时程序访问一般都带有局部性,局部性原理,OS根据局部性原理会在一次 read()系统调用过程中预读更多的文件数据缓存在内核IO缓冲区中,当继续访问的文件数据在缓冲区中时便直接拷贝数据到进程私有空间,避免了再次的低效率磁盘IO操作。
其过程如下:
为什么要搞一个内核IO缓冲区把原本只需一次拷贝数据的事情搞成需要2次数据拷贝呢?
这么做是为了减少磁盘的IO操作,为了提高性能而考虑的,程序访问一般都带有局部性,局部性原理,在这里主要是指的空间局部性,即我们访问了文件的某一段数据,那么接下去很可能还会访问接下去的一段数据,由于磁盘IO操作的速度比直接访问内存慢了好几个数量级,所以OS根据局部性原理会在一次 read()系统调用过程中预读更多的文件数据缓存在内核IO缓冲区中,当继续访问的文件数据在缓冲区中时便直接拷贝数据到进程私有空间,避免了再次的低效率磁盘IO操作。
新IO-intsmaze
讲新IO前先讲讲背景知识,虚拟空间。
虚拟空间-intsmaze
很久很久以前的存储管理技术必须将作业全部装入内存才能执行且作业常驻内存直到运行结束,难以满足较大作业或较多作业进入内存执行。 为了能让作业的一部分装入就可以运行的存储管理技术叫做虚拟内存管理技术。
现代操作系统中的进程在使用内存的时候,都不是直接访问内存物理地址的,进程访问的都是虚拟内存地址,然后虚拟内存地址再转化为内存物理地址。 虚拟内存就是硬盘中的一块区域,它用来存放内存里使用频率不高的页面文件,让使用频率高的页面文件活动在内存区域中,提高CPU对数据操作的速度。
进程看到的所有地址组成的空间,就是虚拟空间。虚拟空间是某个进程对分配给它的所有物理地址(已经分配的和将会分配的)的重新映射。 在Linux中,这个区域叫做swap,一般大小应设置为物理内存的2倍。详情见 https://blog.csdn.net/fengxinlinux/article/details/52071766
局部性原理-intsmaze
大多数程序执行时,在一个较短的时间内仅能使用程序代码的一部分,相应的,程序所访问的存储空间也局限于某个区域,这就是程序执行的局部性原理。
基于局部性原理,在程序装入时可以将程序的一部分放入内存,而将其余部分放在外存,然后启动程序(部分装入)。在程序执行期间,当所访问的信息不在内存中,再由操作系统将所需的部分调入内存(请求调入)。另外,系统将内存中暂时不用的内容置换到外存上,腾出空间存放将要调入内存的信息(置换功能)。
页式虚拟地址与内存页面物理地址转-intsmaze
虚拟地址转化为真实地址的时候,不一定会对应内存地址,还可能对应硬盘地址。 内存的一个地址一般对应1byte,硬盘的一个地址一般对应512byte(一个磁盘扇区).
内存和硬盘里的数据做交换时,也就是把一个内存地址对应的数据拷贝到硬盘里或者反过来把硬盘数据拷贝到内存里,想要方便处理操作系统会统一单位(传说中的页对齐)。 页就是一个统一的单位,页的大小总是磁盘扇区大小的倍数,通常是2次幂,比如1024字节。
有了页这个统一单位,接下来我们说的虚拟地址、内存地址、磁盘地址都是对应的一个页。页式虚拟地址与内存物理地址建立一一对应的页表(硬件地址变换机构来执行转换)。将逻辑地址上连续的页号映射到物理内存中称为离散的多个物理块(页面),将页面和物理块一一对应,体现在页表。(页表由页号和块号组成)
虚拟地址空间可以大于实际的内存空间,比如实际内存大小是1G,但是虚拟地址空间可以是4G。这样在操作系统中的普通应用程序看来,就好像是有4G的可用内存。
虚拟地址空间可以大于实际内存空间,这是怎么实现的呢?
比如我实际内存1G,虚拟内存设成了4G,现在往4G的虚拟内存里放了4G的数据,那么当前只有1G的数据在真实内存中,另外的3G因为装不下就只能以文件形式放到硬盘里,这个存放内存内容的硬盘文件就叫页面文件。
虚拟内存的空间=物理内存+页面文件。
页式管理-intsmaze
各进程的虚拟空间被划分为等的页若干个长度相,页长1K—4K。进程虚拟地址变为由页号P与页内地址W组成。 同时也把内存分成与页面大小相等的区域,称为页面。用户进程在内存空间除了在每个页面内地址连续之外,每个页面之间不再连续。
操作系统层面优化提升程序执行效率-intsmaze
1,设置虚拟内存大小-intsmaze
swap空间就是虚拟内存,在物理内存不足时,有较大的用处。
查看内存空间大小:free -m // m表示显示的字节单位是m(megabytes)
用命令free查看系统内 Swap 分区大小。
free -m total used free shared buffers cached Mem: 1002 964 38 0 21 410 -/+ buffers/cache: 532 470 Swap: 951 32 929 可以看到 Swap 只有951M
如何修改百度即可。
2,设置实际内存和虚拟内存进行数据交换的倾向性-intsmaze
vm.swappiness是Linux内核的一个参数,范围是0~100。它表示实际内存和虚拟内存区域进行数据交换的倾向性大小,数值越大表示倾向性越大,即交换的页面文件越多,反之亦然。一般默认值为60。可用'cat /proc/sys/vm/swappiness’查看。
这个值应该设置成多大才能提高Linux的性能呢?
以下摘自 https://blog.csdn.net/liu870915/article/details/51860932
这个当然要由具体的环境来定了。在一台CentOS机器上,分别把值设为0,60,100,下面是运行'vmstat -S M 5’的三次数据报告。(vmstat命令是用来查看虚拟内存状况的,参数-S M表示以M为单位,5表示每5秒钟产生一次报告。)这里主要关注bi,bo和wa这三个值,bi代表每秒钟从硬盘读入数据的块数(因为硬盘是块设备),bo表示每秒钟写入硬盘数据的块数,wa表示CPU等待IO设备就绪的时间。 当值为100时,wa基本为50左右的值,这表示50%的CPU时间都在等待IO设备就绪(大好的CPU资源就这样被浪费了!)现在你明白瓶颈在哪里了吧?对,就是硬盘。 说明我实验的这台机器硬盘IO的处理能力是最影响性能的了。那么该怎么解决呢?当然了,换个转速更快的硬盘当然可以,还有呢?增加内存有可能也可以。增加了内存以后,再把swappiness的值设小点,以减少硬盘IO的操作。内存够大时,无论页面文件的使用频率是高还是低都放在内存里,无须使用虚拟内存。 但是在这个例子中,swpd的值始终为0,这表示没有虚拟文件被使用。这说明内存容量是足够的,即使再增加内存,作用也不大。最好的办法就是更换硬盘了。 如何改变swapiness的值?你可以运行'echo 数值 > /proc/sys/vm/swapiness’ 或者 'sysctl –w vm.swappiness = 数值' 来修改内核中的实时参数。如果想机器在重启之后仍然保持这个数值的话,就需要在'/etc/sysctl.conf’文件中加上'vm.swappiness = 数值' 这一行。
新IO-内存映射文件-intsmaze
传统IO中当对文件进行操作的时候,一般总是先打开文件,然后申请一块内存用做缓冲区,再将文件数据循环读入并处理,当文件长度大于缓冲区长度的时候需要多次读入。
内存映射文件是将一个文件直接映射到进程的进程空间中(“映射”就是建立一种对应关系,这里指硬盘上文件的位置与进程逻辑地址空间中一块相同区域之间一 一对应,这种关系纯属是逻辑上的概念,物理上是不存在的),这样可以通过内存指针用读写内存的办法直接存取文件内容。
在内存映射过程中,并没有实际的数据拷贝,文件没有被载入内存,只是逻辑上放入了内存,具体到代码,就是建立并初始化了相关的数据结构,这个过程由系统调用mmap()实现,所以映射的效率很高.
经验表明,内存映射IO允许加载不能直接访问的潜在巨大文件,在大文件处理方面性能更加优异。它的不足是增加了页面错误的数目(由于操作系统只将一部分文件加载到内存,如果一个请求页面没有在内存中,它将导致页面错误)。
映射文件区域的能力取决于于内存寻址的大小。在32位机器中,你不能一次访问超过4GB或2 ^ 32(以上的文件),只能分批映射。
内存映射文件优化本质-intsmaze
mmap()是系统调用,没有进行数据拷贝,数据拷贝是在缺页中断处理时进行的,由于mmap()将文件直接映射到用户空间,所以中断处理函数根据这个映射关系,直接将文件从硬盘拷贝到用户空间(没有拷贝到内核空间),只进行了一次数据拷贝 。
从硬盘上将文件读入内存,都是要经过数据拷贝,并且数据拷贝操作是由文件系统和硬件驱动实现的,理论上来说,拷贝数据的效率是一样的。
内存映射文件的效率比标准IO高的重要原因就是因为少了把数据拷贝到OS内核缓冲区这一步,内存映射只拷贝一次效率要比read/write 拷贝两次高。
虚拟内存与内存映射文件的联系-intsmaze
虚拟内存是内存映射文件的基础,内存映射文件的底层还是依赖虚拟内存。虚拟内存和内存映射文件都是将一部分内容加载到内存,另一部分放在磁盘上,二者都是应用程序动态性的基础,由于二者的虚拟性,对于用户都是透明的.
虚拟内存是硬盘的一部分,是计算机RAM(随机存取存储器)与硬盘的数据交换区,因为实际的物理内存可能远小于进程的地址空间,这就需要把内存中暂时不用到的数据放到硬盘上一个特殊的地方,当请求的数据不在内存中时,系统产生缺页中断,内存管理器便将对应的内存页重新从硬盘调入物理内存。
内存映射文件是由一个文件到一块内存的映射,使应用程序可以通过内存指针对磁盘上的文件进行访问,其过程就如同对加载了文件的内存的访问,因此内存文件映射非常适合于用来管理大文件。
虚拟内存与内存映射文件的区别-intsmaze
虚拟内存实现的基础是分页机制和局部性原理,架构在物理内存之上,其引入是因为实际的物理内存运行程序所需的空间,即使现在计算机中的物理内存越来越大,将所有运行着的程序全部加载到内存中非常不现实。
内存映射文件虚拟性并不是由于局部性,而是使进程虚拟地址空间的某个区域建立映射磁盘文件的全部或部分内容,通过该区域可以直接对被映射的磁盘文件进行访问,而不必执行文件I/O操作也无需对文件内容进行缓冲处理。
用图来表示mmap,即为如下所示。mmap函数会在内存中找一段空白内存,然后将这部分内存与文件的内容对应起来。我们对内存的所有操作都会直接反应到文件中去。mmap的主要功能就是建立内存与文件这种对应关系。所以才被命名为memory map。
此图为 Linux 中进程的虚拟存储器,即进程的虚拟地址空间, 32 位操作系统,就有2^32 = 4G的虚拟地址空间,
图中有一块区域: “共享库的内存映射区域” ,这段区域就是在内存映射文件的时候将某一段的虚拟地址和文件对象的某一部分建立起映射关系,此时并没有拷贝数据到内存中去,而是当进程代码第一次引用这段代码内的虚拟地址时,触发了缺页异常,这时候OS根据映射关系直接将文件的相关部分数据拷贝到进程的用户私有空间中去。
请收看下节内容-intsmaze
自此文件IO的演化理论依据介绍完了,下一篇将会基于java的源码去看各种实现。
文章内容参考:
《深入理解计算机系统(原书第三版3)》,《清华大学计算机系列教材:计算机操作系统教程(第4版)》,示例图片来源于其他博客。