在内核初始化完成之后, 内存管理的责任就由伙伴系统来承担. 伙伴系统基于一种相对简单然而令人吃惊的强大算法.
Linux内核使用二进制伙伴算法来管理和分配物理内存页面, 该算法由Knowlton设计, 后来Knuth又进行了更深刻的描述.
伙伴系统是一个结合了2的方幂个分配器和空闲缓冲区合并计技术的内存分配方案, 其基本思想很简单. 内存被分成含有很多页面的大块, 每一块都是2个页面大小的方幂. 如果找不到想要的块, 一个大块会被分成两部分, 这两部分彼此就成为伙伴. 其中一半被用来分配, 而另一半则空闲. 这些块在以后分配的过程中会继续被二分直至产生一个所需大小的块. 当一个块被最终释放时, 其伙伴将被检测出来, 如果伙伴也空闲则合并两者.
- 内核如何记住哪些内存块是空闲的
- 分配空闲页面的方法
- 影响分配器行为的众多标识位
内存碎片的问题和分配器如何处理碎片
当buddy系统还有大量的连续物理内存时,我们可以通过
__pages_alloc
成功分配很大的一块连续物理内存空间,随着系统运行时间加长,buddy系统内很难中找到一块大的连续物理内存空间,因此__pages_alloc
可能会失败,即便通过kswapd进行页面的回收和交换,buddy仍然不可避免的碎片化
首先我们要明确的是,连续物理内存的分配并不是必要的。对于大部分DMA操作,我们的确需要连续的物理内存;但是对于某些分配内存情况:比如,模块加载,设备和声音驱动程序中,可以在内核源码中关键字vmalloc查找,对vmalloc的使用有个感性认识。
vmalloc把buddy系统内的不连续物理内存,映射到内核中一段连续的地址空间内,因此对于那些无法直接映射的高端物理内存Highmem来说,vmalloc是主要用途之一。因此vmalloc理应优先使用廉价的Highmem内存,而把宝贵的低端内存,留给其他的内核操作。事实上也是如此,vmalloc实现函数的分配标志,指明了从Highmem分配
vmalloc是一个接口函数, 内核代码使用它来分配在虚拟内存中连续但在物理内存中不一定连续的内存
/**
* vmalloc - allocate virtually contiguous memory
* @size: allocation size
* Allocate enough pages to cover @size from the page level
* allocator and map them into contiguous kernel virtual space.
*
* For tight control over page level allocator and protection flags
* use __vmalloc() instead.
*/
void *vmalloc(unsigned long size)
{
return __vmalloc_node_flags(size, NUMA_NO_NODE,
GFP_KERNEL | __GFP_HIGHMEM);
}
对于vmalloc来说是需要预留一定的地址空间的。而DMA和Normal内存zone 又需要占用数百M的地址空间,参见下面这个经典的kernel地址空间划分图
Persistent mappings和Fixmaps地址空间都比较小,这里我们忽略它们,这样只剩下直接地址映射和VMALLOC区,这个划分应该是平衡两个需求的结果
尽量增加DMA和Normal区大小,也就是直接映射地址空间大小,当前主流平台的内存,基本上都超过了512MB,很多都是标配1GB内存,因此注定有一部分内存无法进行线性映射。
保留一定数量的VMALLOC大小,这个值是应用平台特定的,如果应用平台某个驱动需要用vmalloc分配很大的地址空间,那么最好通过在kernel参数中指定vmalloc大小的方法,预留较多的vmalloc地址空间。
- 并不是Highmem没有或者越少越好,这个是我的个人理解,理由如下:高端内存就像个垃圾桶和缓冲区,防止来自用户空间或者vmalloc的映射破坏Normal zone和DMA zone的连续性,使得它们碎片化。当这个垃圾桶较大时,那么污染Normal 和DMA的机会自然就小了。
下面的图是VMALLOC地址空间内部划分情况
在直接地址映射和VMALLOC区之间有一个8MiB的隔离带,隔离带是做什么的呢? 隔离带是用来针对内核故障的保护措施,当访问虚拟地址越界时,则会产生一个page fault异常,也就是说这个内核地址空间没有对应相应的物理地址,这在内核地址空间是不允许的。如果不存在隔离带,那么越界访问不知不觉的跨越直接映射和VMALLOC区,内核却没注意到这个错误。
在VMALLOC内部,会划分为多个vmalloc_area
,每个vmalloc_area
直间有一个4KB的地址空隙,通过这个小的隔离,可以防止不同映射区直接的越界访问。
数据结构
内核在管理虚拟内存中的vmalloc区域时, 内核必须跟踪哪些子区域被使用、哪些是空闲的. 为此定义了一个数据结构vm_struct, 将所有使用的部分保存在一个链表中. 该结构提的定义在include/linux/vmalloc.h
struct vm_struct {
struct vm_struct *next;//所有的vm_struct通过next 组成一个单链表,表头为全局变量vmlist
void *addr;//定义了这个虚拟地址空间子区域的起始地址
unsigned long size; //定义了这个虚拟地址空间子区域的大小
unsigned long flags; //存储了与该内存区关联的标志
struct page **pages; //是一个指针,指向page指针的数组,每个数组成员都表示一个映射到这个地址空间的物理页面的实例
unsigned int nr_pages; //page指针数据的长度
phys_addr_t phys_addr; //仅当用ioremap映射了由物理地址描述的物理内存区域才有效。
const void *caller;
};
其中flags只用于指定内存区类型, 所有可能的flag标识以宏的形式定义在include/linux/vmalloc.h
/* bits in flags of vmalloc's vm_struct below */
#define VM_IOREMAP 0x00000001 /* ioremap() and friends *///表示将几乎随机的物理内存区域映射到vmalloc区域中. 这是一个特定于体系结构的操作
#define VM_ALLOC 0x00000002 /* vmalloc() *///指定由vmalloc产生的子区域
#define VM_MAP 0x00000004