内存管理
一、虚拟内存地址空间
二、内存寻址
- 内存分段
- 内存分页
- 段页式内存管理
三、内存的分配与释放
01 slab分配模式
产生原由
内核在运行时,经常需要在内核空间动态申请内存。内存分页的时候会产生较多的碎片,造成了内存空间的浪费。
于是,这就促使人们在不破坏页管理机制的条件下,考虑更小的内存分配粒度。所以自从Linux2.2开始,设计者在Linux系统中采用了一个叫做slab的小对象分配模式。
这里区分一下外部碎片和内部碎片:
假设一段连续的页框,阴影部分表示已经被使用的页框,现在需要申请一个连续的5个页框。这个时候,在这段内存上不能找到连续的5个空闲的页框,就会去另一段内存上去寻找5个连续的页框,这样子,久而久之就形成了页框的浪费。称为外部碎片。内核中使用伙伴算法的迁移机制很好的解决了这种外部碎片。
内部碎片:当我们申请几十个字节的时候,内核也是给我们分配一个页,这样在每个页中就形成了很大的浪费。称之为内部碎片。内核中引入了slab机制去尽力的减少这种内部碎片。
slab概念
slab模式是20世纪90年代提出的一个为小数据分配内存空间的方法。在设计slab模式时,人们看到:内核的这些小数据虽然量很大,但是种类并不多,于是就提出了这样一个思想:把若干的页框合在一起形成一大存储块——slab,并在这个slab中只存储同一类数据,这样就可以在这个slab内部打破页的界限,以该类型数据的大小来定义分配粒度,存放多个数据,这样就可以尽可能地减少页内碎片了。在Linux中,多个存储同类数据的slab的集合叫做一类对象的缓冲区——cache。注意,这不是硬件的那个cache,只是借用这个名词而已。
采用slab模式的另一个考虑就是:一些内核数据不但需要为其分配内存,而且还经常需要对它们进行一些比较费时的初始化操作。这样,当这些数据在内核运行时频繁地创建和撤销,就消耗了大量的CPU时间。而slab模式恰好就能解决这个问题。
但是目前,Linux中的缓冲区只才在用了前一个功能,并没有使用构造函数和析构函数。
slab分配机制:slab分配器是基于对象进行管理的,所谓的对象就是内核中的数据结构(例如:task_struct、file_struct 等)。相同类型的对象归为一类,每当要申请这样一个对象时,slab分配器就从一个slab列表中分配一个这样大小的单元出去,而当要释放时,将其重新保存在该列表中,而不是直接返回给伙伴系统,从而避免外部碎片。
Linux在一个缓冲区中,用链表来管理多个slab,而且把这些slab分成三段:第一段是所有已经被对象充满的slab;第二段是半满的slab;第三段是空闲的slab。slab模式的示意图如下:
也就是说,Linux 的slab 可有三种状态:
满的:slab 中的所有对象被标记为使用;
空的:slab 中的所有对象被标记为空闲;
部分:slab 中的对象有的被标记为使用,有的被标记为空闲。
slab 分配器首先从部分空闲的slab 进行分配。如没有,则从空的slab 进行分配。如没有,则从物理连续页上分配新的slab,并把它赋给一个cache ,然后再从新slab 分配空间。
在文件mm/slab.c中定义的slab的数据结构如下:
struct slab {
struct list_head list; //链表结构
unsigned long colouroff; //对象区的起点与slab起点之间的偏移
void *s_mem; /* 对象区在slab中的起点 */
unsigned int inuse; /* 记录已分配对象空间数目的计数器 */
kmem_bufctl_t free; //指向了对象链中的第一个空闲对象
unsigned short nodeid;
};
同一个缓冲区的slab按对象空间已满、半满和全空顺序组成了链表。为了解slab中有多少对象空间已经被占用,在链表的结构中有成员inuse;为给内核新申请的对象分配空间,在结构中有成员free。
Linux用数据结构kmem_cache_t描述一个缓冲区,并管理该缓冲区中的slab链表。
slab的优缺点
slab的优点:
(1)内核通常依赖于对小对象的分配,它们会在系统生命周期内进行无数次分配。slab 缓存分配器通过对类似大小的对象进行缓存而提供这种功能,从而避免了常见的碎片问题;
(2)slab 分配器还支持通用对象的初始化,从而避免了为同一目的而对一个对象重复进行初始化;
(3)slab 分配器还可以支持硬件缓存对齐和着色,这允许不同缓存中的对象占用相同的缓存行,从而提高缓存的利用率并获得更好的性能。
slab的缺点:
(1)较多复杂的队列管理。在slab分配器中存在众多的队列,例如针对处理器的本地缓存队列,slab中空闲队列,每个slab处于一个特定状态的队列之中。所以,管理太费劲了。
(2)slab管理数据和队列的存储开销比较大。每个slab需要一个struct slab数据结构和一个管理者kmem_bufctl_t型的数组。当对象体积较小时,该数组将造成较大的开销(比如对象大小为32字节时,将浪费1/8空间)。同时,缓冲区针对节点和处理器的队列也会浪费不少内存。
(3)缓冲区回收、性能调试调优比较复杂。
02 伙伴系统(buddy system)
产生缘由
伙伴系统是内核用来管理物理内存的一种算法(需要注意的是它是用来管理物理内存的,而不是映射后的虚拟内存),在物理内存中会除了内核和一些特殊用途的内存外,其余的空闲内存就会交给内核内存管理系统统一管理和分配。
如果没有伙伴系统就会出现内存断断续续的情况,如:
假设这是一段连续的页框,阴影部分表示已经被使用的页框,现在需要申请一个连续的5个页框。这个时候,在这段内存上不能找到连续的5个空闲的页框,就会去另一段内存上去寻找5个连续的页框,这样子,久而久之就形成了页框的浪费。
伙伴系统就是为了缓解这种碎片化(注意:是缓解!!!),它把管理起来的内存分为了不同的组,总共11组,每个组中的内存块大小都是一样的,都是2的幂次个物理页。
分配方法
当分配内存时,会优先从大小匹配的内存链表中查找是否有空闲的内存,当发现对应大小的内存都被使用完毕后,就会向大一倍的内存链表去寻找空闲内存。
但并不是直接去使用更大内存链表中的内存块,而是把内存块分为相同的两份,一份给请求者使用,另一份则给下一级,也就是对应内存块的上的内存链表上。
如:现在需要分配一个大小为8K内存,但是在对应内存块的内存链表上已经没有了,那么伙伴系统就会去16K内存链表上去查找一个空闲内存块,并分为两个8K的内存块。一个给申请内存块的使用,另一个则加入到8K的内存链表中去进行管理。当然如果16K的也没有空闲内存块了,就会去更高一级的32KB中去查询有没有空间的内存块,会把32K分成一个16K和两个8K,16K放到16K的内存块中管理,两个8K的,一个返回给申请者,一个8K的放到8K的内存块中管理。
具体过程:
伙伴系统是一个结合了2的方幂个分配器和空闲缓冲区合并计技术的内存分配方案, 其基本思想很简单. 内存被分成含有很多页面的大块, 每一块都是2个页面大小的方幂. 如果找不到想要的块, 一个大块会被分成两部分, 这两部分彼此就成为伙伴. 其中一半被用来分配, 而另一半则空闲. 这些块在以后分配的过程中会继续被二分直至产生一个所需大小的块. 当一个块被最终释放时, 其伙伴将被检测出来, 如果伙伴也空闲则合并两者.
03 内存池
在 Linux 内核中还包含对内存池的支持,内存池技术也是一种非常经典的用于分配大量小对象的后备缓存技术。
- 创建内存池
mempool_t *mempool_create(int min_nr, mempool_alloc_t *alloc_fn, \
mempool_free_t *free_fn, void *pool_data);
- 分配和回收对象
在内存池中分配和回收对象需由以下函数来完成:
void *mempool_alloc(mempool_t *pool, int gfp_mask);
void mempool_free(void *element, mempool_t *pool);
- 销毁内存池
void mempool_destroy(mempool_t *pool);
mempool_create()函数创建的内存池需由 mempool_destroy()来回收。
04 kmalloc与vmalloc
- kmalloc
kmalloc()函数类似与我们常见的malloc()函数,前者用于内核态的内存分配,后者用于用户态。
kmalloc()函数在物理内存中分配一块连续的存储空间,且和malloc()函数一样,不会清除里面的原始数据,如果内存充足,它的分配速度很快。其原型如下:
static inline void *kmalloc(size_t size, gfp_t flags); /*返回的是虚拟地址*/
- vmalloc
vmalloc()一般用在为只存在于软件中(没有对应的硬件意义)的较大的顺序缓冲区分配内存,当内存没有足够大的连续物理空间可以分配时,可以用该函数来分配虚拟地址连续但物理地址不连续的内存。由于需要建立新的页表,所以它的开销要远远大于kmalloc及后面将要讲到的__get_free_pages()函数。且vmalloc()不能用在原子上下文中,因为它的内部实现使用了标志为 GFP_KERNEL 的kmalloc()。其函数原型如下:
void *vmalloc(unsigned long size);
void vfree(const void *addr);