内核要分配一组连续的页框,必须建立一种健壮、高效的分配策略。为此,必须解决著名的外部碎片(external fragmentation)问题。频繁地请求和释放不同大小的一组连续页框,必然导致在已分配页框的块内分散了许多小块的空闲页框。由此带来的问题是,即使有足够的空闲页框可以满足请求,但要分配一个大块的连续页框就可能无法满足。Linux内核内存管理的一项重要工作就是如何在频繁申请释放内存的情况下,避免碎片的产生。
1、什么是伙伴系统
伙伴系统的宗旨就是用最小的内存块来满足内核的对于内存的请求。在最初,只有一个块,也就是整个内存,假如为1M大小,而允许的最小块为64K,那么当我们申请一块200K大小的内存时,就要先将1M的块分裂成两等分,各为512K,这两分之间的关系就称为伙伴,然后再将第一个512K的内存块分裂成两等分,各位256K,将第一个256K的内存块分配给内存,这样就是一个分配的过程。下面我们结合示意图来了解伙伴系统分配和回收内存块的过程。
1 初始化时,系统拥有1M的连续内存,允许的最小的内存块为64K,图中白色的部分为空闲的内存块,着色的代表分配出去了得内存块。
2 程序A申请一块大小为34K的内存,对应的order为0,即2^0=1个最小内存块
2.1 系统中不存在order 0(64K)的内存块,因此order 4(1M)的内存块分裂成两个order 3的内存块(512K)
2.2 仍然没有order 0的内存块,因此order 3的内存块分裂成两个order 2的内存块(256K)
2.3 仍然没有order 0的内存块,因此order 2的内存块分裂成两个order 1的内存块(128K)
2.4 仍然没有order 0的内存块,因此order 1的内存块分裂成两个order 0的内存块(64K)
2.5 找到了order 0的内存块,将其中的一个分配给程序A,现在伙伴系统的内存为一个order 0的内存块,一个order 1的内存块,一个order 2的内存块以及一个order 3的内存块
3 程序B申请一块大小为66K的内存,对应的order为1,即2^1=2个最小内存块,由于系统中正好存在一个order 1的内存块,所以直接用来分配
4 程序C申请一块大小为35K的内存,对应的order为0,同样由于系统中正好存在一个order 0的内存块,直接用来分配
5 程序D申请一块大小为67K的内存,对应的order为1
5.1 系统不存在order 1的内存块,于是将order 2的内存块分裂成两块order 1的内存块
5.2 找到order 1的内存块,进行分配
6 程序B释放了它申请的内存,即一个order 1的内存块
7 程序D释放了它申请的内存
7.1 一个order 1的内存块回收到内存当中
7.2由于该内存块的伙伴也是空闲的,因此两个order 1合并成一个order 2的内存块
8 程序A释放了它申请的内存,即一个order 0的内存块
9 程序C释放了它申请的内存
9.1 一个order 0的内存块被释放
9.2 两个order 0伙伴块都是空闲的,进行合并,生成一个order 1的内存块m
9.3 两个order 1伙伴块都是空闲的,进行合并,生成一个order 2的内存块
9.4 两个order 2伙伴块都是空闲的,进行合并,生成一个order 3的内存块
9.5 两个order 3伙伴块都是空闲的,进行合并,生成一个order 4的内存块
2.1 内核启动mem_init函数分析
它的调用过程是这样的:mem_init()->free_all_bootmem_node():我们知道这个函数是统计一共释放了多少空闲页。->free_all_bootmem_core()就是在这个函数里面先后多次调用__free_pages()函数。目的就是为了释放每个内存node里面无用的内存孔洞页。我们现在就来好好探讨这个函数。mem_init和free_all_bootmem_core函数请见《mem_init源码分析.c》
如果cold是0的话,表示要把page所指向内存页释放到其所在内存页区中当前处理器的“热区”内存中,如果为1的话,就释放到这页所在页区中当前处理器的“冷区”高速缓存内存中。在这里肯定有不少人不太了解linux在启动后是如何分配和释放内存的。我在这里讲个大概,在以后的文章中会结合代码细讲的。一个核心概念要记住,多页时通过伙伴机制,单页时使用“热冷区”。我们这个函数先涉及后者机制。我们先来说说这个机制。
首先,大家都很了解struct zoon这个结构体,里面涉及了一个与“冷热区”相关的重要成员:struct per_cpu_pageset pageset[NR_CPUS]里面的成员我不打算全讲,NR_CPUS表示这个系统中cpu对应的号。struct per_cpu_pages pcp[2]这个是我要讲的成员我们来看看struct per_cpu_pages 这个结构体的全貌:
struct per_cpu_pages {
int count;
int low;
int high;
int batch;
struct list_head list;
};
这个结构体大家都是用“高速缓存内存”来称呼它的,这样说它也是表示一段内存啦!没错,我们对于一页的内存释放和分配的时候就会先去这两个内存看看。为什么说是两个内存?因为我们每个zone都会有冷区和热区,zone中的struct per_cpu_pageset[]对应着的其中一个cpu的冷区和热区,冷区我们会用pageset[n]->pcp[1],热区我们会用pageset[n]->pcp[0]来表示。我们会想,这个per_cpu_pages是怎么表示一段内存呢?我们会发现这里有个链表list,它是指向一段内存的开头。count就是表示现在有多少内存页。当我们要释放一页内存的时候,我们先看看找到这页所属的内存区zone当前cpu的热区或是冷区。如果此时发现count>high时,我们先把list的前batch数量的内存页释放到这个页区的free_area中(这个和伙伴机制有关,我们等下再讲),然后将要释放的页添加到list中。当我们要分配时,我们先考虑热区或是冷区的count是否大于low,如果不是的话,我们先要从该页区一页一页释放batch数量内存页到list链表中,然后我们才从list中申请一页内存。好了,到这里我们来看看上述过程是如何实现的,但是我们这里只是讨论释放这个过程。
我们现在来看看__free_pages_bulk()函数。
好了,现在已经全部分析了一遍,关于Linux的连续页或是一页内存的释放有了比较深入的理解,我们接下来会说说申请的大概过程。
2.2 内存分配分析
__alloc_pages分析请详见: mem_init源码分析.c
注意:
在linux内核中伙伴系统用来管理物理内存,其分配的单位是页,但是向用户程序一样,内核也需要动态分配内存,而伙伴系统分配的粒度又太大。由于内核无法借助标准的C库,因而需要别的手段来实现内核中动态内存的分配管理,linux采用的是slab分配器。slab分配器不仅可以提供动态内存的管理功能,而且可以作为经常分配并释放的内存的缓存。
kmalloc是基于slab的,所以速度比较快。vmalloc的内部会调用到kmalloc。
Slab最终会调用到__alloc_pages来给分配器申请一些内存。