在Linux下,当你使用top、vmstat、free等命令查看系统或进程的内存使用情况时,经常会看到buff/cachememeory、swap、availMem等,它们是什么意思?本文将谈谈Linux下的内存管理,并回答这个问题。讨论Linux下的内存管理,其实就是讨论Linux下虚拟内存的实现。我不是内核专家,所以这篇文章只会介绍一些概念性的东西,不会深入到实现的细节。有些描述可能不准确。在早期,物理内存是比较有限的,人们希望程序可以使用的内存空间能够超过实际的物理内存,于是就出现了虚拟内存的概念,但是随着时间的推移,虚拟内存的含义有了远远超出了最初的想法。虚拟内存虚拟内存是Linux管理内存的一种技术。它让每个应用程序都以为自己有一个独立的、连续的可用内存空间(一个连续的、完整的地址空间),但实际上,它通常映射到多个物理内存段,有的是暂时存储在外部的。在磁盘存储上并在需要时加载到内存中。每个进程可以使用的虚拟地址大小与CPU位数有关。在32位系统上,虚拟地址空间大小为4G。在64位系统上,它是2^64=?(想不通)。实际的物理内存可能远小于虚拟地址空间的大小。虚拟地址与进程密切相关。同一个虚拟地址在不同进程中指向的物理地址不一定相同,所以脱离进程再谈虚拟地址没有任何意义。注意:网上很多文章都把虚拟内存等同于交换空间。其实描述的不够严谨。交换空间只是虚拟内存大蓝图的一部分。虚拟内存和物理内存的关系下表直观的表达了它们之间的关系ProcessXProcessY+--------++-------+|VPFN7|--+|VPFN7|+------+|+------+|VPFN6||页表页表+-|VPFN6|+------+|+进程Y的进程X------++------+|+------+|VPFN5|+----->|....|---++-------|....|<---+||VPFN5|+------++------+|+------+|+------+||+------+|VPFN4|+--->|....|---+-+|PFN4|||....||||VPFN4|+------+|+------+||+------+|+------+||+------+|--+||....|||+--->|PFN3|<---++----|....|<---+--+|VPFN3|+------+||+------+|||+------+|+------+|+------+|VPFN2|+-+--->|....|---+-+-+|PFN2|<------+|....|||VPFN2|+------+|+------+||+-----++-----+|+------+|VPFN1|||+----->|FPN1|+----|VPFN1|+------+||+------++------+|VPFN0|----++-------->|PFN0||VPFN0|+------++------++------+virtualmemoryphysicalmemoryvirtualmemoryPFN(thepageframenumber):进程执行程序时的页码,它需要从第一个内存中读取进程的指令,然后执行它。获取指令时使用虚拟地址。这个地址是在程序链接时确定的(内核在加载和初始化进程时,会调整动态库的地址范围)。CPU为了获取实际数据,需要将虚拟地址转换为物理地址。CPU在转换地址的时候需要用到进程的页表,页表中的数据由操作系统维护注:Linux内核代码访问内存时使用的是实际物理地址,所以没有虚拟地址到物理地址的转换,只有应用层程序需要。为了转换方便,Linux将虚拟内存和物理内存都拆分成固定大小的页面。x86系统一般内存页大小为4K,每页都会分配一个唯一的编号,即页码(PFN)。从上图中可以看出,虚拟内存和物理内存页是通过页表进行映射的。进程X和Y的虚拟内存是相互独立的,页表也是独立的,共享物理内存。进程可以自由访问自己的虚拟地址空间,而页表和物理内存则由内核维护。当进程需要访问内存时,CPU根据进程的页表将虚拟地址翻译成物理地址,然后访问。注意:并非虚拟地址空间中的每个页面都有相应的页表。只有将虚拟地址分配给进程后,即进程调用类似malloc的函数后,系统才会在PageTable中添加相应的虚拟地址。添加一条记录,如果进程访问了一个与页表没有关联的虚拟地址,系统会抛出一个SIGSEGV信号,导致进程退出,这就是为什么我们在访问野指针时经常会出现segmentfault的原因。也就是说,虽然每个进程都有4G(32位系统)的虚拟地址空间,但是只能使用那些已经申请到系统的地址空间,访问未分配的地址空间时会出现segmentfault错误。Linux不会把虚拟地址0映射到任何地方,所以我们访问空指针肯定会报segmentfault错误。虚拟内存的优点地址空间更大:且连续,使程序编写和链接更容易进程隔离:不同进程的虚拟地址之间没有关系,一个进程的运行不会影响其他进程具有相应的读写属性,可以保护程序的代码段不被修改,数据块不能执行等,增加了系统的安全性。内存映射:有了虚拟内存后,可以直接将磁盘上的文件(可执行文件或动态库)映射到虚拟地址空间,从而实现物理内存的延迟分配。只有当需要读取相应的文件时,才真正从磁盘加载到内存中。而当内存吃紧的时候,可以将这部分内存清空,以提高物理内存的利用效率,而这一切对于应用程序来说都是透明的共享内存:比如动态库只需要在内存中存放一份即可,然后将其映射到不同进程的虚拟地址空间,让该进程感觉它独占该文件。进程间内存共享也可以通过将同一块物理内存映射到进程的不同虚拟地址空间来实现共享物理内存管理:物理地址空间全部由操作系统管理,进程不能直接分配并回收利用,以便系统更好地利用它。内存,平衡进程间的内存需求其他:有了虚拟地址空间,交换空间和COW(copyonwrite)等功能就可以轻松实现pagetablepagetable可以简单理解为内存映射的链表(当然实际结构非常复杂),里面的每一个内存映射都将一个虚拟地址映射到一个特定的资源(物理内存或外部存储空间)。每个进程都有自己的页表,与其他进程的页表无关。内存映射每一个内存映射都是对一段虚拟内存的描述,包括虚拟地址的起始位置、长度、权限(比如是否可以读、写、执行这块内存中的数据),以及关联的资源(例如物理内存页面、交换空间页面、磁盘上的文件内容等)。当一个进程申请内存时,系统会返回虚拟内存地址,同时为对应的虚拟内存创建内存映射并放入页表中。只有分配了这块内存,物理内存才会关联到相应的内存映射。这就是所谓的延迟分配/按需分配。每个内存映射都有一个标记来表示关联的物理资源类型,一般分为两类,即匿名和文件备份,在这两类中,又分为一些子类,比如匿名下更具体的是shared和copy关于写入类型,以及文件支持下更具体的设备支持类型。下面是每种类型的含义:filebacked该类型表示内存映射对应的物理资源存储在磁盘上的文件中,其包含的信息包括文件的位置、偏移量、rwx权限等。当进程第一次访问对应的虚拟页时,由于在内存映射中找不到对应的物理内存,CPU会报缺页中断,然后操作系统处理中断,加载内容文件写入物理内存,然后更新内存映射,以便CPU下次访问这个虚拟地址。这种方式加载到内存中的数据一般都是放在pagecache中,后面会介绍pagecache。一般程序的可执行文件和动态库都是通过这种方式映射到进程的虚拟地址空间的。Devicebacked类似于filebacked,只是后端映射到磁盘的物理地址。例如,当物理内存被换出时,它将被标记为设备支持。匿名程序本身使用的数据段和栈空间,以及通过mmap分配的共享内存,都无法在磁盘上找到对应的文件,所以这部分内存页称为匿名页。匿名页面和文件备份最大的区别是当内存吃紧的时候,系统会直接删除文件备份对应的物理内存,因为下次需要的时候可以从磁盘加载到内存中,但是匿名页面无法删除,只能删除换出。不同进程共享的PageTable中的多个内存映射可以映射到同一个物理地址,通过虚拟地址访问相同的内容(不同进程中的虚拟地址可能不同)。当内存中的内容在一个进程中被修改时,可以立即被另一个进程读取。这种方式一般用于实现进程间的高速共享数据(如mmap)。当标记为共享的内存映射被删除和回收时,需要更新物理页上的引用计数,以便物理页在计数变为0后可以被回收。copyonwritecopyonwrite基于共享技术。读取这类内存时,系统不需要做任何特殊操作。当写入这段内存时,系统会生成一个新的内存并复制原来的内存。将数据存储到新的内存中,然后将新的内存关联到对应的内存映射,然后进行写操作。Linux下的很多功能都依赖copyonwrite技术来提高性能,比如fork。通过上面的介绍,我们可以简单的总结一下进程使用内存的过程:进程向系统发送内存申请请求,系统会检查进程的虚拟地址空间是否用完。虚拟地址创建对应的内存映射(可能是多个),并放入进程的页表中。系统将虚拟地址返回给进程,进程开始访问虚拟地址。CPU根据虚拟地址在进程的页表中找到对应的内存。内存映射,但是映射与物理内存没有关联,所以会产生缺页中断。操作系统收到缺页中断后,分配真正的物理内存,并关联到相应的内存映射中断处理。CPU可以访问之后当然,pagefault中断不会每次都发生,只有当系统觉得有必要延迟分配内存时,才会使用,也就是上面的第3步,系统会分配真正的物理内存内存并将其与内存映射相关联。其他概念操作系统只要实现了虚拟内存和物理内存的映射关系就可以正常工作,但是要让内存访问更高效,还有很多事情需要考虑。这里我们可以看看一些与内存相关的其他概念以及它们的作用。MMU(MemoryManagementUnit)MMU是CPU的一个模块,用来将进程的虚拟地址转换成物理地址。简单的说,这个模块的输入是进程的页表和虚拟地址,输出是物理地址。虚拟地址转换为物理地址的快慢直接影响系统的运行速度,所以CPU包含了这个模块来加速。上面提到的TLB(TranslationLookasideBuffer),MMU的输入是页表,页表存放在内存中。与CPU的缓存相比,内存的速度是非常慢的,所以为了进一步加快虚拟地址到物理地址的转换速度,Linux发明了TLB,它存在于CPU的一级缓存中,用于缓存已经找到的虚拟地址到物理地址的映射,以便下次转换前检查TLB。如果已经在里面了,就不用调用MMU了。.按需分配物理页面由于物理内存在实际情况下远小于虚拟内存,因此操作系统必须仔细分配物理内存以最大限度地利用内存。一种节省物理内存的方法是只将当前使用的虚拟页对应的数据加载到内存中。例如,对于一个大型数据库程序,如果只使用查询操作,那么负责插入和删除部分的代码段就不需要加载到内存中,这样可以节省大量的物理内存。这种方式称为物理内存页按需分配,也可以称为延迟加载。实现原理很简单,就是当CPU访问一个虚拟内存页时,如果该虚拟内存页对应的数据还没有加载到物理内存中,CPU就会通知操作系统发生了pagefault,然后操作系统会负责将数据加载到物理内存中。由于将数据加载到内存中需要时间,因此CPU不会在那里等待,而是调度其他进程。下次调度到这个进程时,数据已经在物理内存中了。Linux主要使用这种方式加载可执行文件和动态库。当程序被内核调度执行时,内核将进程的可执行文件和动态库映射到进程的虚拟地址空间,只加载将立即使用的。那一小部分数据进入物理内存,其余部分仅在CPU访问它们时加载。交换空间当一个进程需要加载数据到物理内存中,但是实际的物理内存已经用完了,操作系统需要回收物理内存中的一些页面来满足当前进程的需要。对于文件备份内存数据,即物理内存中的数据来自磁盘上的文件,内核会直接将这部分数据从内存中移除,以释放更多的内存。当一个进程下次需要访问这部分数据时,就从磁盘加载到内存中。但是,如果这部分数据被修改过,没有写入文件,那么这部分数据就变成了脏数据。脏数据不能直接删除,只能移动到swap空间。(可执行文件和动态库文件不会被修改,但是通过mmap+private映射到内存的磁盘文件可能会被修改,这种方式映射的内存比较特殊,修改前是filebacked,修改后,却变成了anonymousbeforeitwritebacktothedisk)对于匿名内存数据,磁盘上没有对应的文件。这部分数据不能直接删除,而是由系统移动到交换空间。交换空间是在磁盘上预留的特殊空间,供系统用来临时存放内存中不经常访问的数据。当下一次进程需要访问交换空间上的数据时,系统将数据加载到内存中。由于swap空间在磁盘上,访问速度比内存慢很多,频繁读写swap空间会造成性能问题。关于swap空间的详细介绍可以参考LinuxSwapSpaceSharedMemory有了虚拟内存,进程间共享内存变得特别方便。一个进程所有的内存访问都是通过虚拟地址实现的,每个进程都有自己的页表。当两个进程共享一块物理内存时,只需要将物理内存的页码映射到两个进程的页表中,这样两个进程就可以通过不同的虚拟地址访问同一块物理内存。从上图可以看出,进程X和进程Y共享物理内存页PFN3。在进程X中,PFN3映射到VPFN3,在进程Y中,PFN3映射到VPFN1,但是两个进程通过不同的虚拟地址访问的物理内存是同一个块。访问控制页表中的每条虚拟内存到物理内存的映射记录(memorymapping)都包含一条控制信息。当进程要访问一块虚拟内存时,系统可以检查当前操作是否合法。为什么需要这项检查?比如,有些内存存放的是程序的可执行代码,不应该修改;一些内存存储程序运行时使用的数据,所以这部分内存只能读写,不应该被执行;有的内存存放内核代码,不应该在用户态执行;通过这些检查,系统的安全性将大大增强。由于CPU的缓存有限,大页面在TLB中的缓存数据也有限。使用hugepage之后,由于每个page的内存变大了(比如原来的4K变成了4M),虽然TLB中的记录数没有了,但是这些记录所能覆盖的地址空间变大了,也就是相当于在相同大小的TLB中可以缓存更大的映射范围,从而减少对MMU的调用次数,加快虚拟地址到物理地址的转换速度。缓存为了提高系统性能,Linux使用了一些与内存管理相关的缓存,并尽量为这些缓存使用空闲内存。这些缓存由系统全局共享:BufferCache用于缓冲块设备(例如磁盘)上的数据。在读写块设备时,系统会将相应的数据存储在这个缓存中。直接从缓存中获取数据,从而提高系统效率。其中的数据结构是块设备ID和块号到具体数据的映射。只要使用块设备ID和块号,就可以查到对应的数据。PageCache这种缓存主要用来加快磁盘上文件的读写速度。里面的数据结构是文件ID和偏移量到文件内容的映射,根据文件ID和偏移量可以找到对应的数据(这里的文件ID可能是一个inode,也可能是一个路径,我做的不仔细研究)。从上面的定义我们可以看出pagecache和buffercache有重叠,但实际情况是buffercache只缓存了pagecache不缓存的部分,比如磁盘上文件的元数据。所以一般来说,和pagecache相比,BufferCache的大小基本可以忽略不计。当然,使用缓存也有一些缺点,比如需要时间和空间来维护缓存,一旦缓存失效,整个系统就会挂掉。总结了上面介绍的知识,我们再来看看一开始提出的问题。以top命令的输出为例:KiBMem:500192total,349264free,36328used,114600buff/cacheKiBSwap:524284total,524284free,0used。433732availMemKiBMem代表物理内存,KiBSwap代表交换空间,它们的单位是KiB。total、used、free没什么好介绍的,就是total多少,用了多少,还剩多少。buff/cached代表buff和cache的总使用量,buff代表buffercache占用多少空间。由于它主要用来缓存磁盘上文件的元数据,所以一般比较小,与缓存相比可以忽略;cache代表页面缓存和其他占用空间比较小,大小比较固定的缓存的总和,基本等于页面缓存。可以通过查看/proc/meminf中的Cached来获取页面缓存的确切值。由于pagecache用于在磁盘上缓存文件内容,占用空间较大。Linux通常使用尽可能多的空闲物理内存作为页面缓存。availMem表示可用于进程下一次分配的物理内存量。这个大小一般比free大一点,因为除了free空间,系统还可以马上释放一些空间。那么如何判断当前内存使用异常呢?以下几点供参考:Memfree的值比较小,buff/cache的值也小。free的值比较小并不一定说明有问题,因为linux会尽可能多的使用内存做pagecache,但是如果buff/cache的值也小,说明内存吃紧,而系统没有足够的内存用于缓存。如果现在的服务器部署是需要频繁读写磁盘的应用,比如FTP服务器,对性能的影响会很大。很大。使用的Swap价值比较大。这种情况比上面的更严重。一般情况下,swap应该很少用到。如果used的值比较大,说明swap空间使用的比较多。如果通过vmstat命令看到swapin/out比较频繁,说明系统内存严重不足,整体性能受到严重影响。参考内存管理MmemoryFAQ
