在linux下,使用top,free等命令查看系统或者进程的内存使用情况时,经常看到buff/cache memeory,swap,avail Mem等,他们都代表什么意思呢?这篇文章将来聊一聊Linux下的内存管理并解答这个问题。
虚拟内存
虚拟内存是Linux管理内存的一种技术。它使得每个应用程序都认为自己拥有独立且连续的可用的内存空间(一段连续完整的地址空间),而实际上,它通常是被映射到多个物理内存段,还有部分暂时存储在外部磁盘存储器上,在需要时再加载到内存中来。
每个进程所能使用的虚拟地址大小和CPU位数有关,在32位的系统上,虚拟地址空间大小是4G,在64位系统上,是2^64=?(算不过来了)。而实际的物理内存可能远远小于虚拟地址空间的大小。
虚拟地址和进程息息相关,不同进程里的同一个虚拟地址指向的物理地址不一定一样,所以离开进程谈虚拟地址没有任何意义。
注意:网上很多文章将虚拟内存等同于交换空间,其实描述不够严谨,交换空间只是虚拟内存这张大蓝图中的一部分。
虚拟内存和物理内存的关系
当进程执行一个程序时,需要先从先内存中读取该进程的指令,然后执行,获取指令时用到的就是虚拟地址,这个地址是程序链接时确定的(内核加载并初始化进程时会调整动态库的地址范围),为了获取到实际的数据,CPU需要将虚拟地址转换成物理地址,CPU转换地址时需要用到进程的page table,而page table里面的数据由操作系统维护。
注意:Linux内核代码访问内存时用的都是实际的物理地址,所以不存在虚拟地址到物理地址的转换,只有应用层程序才需要。
为了转换方便,Linux将虚拟内存和物理内存都拆分为固定大小的页,x86的系统一般内存页大小是4K,每个页都会分配一个唯一的编号,这就是页编号(PFN).
从上面的图中可以看出,虚拟内存和物理内存的page之间通过page table进行映射。进程X和Y的虚拟内存是相互独立的,且page table也是独立的,它们之间共享物理内存。进程可以随便访问自己的虚拟地址空间,而page table和物理内存由内核维护。当进程需要访问内存时,CPU会根据进程的page table将虚拟地址翻译成物理地址,然后进行访问。
注意:并不是每个虚拟地址空间的page都有对应的Page Table相关联,只有虚拟地址被分配给进程后,也即进程调用类似malloc函数之后,系统才会为相应的虚拟地址在Page Table中添加记录,如果进程访问一个没有和Page Table关联的虚拟地址,系统将会抛出SIGSEGV信号,导致进程退出,这也是为什么我们访问野指针时会经常出现segmentfault的原因。换句话说,虽然每个进程都有4G(32位系统)的虚拟地址空间,但只有向系统申请了的那些地址空间才能用,访问未分配的地址空间将会出segmentfault错误。Linux会将虚拟地址0不映射到任何地方,这样我们访问空指针就一定会报segmentfault错误。
虚拟内存的优点
- 更大的地址空间:并且是连续的,使得程序编写、链接更加简单
- 进程隔离:不同进程的虚拟地址之间没有关系,所以一个进程的操作不会对其它进程造成影响
- 数据保护:每块虚拟内存都有相应的读写属性,这样就能保护程序的代码段不被修改,数据块不能被执行等,增加了系统的安全性
- 内存映射:有了虚拟内存之后,可以直接映射磁盘上的文件(可执行文件或动态库)到虚拟地址空间,这样可以做到物理内存延时分配,只有在需要读相应的文件的时候,才将它真正的从磁盘上加载到内存中来,而在内存吃紧的时候又可以将这部分内存清空掉,提高物理内存利用效率,并且所有这些对应用程序来说是都透明的
- 共享内存:比如动态库,只要在内存中存储一份就可以了,然后将它映射到不同进程的虚拟地址空间中,让进程觉得自己独占了这个文件。进程间的内存共享也可以通过映射同一块物理内存到进程的不同虚拟地址空间来实现共享
- 物理内存管理:物理地址空间全部由操作系统管理,进程无法直接分配和回收,从而系统可以更好的利用内存,平衡进程间对内存的需求
- 其它:有了虚拟地址空间后,交换空间和COW(copy on write)等功能都能很方便的实现
page table
page table可以简单的理解为一个memory mapping的链表(当然实际结构很复杂),里面的每个memory mapping都将一块虚拟地址映射到一个特定的资源(物理内存或者外部存储空间)。每个进程拥有自己的page table,和其它进程的page table没有关系。
memory mapping
每个memory mapping就是对一段虚拟内存的描述,包括虚拟地址的起始位置,长度,权限(比如这段内存里的数据是否可读、写、执行), 以及关联的资源(如物理内存page,swap空间上的page,磁盘上的文件内容等)。
当进程申请内存时,系统将返回虚拟内存地址,同时为相应的虚拟内存创建memory mapping并将它放入page table,但这时系统不一定会分配相应的物理内存,系统一般会在进程真正访问这段内存的时候才会分配物理内存并关联到相应的memory mapping,这就是所谓的延时分配/按需分配。
每个memory mapping都有一个标记,用来表示所关联的物理资源类型,一般分两大类,那就是anonymous和file backed,在这两大类中,又分了一些小类,比如anonymous下面有更具体的shared和copy on write类型, file backed下面有更具体的device backed类型。下面是每个类型所代表的意思:
file backed
这种类型表示memory mapping对应的物理资源存放在磁盘上的文件中,它所包含的信息包括文件的位置、offset、rwx权限等。
当进程第一次访问对应的虚拟page的时候,由于在memory mapping中找不到对应的物理内存,CPU会报page fault中断,然后操作系统就会处理这个中断并将文件的内容加载到物理内存中,然后更新memory mapping,这样下次CPU就能访问这块虚拟地址了。以这种方式加载到内存的数据一般都会放到page cache中,关于page cache会在后面介绍到.
一般程序的可执行文件,动态库都是以这种方式映射到进程的虚拟地址空间的。
device backed
和file backed类似,只是后端映射到了磁盘的物理地址,比如当物理内存被swap out后,将被标记为device backed。
anonymous
程序自己用到的数据段和堆栈空间,以及通过mmap分配的共享内存,它们在磁盘上找不到对应的文件,所以这部分内存页被叫做anonymous page。anonymous page和file backed最大的差别是当内存吃紧时,系统会直接删除掉file backed对应的物理内存,因为下次需要的时候还能从磁盘加载到内存,但anonymous page不能被删除,只能被swap out。
shared
不同进程的Page Table里面的多个memory mapping可以映射到相同的物理地址,通过虚拟地址(不同进程里的虚拟地址可能不一样)可以访问到相同的内容,当一个进程里面修改内存的内容后,在另一个进程中可以立即读取到。这种方式一般用来实现进程间高速的共享数据(如mmap)。当标记为shared的memory mapping被删除回收时,需要更新物理page上的引用计数,便于物理page的计数变0后被回收。
copy on write
copy on write基于shared技术,当读这种类型的内存时,系统不需要做任何特殊的操作,而当要写这块内存时,系统将会生成一块新的内存并拷贝原来内存中的数据到新内存中,然后将新内存关联到相应的memory mapping,然后执行写操作。Linux下很多功能都依赖于copy on write技术来提高性能,比如fork等。
进程使用内存过程
内存的使用过程总结如下:
- 进程向系统发出内存申请请求
- 系统会检查进程的虚拟地址空间是否被用完,如果有剩余,给进程分配虚拟地址
- 系统为这块虚拟地址创建相应的memory mapping(可能多个),并将它放进该进程的page table
- 系统返回虚拟地址给进程,进程开始访问该虚拟地址
- CPU根据虚拟地址在该进程的page table中找到了相应的memory mapping,但是该mapping没有和物理内存关联,于是产生缺页中断
- 操作系统收到缺页中断后,分配真正的物理内存并将它关联到相应的memory mapping
- 中断处理完成后,CPU就可以访问该内存了
- 当然缺页中断不是每次都会发生,只有系统觉得有必要延迟分配内存的时候才用的着,也即很多时候在上面的第3步系统会分配真正的物理内存并和memory mapping关联。
当然缺页中断不是每次都会发生,只有系统觉得有必要延迟分配内存的时候才用的着,也即很多时候在上面的第3步系统会分配真正的物理内存并和memory mapping关联
其它概念
操作系统只要实现了虚拟内存和物理内存之间的映射关系,就能正常工作了,但要使内存访问更高效,还有很多东西需要考虑,在这里我们可以看看跟内存有关的一些其它概念以及它们的作用。
内存管理单元(MMU,Memory Management Unit)
MMU是CPU的一个用来将进程的虚拟地址转换成物理地址的模块,简单点说,这个模块的输入是进程的page table和虚拟地址,输出是物理地址。将虚拟地址转换成物理地址的速度直接影响着系统的速度,所以CPU包含了这个模块用来加速。
TLB(Translation Lookaside Buffer)
上面介绍到,MMU的输入是page table,而page table又存在内存里面,跟CPU的cache相比,内存的速度很慢,所以为了进一步加快虚拟地址到物理地址的转换速度,Linux发明了TLB,它存在于CPU的L1 cache里面,用来缓存已经找到的虚拟地址到物理地址的映射,这样下次转换前先查一下TLB,如果已经在里面了就不需要调用MMU了.
按需分配物理页
由于实际情况下物理内存要比虚拟内存少很多,所以操作系统必须很小心的分配物理内存,以使内存的使用率达到最大化。一个节约物理内存的办法就是只加载当前正在使用的虚拟page对应的数据到内存。比如,一个很大的数据库程序,如果你只是用了查询操作,那么负责插入删除等部分的代码段就没必要加载到内存中,这样就能节约很多物理内存,这种方法就叫做物理内存页按需分配,也可以称作延时加载。
其实现原理很简单,就是当CPU访问一个虚拟内存页的时候,如果这个虚拟内存页对应的数据还没加载到物理内存中,则CPU就会通知操作系统发生了page fault,然后由操作系统负责将数据加载进物理内存。由于将数据加载进内存比较耗时,所以CPU不会等在那里,而是去调度其它进程,当它下次再调度到该进程时,数据已经在物理内存上了。
Linux主要使用这种方式来加载可执行文件和动态库,当程序被内核开始调度执行时,内核将进程的可执行文件和动态库映射到进程的虚拟地址空间,并只加载马上要用到的那小部分数据到物理内存中,其它的部分只有当CPU访问到它们时才去加载。
交换空间
当一个进程需要加载数据到物理内存中,但实际的物理内存已经被用完时,操作系统需要回收一些物理内存中的page以满足当前进程的需要。
对于file backed的内存数据,即物理内存里面的数据来自于磁盘上的文件,那么内核将直接将该部分数据从内存中移除掉来释放出更多的内存,当下次有进程需要访问这部分数据时,再将它从磁盘上加载到内存中来。但是,如果这部分数据被修改过且没被写入文件,那这部分数据就变成了脏数据,脏数据不能被直接删掉,只能被移动到交换空间上去。(可执行文件和动态库文件不会被修改,但通过mmap+private的方式映射到内存的磁盘文件有可能被修改,这种方式映射的内存比较特殊,没修改之前是file backed,修改后但没有写回磁盘之前就变成了anonymous的)
对于anonymous的内存数据,在磁盘上没有对应的文件,这部分数据不能直接被删除,而是被系统移到交换空间上去。交换空间就是磁盘上预留的一块特殊空间,被系统用来临时存放内存中不常被访问的数据,当下次有进程需要访问交换空间上的数据时,系统再将数据加载到内存中。由于交换空间在磁盘上,所以访问速度要比内存慢很多,频繁的读写交换空间会带来性能问题。
关于swap空间的详细介绍请参考Linux交换空间
共享内存
有了虚拟内存之后,进程间共享内存变得特别的方便。进程所有的内存访问都通过虚拟地址来实现,而每个进程都有自己的page tables。当两个进程共享一块物理内存时,只要将物理内存的页号映射到两个进程的page table中就可以了,这样两个进程就可以通过不同的虚拟地址来访问同一块物理内存。
从上面的那个图中可以看出,进程X和进程Y共享了物理内存页PFN3,在进程X中,PFN3被映射到了VPFN3,而在进程Y中,PFN3被映射到了VPFN1,但两个进程通过不同的虚拟地址访问到的物理内存是同一块。
访问控制
page table里面的每条虚拟内存到物理内存的映射记录(memory mapping)都包含一份控制信息,当进程要访问一块虚拟内存时,系统可以根据这份控制信息来检查当前的操作是否是合法的。
为什么需要做这个检查呢?比如有些内存里面放的是程序的可执行代码,那么就不应该去修改它;有些内存里面存放的是程序运行时用到的数据,那么这部分内存只能被读写,不应该被执行;有些内存里面存放的是内核的代码,那么在用户态就不应该去执行它;有了这些检查之后会大大增强系统的安全性。
huge pages
由于CPU的cache有限,所以TLB里面缓存的数据也有限,而采用了huge page后,由于每页的内存变大(比如由原来的4K变成了4M),虽然TLB里面的纪录数没变,但这些纪录所能覆盖的地址空间变大,相当于同样大小的TLB里面能缓存的映射范围变大,从而减少了调用MMU的次数,加快了虚拟地址到物理地址的转换速度。
Caches
为了提高系统性能,Linux使用了一些跟内存管理相关的cache,并且尽量将空闲的内存用于这些cache。这些cache都是系统全局共享的:
Buffer Cache
用来缓冲块设备上的数据,比如磁盘,当读写块设备时,系统会将相应的数据存放到这个cache中,等下次再访问时,可以直接从cache中拿数据,从而提高系统效率。它里面的数据结构是一个块设备ID和block编号到具体数据的映射,只要根据块设备ID和块的编号,就能找到相应的数据。Page Cache
这个cache主要用来加快读写磁盘上文件的速度。它里面的数据结构是文件ID和offset到文件内容的映射,根据文件ID和offset就能找到相应的数据(这里文件ID可能是inode或者path,本人没有仔细去研究)。
从上面的定义可以看出,page cache和buffer cache有重叠的地方,不过实际情况是buffer cache只缓存page cache不缓存的那部分内容,比如磁盘上文件的元数据。所以一般情况下和page cache相比,Buffer Cache的大小基本可以忽略不计。
当然,使用cache也有一些不好的地方,比如需要时间和空间去维护cache,cache一旦出错,整个系统就挂了。