Linux内存管理

在linux下,使用top,vmstat,free等命令查看系统或者进程的内存使用状况时,常常看到buff/cache memeory,swap,avail Mem等,他们都表明什么意思呢?这篇文章未来聊一聊Linux下的内存管理并解答这个问题。html

讨论Linux下的内存管理其实就是讨论Linux下虚拟内存的实现方式,本人不是内核专家,因此这篇文章只会介绍一些概念性的东西,不会深刻实现细节,有些地方描述的有可能不精确。node

在早些时候,物理内存比较有限,人们但愿程序可使用的内存空间能超过实际物理内存,因而出现了虚拟内存的概念,不过随着时间的推移,虚拟内存的意义已经远远的超过了最初的想法。linux

虚拟内存

虚拟内存是Linux管理内存的一种技术。它使得每一个应用程序都认为本身拥有独立且连续的可用的内存空间(一段连续完整的地址空间),而实际上,它一般是被映射到多个物理内存段,还有部分暂时存储在外部磁盘存储器上,在须要时再加载到内存中来。数据库

每一个进程所能使用的虚拟地址大小和CPU位数有关,在32位的系统上,虚拟地址空间大小是4G,在64位系统上,是2^64=?(算不过来了)。而实际的物理内存可能远远小于虚拟地址空间的大小。segmentfault

虚拟地址和进程息息相关,不一样进程里的同一个虚拟地址指向的物理地址不必定同样,因此离开进程谈虚拟地址没有任何意义。后端

注意:网上不少文章将虚拟内存等同于交换空间,其实描述不够严谨,交换空间只是虚拟内存这张大蓝图中的一部分。缓存

虚拟内存和物理内存的关系

下面这张表很直观的表述了它们之间的关系安全

进程X                                                                      进程Y
+-------+                                                                  +-------+
| VPFN7 |--+                                                               | VPFN7 |
+-------+  |       进程X的                                 进程Y的           +-------+
| VPFN6 |  |      Page Table                              Page Table     +-| VPFN6 |
+-------+  |      +------+                                +------+       | +-------+
| VPFN5 |  +----->| .... |---+                    +-------| .... |<---+  | | VPFN5 |
+-------+         +------+   |        +------+    |       +------+    |  | +-------+
| VPFN4 |    +--->| .... |---+-+      | PFN4 |    |       | .... |    |  | | VPFN4 |
+-------+    |    +------+   | |      +------+    |       +------+    |  | +-------+
| VPFN3 |--+ |    | .... |   | | +--->| PFN3 |<---+  +----| .... |<---+--+ | VPFN3 |
+-------+  | |    +------+   | | |    +------+       |    +------+    |    +-------+
| VPFN2 |  +-+--->| .... |---+-+-+    | PFN2 |<------+    | .... |    |    | VPFN2 |
+-------+    |    +------+   | |      +------+            +------+    |    +-------+
| VPFN1 |    |               | +----->| FPN1 |                        +----| VPFN1 |
+-------+    |               |        +------+                             +-------+
| VPFN0 |----+               +------->| PFN0 |                             | VPFN0 |
+-------+                             +------+                             +-------+
 虚拟内存                               物理内存                               虚拟内存


PFN(the page frame number): 页编号

当进程执行一个程序时,须要先从先内存中读取该进程的指令,而后执行,获取指令时用到的就是虚拟地址,这个地址是程序连接时肯定的(内核加载并初始化进程时会调整动态库的地址范围),为了获取到实际的数据,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等。

经过上面的介绍,咱们能够简单的将内存的使用过程总结以下:

  1. 进程向系统发出内存申请请求

  2. 系统会检查进程的虚拟地址空间是否被用完,若是有剩余,给进程分配虚拟地址

  3. 系统为这块虚拟地址建立相应的memory mapping(可能多个),并将它放进该进程的page table

  4. 系统返回虚拟地址给进程,进程开始访问该虚拟地址

  5. CPU根据虚拟地址在该进程的page table中找到了相应的memory mapping,可是该mapping没有和物理内存关联,因而产生缺页中断

  6. 操做系统收到缺页中断后,分配真正的物理内存并将它关联到相应的memory mapping

  7. 中断处理完成后,CPU就能够访问该内存了

固然缺页中断不是每次都会发生,只有系统以为有必要延迟分配内存的时候才用的着,也即不少时候在上面的第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一旦出错,整个系统就挂了。

总结

有了上面介绍的知识,再来看看咱们刚开始提出来的问题,以top命令的输出为例:

KiB Mem :   500192 total,   349264 free,    36328 used,   114600 buff/cache
KiB Swap:   524284 total,   524284 free,        0 used.   433732 avail Mem

KiB Mem表明物理内存,KiB Swap表明交换空间,它们的单位都是KiB。

total、used和free没什么好介绍的,就是总共多少,而后用了多少,还剩多少。

buff/cached表明了buff和cache总共用了多少,buff表明buffer cache占了多少空间,因为它主要用来缓存磁盘上文件的元数据,因此通常都比较小,跟cache比能够忽略不计;cache表明page cache和其它一些占用空间比较小且大小比较固定的cache的总和,基本上cache就约等于page cache,page cache的准确值能够经过查看/proc/meminf中的Cached获得。因为page cache是用来缓存磁盘上文件内容的,因此占有空间很大,Linux通常会尽量多的将空闲物理内存用于page cache。

avail Mem表示可用于进程下一次分配的物理内存数量,这个大小通常比free大一点,由于除了free的空间外,系统还能当即释放出一些空间来。

那么怎么判断当前内存使用状况出现了异常呢?有下面几点供参考:

  • Mem free的值比较小,而且buff/cache的值也小
    free的值比较少并不必定表明有问题,由于Linux会尽量多的将内存用于page cache,可是若是buff/cache的值也小,就说明内存吃紧了,系统没有足够多的内存用于cache,若是当前服务器部署是一个须要频繁的读写磁盘的应用,如FTP服务器,那么对性能的影响将会很是大。

  • Swap used的值比较大,
    这种状况比上面的更严重,正常状况下swap应该不多被使用,used值比较大说明交换空间被使用的比较多,若是经过vmstat命令看到swap in/out的比较频繁的话,说明系统内存严重不足,总体性能已经受到严重影响

参考