Concepts overview — The Linux Kernel documentation
Linux中的内存管理是一个复杂的系统,经过多年的发展,它包含越来越多的功能,以支持从 MMU-less microcontrollers 到 supercomputers 的各种系统。
没有MMU内存管理的系统被称为 nommu ,它值得写一份专门的文档进行描述。
尽管有些概念是相同的,这里我们假设MMU可用,CPU可以将虚拟地址转换为物理地址。
计算机系统中的物理内存是有限资源,即便支持内存热插拔,其可以安装的内存也有限的。物理内存不一定必须是连续的;它可以作为一组不同的地址范围被访问。此外,不同的CPU架构,甚至同架构的不同实现对如何定义这些地址范围都是不同的。
这使得直接处理物理内存异常复杂,为了避免这种复杂性,开发了 虚拟内存 (virtual memory) 的概念。
虚拟内存从应用软件中抽象出物理内存的细节,只允许在物理内存中保留需要的信息 (demand paging) ,并提供一种机制来保护和控制进程之间的数据共享。
通过虚拟内存,每次内存访问都访问一个 虚拟地址 。当CPU对从系统内存读取(或写入)的指令进行解码时,它将该指令中编码的虚拟地址转换为内存控制器可以理解的物理地址。
物理内存被切分为 页帧 page frames 或 页 pages 。页的大小是基于架构的。一些架构允许从几个支持的值中选择页大小;此选择在内核编译时设置到内核配置。
每个物理内存页都可以映射为一个或多个 虚拟页(virtual pages) 。映射关系描述在 页表(page tables) 中,页表将程序使用的虚拟地址转换为物理内存地址。页表以层次结构组织。
最底层的表包含软件使用的实际内存页的物理地址。较高层的表包含较低层表页的物理地址。顶层表的指针驻留在寄存器中。
当CPU进行地址转换的时候,它使用寄存器访问顶级页表。
虚拟地址的高位,用于顶级页表的条目索引。然后,通过该条目访问下级,下级的虚拟地址位又作为其下下级页表的索引。虚拟地址的最低位定义实际页内的偏移量。
地址转换需要多次内存访问,而内存访问相对于CPU速度来说比较慢。为了避免在地址转换上花费宝贵的处理器周期,CPU维护着一个称为 TLB (Translation Lookaside Buffer)的用于地址转换缓存(cache)。通常TLB是非常稀缺的资源,需要大内存工作应用程序会因为TLB未命中而影响性能。
很多现代CPU架构允许页表的高层直接映射到内存页。例如,x86架构,可以通过二级、三级页表的条目映射2M甚至1G内存页。在Linux中,这些内存页称为 大页 (Huge) 。大页的使用显著降低了TLB的压力,提高了TLB命中率,从而提高了系统的整体性能。
Linux提供两种机制开启使用大页映射物理内存。
第一个是 HugeTLB 文件系统,即 hugetlbfs 。它是一个伪文件系统,使用RAM作为其存储。在此文件系统中创建的文件,数据驻留在内存中,并使用大页进行映射。
关于 HugeTLB Pages
另一个被称为 THP (Transparent HugePages) ,后出的开启大页映射物理内存的机制。
与 hugetlbfs 不同,hugetlbfs要求用户和/或系统管理员配置系统内存的哪些部分应该并可以被大页映射;THP透明地管理这些映射并获取名称。
关于 Transparent Hugepage Support
通常,硬件对不同物理内存范围的访问方式有所限制。某些情况下,设备不能对所有可寻址内存执行DMA。在其他情况下,物理内存的大小超过虚拟内存的最大可寻址大小,需要采取特殊措施来访问部分内存。还有些情况,物理内存的尺寸超过了虚拟内存的最大可寻址尺寸,需要采取特殊措施来访问部分内存。
Linux根据内存页的使用情况,将其组合为多个 zones 。比如, ZONE_DMA 包含设备用于DMA的内存, ZONE_HIGHMEM 包含未永久映射到内核地址空间的内存, ZONE_NORMAL 包含正常寻址内存页。
内存zones的实际层次架构取决于硬件,因为并非所有架构都定义了所有的zones,不同平台对DMA的要求也不同。
多处理器机器很多基于 NUMA (Non-Uniform Memory Access system - 非统一内存访问系统 )架构。 在这样的系统中,根据与处理器的“距离”,内存被安排成具有不同访问延迟的 banks 。每个 bank 被称为一个 node ,Linux为每个 node 构造一个独立的内存管理子系统。 Node 有自己的zones集合、free&used页面列表,以及各种统计计数器。
What is NUMA
NUMA Memory Policy
物理内存易失,将数据放入内存的常见情况是读取文件。读取文件时,数据会放入 页面缓存(page cache) ,可以在再次读取时避免耗时的磁盘访问。同样,写文件时,数据也会被放入 页面缓存 ,并最终进入存储设备。被写入的页被标记为 脏页(dirty page) ,当Linux决定将其重用时,它会将更新的数据同步到设备上的文件。
匿名内存 anonymous memory 或 匿名映射 anonymous mappings 表示没有后置文件系统的内存。这些映射是为程序的stack和heap隐式创建的,或调用mmap(2)显式创建的。通常,匿名映射只定义允许程序访问的虚拟内存区域。读,会创建一个页表条目,该条目引用一个填充有零的特殊物理页。写,则分配一个常规物理页来保存写入数据。该页将被标记为脏页,如果内核决定重用该页,则脏页将被交换出去 swapped out 。
纵贯整个系统生命周期,物理页可用于存储不同类型的数据。它可以是内核内部数据结构、设备驱动DMA缓冲区、读取自文件系统的数据、用户空间进程分配的内存等。
根据内存页使用情况,Linux内存管理会区别处理。可以随时释放的页面称为 可回收(reclaimable) 页面,因为它们把数据缓存到了其他地方(比如,硬盘),或者被swap out到硬盘上。
可回收页最值得注意的是 页面缓存 和 匿名页面 。
在大多数情况下,存放内部内核数据的页,和用作DMA缓冲区的页无法重用,它们将保持现状直到用户释放。这样的被称为 不可回收页(unreclaimable) 。
然而,在特定情况下,即便是内核数据结构占用的页面也会被回收。
例如,文件系统元数据的缓存(in-memory)可以从存储设备中重新读取,因此,当系统存在内存压力时,可以从主内存中丢弃它们。
释放可回收物理内存页并重新调整其用途的过程称为 (surprise!) reclaim 。
Linux支持异步或同步回收页,取决于系统的状态。
当系统负载不高时,大部分内存是空闲的,可以立即从空闲页得到分配。
当系统负载提升后,空闲页减少,当达到某个阈值( low watermark )时,内存分配请求将唤醒 kswapd 守护进程。它将以异步的方式扫描内存页。如果内存页中的数据在其他地方也有,则释放这些内存页;或者退出内存到后置存储设备(关联 脏页 )。
随着内存使用量进一步增加,并达到另一个阈值- min watermark -将触发回收。这种情况下,分配将暂停,直到回收到足够的内存页。
当系统运行时,任务分配并释放内存,内存变得碎片化。
虽然使用虚拟内存可以将分散的物理页表示为虚拟连续范围,但有时需要分配大的连续的物理内存。这种需求可能会提升。例如,当设备驱动需要一个大的DMA缓冲区时,或当THP分配一个大页时。
内存地址压缩(compaction ) 解决了碎片问题。
该机制将占用的页从内存zone的下部移动到上部的空闲页。压缩扫描完成后,zone开始处的空闲页就并在一起了,分配较大的连续物理内存就可行了。
与 reclaim 类似, compaction 可以在 kcompactd守护进程中异步进行,也可以作为内存分配请求的结果同步进行。
在存在负载的机器上,内存可能会耗尽,内核无法回收到足够的内存以继续运行。
为了保障系统的其余部分,引入了 OOM killer 。
OOM killer 选择牺牲一个任务来保障系统的总体健康。选定的任务被killed,以期望在它退出后释放足够的内存以继续正常的 *** 作。
vmstat是Virtual Meomory Statistics(虚拟内存统计)的缩写,可对 *** 作系统的虚拟内存、进程、CPU活动进行监控。他是对系统的整体情况进行统计,不足之处是无法对某个进程进行深入分析。vmstat 工具提供了一种低开销的系统性能观察方式。因为 vmstat 本身就是低开销工具,在非常高负荷的服务器上,你需要查看并监控系统的健康情况,在控制窗口还是能够使用vmstat 输出结果。在学习vmstat命令前,我们先了解一下Linux系统中关于物理内存和虚拟内存相关信息。
物理内存和虚拟内存区别:
我们知道,直接从物理内存读写数据要比从硬盘读写数据要快的多,因此,我们希望所有数据的读取和写入都在内存完成,而内存是有限的,这样就引出了物理内存与虚拟内存的概念。
物理内存就是系统硬件提供的内存大小,是真正的内存,相对于物理内存,在linux下还有一个虚拟内存的概念,虚拟内存就是为了满足物理内存的不足而提出的策略,它是利用磁盘空间虚拟出的一块逻辑内存,用作虚拟内存的磁盘空间被称为交换空间(Swap Space)。
作为物理内存的扩展,linux会在物理内存不足时,使用交换分区的虚拟内存,更详细的说,就是内核会将暂时不用的内存块信息写到交换空间,这样以来,物理内存得到了释放,这块内存就可以用于其它目的,当需要用到原始的内容时,这些信息会被重新从交换空间读入物理内存。
linux的内存管理采取的是分页存取机制,为了保证物理内存能得到充分的利用,内核会在适当的时候将物理内存中不经常使用的数据块自动交换到虚拟内存中,而将经常使用的信息保留到物理内存。
要深入了解linux内存运行机制,需要知道下面提到的几个方面:
首先,Linux系统会不时的进行页面交换 *** 作,以保持尽可能多的空闲物理内存,即使并没有什么事情需要内存,Linux也会交换出暂时不用的内存页面。这可以避免等待交换所需的时间。
其次,linux进行页面交换是有条件的,不是所有页面在不用时都交换到虚拟内存,linux内核根据”最近最经常使用“算法,仅仅将一些不经常使用的页面文件交换到虚拟内存,有时我们会看到这么一个现象:linux物理内存还有很多,但是交换空间也使用了很多。其实,这并不奇怪,例如,一个占用很大内存的进程运行时,需要耗费很多内存资源,此时就会有一些不常用页面文件被交换到虚拟内存中,但后来这个占用很多内存资源的进程结束并释放了很多内存时,刚才被交换出去的页面文件并不会自动的交换进物理内存,除非有这个必要,那么此刻系统物理内存就会空闲很多,同时交换空间也在被使用,就出现了刚才所说的现象了。关于这点,不用担心什么,只要知道是怎么一回事就可以了。
最后,交换空间的页面在使用时会首先被交换到物理内存,如果此时没有足够的物理内存来容纳这些页面,它们又会被马上交换出去,如此以来,虚拟内存中可能没有足够空间来存储这些交换页面,最终会导致linux出现假死机、服务异常等问题,linux虽然可以在一段时间内自行恢复,但是恢复后的系统已经基本不可用了。
因此,合理规划和设计linux内存的使用,是非常重要的。
虚拟内存原理:
在系统中运行的每个进程都需要使用到内存,但不是每个进程都需要每时每刻使用系统分配的内存空间。当系统运行所需内存超过实际的物理内存,内核会释放某些进程所占用但未使用的部分或所有物理内存,将这部分资料存储在磁盘上直到进程下一次调用,并将释放出的内存提供给有需要的进程使用。
在Linux内存管理中,主要是通过“调页Paging”和“交换Swapping”来完成上述的内存调度。调页算法是将内存中最近不常使用的页面换到磁盘上,把活动页面保留在内存中供进程使用。交换技术是将整个进程,而不是部分页面,全部交换到磁盘上。
分页(Page)写入磁盘的过程被称作Page-Out,分页(Page)从磁盘重新回到内存的过程被称作Page-In。当内核需要一个分页时,但发现此分页不在物理内存中(因为已经被Page-Out了),此时就发生了分页错误(Page Fault)。
当系统内核发现可运行内存变少时,就会通过Page-Out来释放一部分物理内存。经管Page-Out不是经常发生,但是如果Page-out频繁不断的发生,直到当内核管理分页的时间超过运行程式的时间时,系统效能会急剧下降。这时的系统已经运行非常慢或进入暂停状态,这种状态亦被称作thrashing(颠簸)。
总结:物理内存就是硬件提供的真实的内存,比如我们电脑内存不够了,就会加一个内存条
虚拟内存就是从磁盘上虚拟出来的一块逻辑内存,用做虚拟内存的磁盘空间被称为交换空间(Swap Space
经常使用的文件会优先放在物理内存,不经常使用的文件会放到虚拟内存里面。
采用伙伴算法分配内存时,每次至少分配一个页面。但当请求分配的内存大小为几十个字节或几百个字节时应该如何处理?如何在一个页面中分配小的内存区,小内存区的分配所产生的内碎片又如何解决?
Linux20采用的解决办法是建立了13个空闲区链表,它们的大小从32字节到132056字节。从Linux22开始,MM的开发者采用了一种叫做slab的分配模式,该模式早在1994年就被开发出来,用于Sun Microsystem Solaris 24 *** 作系统中。Slab的提出主要是基于以下考虑:
· 内核对内存区的分配取决于所存放数据的类型。例如,当给用户态进程分配页面时,内核调用get_free_page()函数,并用0填充这个页面。 而给内核的数据结构分配页面时,事情没有这么简单,例如,要对数据结构所在的内存进行初始化、在不用时要收回它们所占用的内存。因此,Slab中引入了对象这个概念,所谓对象就是存放一组数据结构的内存区,其方法就是构造或析构函数,构造函数用于初始化数据结构所在的内存区,而析构函数收回相应的内存区。但为了便于理解,你也可以把对象直接看作内核的数据结构。为了避免重复初始化对象,Slab分配模式并不丢弃已分配的对象,而是释放但把它们依然保留在内存中。当以后又要请求分配同一对象时,就可以从内存获取而不用进行初始化,这是在Solaris 中引入Slab的基本思想。
实际上,Linux中对Slab分配模式有所改进,它对内存区的处理并不需要进行初始化或回收。出于效率的考虑,Linux并不调用对象的构造或析构函数,而是把指向这两个函数的指针都置为空。Linux中引入Slab的主要目的是为了减少对伙伴算法的调用次数。
· 实际上,内核经常反复使用某一内存区。例如,只要内核创建一个新的进程,就要为该进程相关的数据结构(task_struct、打开文件对象等)分配内存区。当进程结束时,收回这些内存区。因为进程的创建和撤销非常频繁,因此,Linux的早期版本把大量的时间花费在反复分配或回收这些内存区上。从Linux22开始,把那些频繁使用的页面保存在高速缓存中并重新使用。
· 可以根据对内存区的使用频率来对它分类。对于预期频繁使用的内存区,可以创建一组特定大小的专用缓冲区进行处理,以避免内碎片的产生。对于较少使用的内存区,可以创建一组通用缓冲区(如Linux20中所使用的2的幂次方)来处理,即使这种处理模式产生碎片,也对整个系统的性能影响不大。
· 硬件高速缓存的使用,又为尽量减少对伙伴算法的调用提供了另一个理由,因为对伙伴算法的每次调用都会“弄脏”硬件高速缓存,因此,这就增加了对内存的平均访问次数。
Slab分配模式把对象分组放进缓冲区(尽管英文中使用了Cache这个词,但实际上指的是内存中的区域,而不是指硬件高速缓存)。因为缓冲区的组织和管理与硬件高速缓存的命中率密切相关,因此,Slab缓冲区并非由各个对象直接构成,而是由一连串的“大块(Slab)”构成,而每个大块中则包含了若干个同种类型的对象,这些对象或已被分配,或空闲,如图612所示。一般而言,对象分两种,一种是大对象,一种是小对象。所谓小对象,是指在一个页面中可以容纳下好几个对象的那种。例如,一个inode结构大约占300多个字节,因此,一个页面中可以容纳8个以上的inode结构,因此,inode结构就为小对象。Linux内核中把小于512字节的对象叫做小对象。
欢迎分享,转载请注明来源:内存溢出
评论列表(0条)