linux内存管理概述
内存管理的目标:
提供一种方法,在各种目的各个用户之间实现内存共享,应该实现以下两个功能:
1、最小化管理内存的时间,内存申请和释放响应时间短
2、最优化用于一般应用的可用内存,内存管理(算法)所占用的内存少,浪费的内存少(内存碎片少)
下图为内存分配器的关系:
1、kmalloc用于分配一块以字节数为单位的内存,所分配的内存物理地址是连续的
void *kmalloc(size_t size, gfp_t flags);
size > SLUB_MAX_SIZE(2*PAGE_SIZE),通过buddy system分配,否则通过slab分配器来分配
2、vmalloc分配的内存在虚拟地址上是连续的,而物理地址无需连续
void *vmalloc(unsigned long size);
3、buddy system是Linux内存管理的核心部分。用户态内存是通过缺页处理由buddy system分配,然后再由用户态的内存管理算法进行管理
内存管理概述图
这张图中描述了包含很多的信息,如果全部都了解了内存管理也就大概了解了。
buddy system系统
外碎片:频繁地请求和释放不同大小的一组连续页框,必然导致在已分配页框的块内分散了许多小块的空闲页框。由此带来的问题是,即使有足够多的空闲页框可以可以满足请求,但要分配一个大块的连续页框就可能无法满足。
伙伴系统算法就是要解决内存管理中著名的外碎片问题。
buddy system把页框(一般4k)作为基本单位,对物理内存进行管理。其思想是以2的幂为基准来合并或者拆分连续的页。幂(以order表示)的范围一般从0到10,代表了1,2,4,8,16,32,64,128,256,512,1024个连续的页框。
根据order的不同,空闲页框由11个链表来管理,例如order 2的链表,其每个节点表示4个连续的页框。
当用户申请1个页时:
1、buddy system会先搜索order 0的链表,看看是否为空
a)如果不会空,则将1个节点(1页)返回给用户
b)如果链表为空,则搜索order 1的链表
2、buddy system 搜索order 1的链表,看看是否为空
a)如果链表不为空,则将1个order(2页)的节点拆分成2个order 0的节点,然后将其中一个返回给用户
b)如果链表为空,则搜索order 2的链表
释放内存的过程是分配的逆过程。buddy system算法试图把大小为size的一对空闲节点合并为大小2*size的节点
slab分配器
内碎片:内碎片产生的主要原因是由于请求内存的大小与分配给它的大小不匹配造成的
采用伙伴算法分配内存时,每次至少分配一个页面。但当请求分配的内存大小为几十个字节或几百个字节时应该如何处理?如何在一个页面中分配小的内存区,小内存区的分配所产生的内碎片又如何解决?Linux采用Slab
这里先给出slab分配器的一些特性,下面的文字可能晦涩难懂,大家可以先把后面的数据结构和相关算法掌握了再回过头来浏览:
(1)所存放数据的类型可以影响内存区的分配方式。例如,当给用户态进程分配一个页框时,内核调用get_zeroed_page()函数用0 填充这个页。slab 分配器概念扩充了这种思想,并把内存区看作对象(object),这些对象由一组数据结构和几个叫做构造(constructor)或析构(destructor)的函数(或方法)组成。前者初始化内存区,而后者回收内存区。为了避免重复初始化对象,slab分配器并不丢弃已分配的对象,而是释放但把它们保存在内存中。于是,slab分配器具有了想当一部分缓存的功能,当以后又要请求新的对象时,就可以从内存获取而不用重新初始化。
(2)内核函数倾向于反复请求同一类型的内存区。例如,只要内核创建一个新进程,它就要为一些固定大小的数据结构分配内存区。当进程结束时,包含这些数据结构的内存区还可以被重新使用。因为进程的创建和撤消非常频繁,在没有slab分配器时,内核把时间浪费在反复分配和回收那些包含同一内存区的页框上;slab分配器把那些页框保存在高速缓存中并很快地重新使用它们。
(3)对内存区的请求可以根据它们发生的频率来分类。对于预期频繁请求一个特定大小的内存区而言,可以通过创建一组具有适当大小的专用对象来高效地处理,由此以避免内碎片的产生。另一种情况,对于很少遇到的内存区大小,可以通过基于一系列几何分布大小(如早期Linux 版本所使用的2的幂次方大小)的对象的分配模式来处理,即使这种方法会导致内碎片的产生(即普通缓存与专用缓存)。
(4)在引入的对象大小不是几何分布的情况下,也就是说,数据结构的起始地址不是物理地址值的2 的幂次方,事情反倒好办。这可以借助处理器硬件高速缓存而导致较好的性能。
(5)硬件高速缓存的高性能又是尽可能地限制对伙伴系统分配器调用的另一个理由,因为对伙伴系统函数的每次调用都“弄脏”硬件高速缓存,所以增加了对内存的平均访问时间。内核函数对硬件高速缓存的影响就是所谓的函数“足迹(footprint)”,其定义为函数结束时重写高速缓存的百分比。显而易见,大的“足迹”导致内核函数刚执行之后较慢的代码执行,因为硬件高速缓存此时填满了无用的信息。
slab 分配器把对象分组放进高速缓存。每个高速缓存都是同种类型对象的一种“储备”。例如,当一个文件被打开时,存放相应“打开文件”对象所需的内存区是从一个叫做filp(“文件指针”)的slab 分配器的高速缓存中得到的。
包含高速缓存的主内存区被划分为多个slab,每个slab 由一个或多个连续的页框组成,这些页框中既包含已分配的对象,也包含空闲的对象。我们将在以后有关回收页框的博文中看到,内核周期性地扫描高速缓存并释放空slab 对应的页框。
slab 分配器的主要结构
每个缓存都包含了一个 slabs 列表,这是一段连续的内存块(通常都是页面)。存在 3 种 slab:
slabs_full 完全分配的 slab
slabs_partial 部分分配的 slab
slabs_empty 空 slab,或者没有对象被分配
注意:slabs_empty 列表中的 slab 是进行回收(reaping)的主要备选对象。正是通过此过程,slab 所使用的内存被返回给操作系统供其他用户使用。
slab 列表中的每个 slab 都是一个连续的内存块(一个或多个连续页),它们被划分成一个个对象。这些对象是从特定缓存中进行分配和释放的基本元素。注意 slab 是 slab 分配器进行操作的最小分配单位,因此如果需要对 slab 进行扩展,这也就是所扩展的最小值。通常来说,每个 slab 被分配为多个对象。
由于对象是从 slab 中进行分配和释放的,因此单个 slab 可以在 slab 列表之间进行移动。例如,当一个 slab 中的所有对象都被使用完时,就从 slabs_partial 列表中移动到 slabs_full 列表中。当一个 slab 完全被分配并且有对象被释放后,就从 slabs_full 列表中移动到 slabs_partial 列表中。当所有对象都被释放之后,就从 slabs_partial
列表移动到 slabs_empty 列表中。
数据结构全图如下所示
普通和专用高速缓存
高速缓存被分为两种类型:普通和专用。普通高速缓存只有slab分配器用于自己的目的,而专用高速缓存由内核的其余部分使用。
普通高速缓存:
内存区大小的范围一般包括13个集合分布的内存区。一个叫做malloc_sizes的表(其元素类型为cache_sizes)分别指向26个高速缓存描述符,与其相关的内存区大小为32, 64, 128, 256, 512,1024, 2048, 4096, 8192, 16384, 32768, 65536
和131072 字节。对于每种大小,都有两个高速缓存:一个适用于ISA DMA 分配,另一个适用于常规分配。在系统初始化期间调用kmem_cache_init()来建立普通高速缓存
专用高速缓存是由kmem_cache_create()函数创建。
Linux内存信息查看
/proc/meminfo 查看系统整体的内存信息
/proc/buddyinfo 查看空闲物理页的信息
/proc/pagetypeinfo 查看不同类型的空闲物理页信息
/proc/slabinfo 查看slab分配器的所有cache信息
/sys/kernel/slab/ 目录下是slab分配器的各个cache的具体信息