Linux内核使用Buddy System来负责连续页面的分配与释放,内核将连续的内存页面按照固定的数量进行分组,对于任何一个分组,合法的页面数量是 2^n 且 0<=n<=10, 大小相等且相邻的两个分组是彼此的Buddy, 例如下图中,当 n=1 时,组的页面数量是2, 此时 Group 1 与 Group 2 互为Buddy, 但Group 1 与 Group 3 由于隔开了,就不是Buddy.
我们定义Buddy这个概念的目的是在内存分配与释放的过程中,能够动态地维护尽可能长的连续内存。例如上图中,如果 Group1 与 Group 2 都被释放回来后,系统就将会对两个Buddy进行合并,形成一个数量更大的组Group 4, 这也是两个Buddy必须相邻的原因。在分配内存时,Buddy System 会首先根据请求的页面数量确定目标分组,如果请求的连续内存数量是k, 那么Buddy System 将选择满足 2^n >= k 的最小的数字n 对应的那个分组,如果Buddy System 中没有对应的分组,那么系统将会将更大的分组劈开形成一对Buddy, 然后看劈开后的分组是否适合,如果不行的话就继续劈开,直到到达合适的大小为止。下图演示了一个Buddy System 从64页连续内存中分配8页的过程:
系统本来只有一个64 页的分组,完成此次分配之后,系统中可用的分组如下:一个8页分组、一个16页分组、以及一个32页的分组,即图中红色方框的部分。由于每个分组的页数都满足 2^n, 为了避免分配页面时出现碎片,Buddy System要求分配时请求的页面数量也必须满足 2^n 的数字大小,如果实际请求的数字 k 不满足2的n次方的话,Buddy System会分配大于等于 k 的最小的2的某次方的那个数字进行分配,例如如果系统请求的页面数量是7, 那么系统实际上会分配8个页面。
当释放内存时,Buddy System 会查看释放分组的Buddy 是否空闲,如果空闲的话便会合并两个分组形成一个更大的分组,并递归该操作直到当前分组的Buddy 繁忙(指被分配出去还未释放)或已经形成最大数量的分组为止。例如上图中,如果系统释放刚刚分配回去的8 个页面的话,Buddy System 又会自底向上地合并所有的分组,最终又形成一个64页的分组。
由此可知,图2中的Group 2与 Group 3虽然数量相等且相邻,但他们并不是Buddy, 根据Buddy System 中对于分组数量的规定,两个分组如何可合并的话,那么第一个分组的首地址必然是合并后组内页面数量的整数倍。如果将Group 2与Group 3合并,将得到一个页数是4的分组,因此Group 2的起始地址应该是4的整数倍,但Group 2的起始地址是2, 因此Group 2与Group 3不会有合并的机会。
Buddy System是内存管理的基础,其分配的对象的连续的物理页面,这是一个比较大的粒度,内核会在Buddy System 之上构建更加精细的内存分配机制提供给程序使用,这就是后续将要介绍的Slab系统。
3.2.4.2 数据结构
Buddy System从 zone 中分配内存,zone 中使用一个数组来管理不同大小的Buddy, 该字段定义如下:
/* file: include/linux/mmzone.h */
struct zone {
/* free areas of different sizes */
struct free_area free_area[MAX_ORDER];
}
MAX_ORDER 的值是 11, 也就是说,Buddy System 一次能够分配的最大连续页面是 210=1024 个,即4M的连续内存。数组 free_area 每个索引 i 存放的就是大小为 2i 页面数量的分组,这样可以提升Buddy System分配内存时查找分组的效率。数据结构 free_area 的定义如下:
/* file: include/linux/mmzone.h */
typedef struct pglist_data {
/*
* node_zonelists contains references to all zones in all nodes.
* Generally the first zones will be references to this node's
* node_zones.
*/
struct zonelist node_zonelists[MAX_ZONELISTS];
}
/*
* One allocation request operates on a zonelist. A zonelist
* is a list of zones, the first one is the 'goal' of the
* allocation, the other zones are fallback zones, in decreasing
* priority.
*
* To speed the reading of the zonelist, the zonerefs contain the zone index
* of the entry being read. Helper functions to access information given
* a struct zoneref are
*
* zonelist_zone() - Return the struct zone * for an entry in _zonerefs
* zonelist_zone_idx() - Return the index of the zone for an entry
* zonelist_node_idx() - Return the index of the node for an entry
*/
struct zonelist {
struct zoneref _zonerefs[MAX_ZONES_PER_ZONELIST + 1];
};
/*
* This struct contains information about a zone in a zonelist. It is stored
* here to avoid dereferences into large structures and lookups of tables
*/
struct zoneref {
struct zone *zone; /* Pointer to actual zone */
int zone_idx; /* zone_idx(zoneref->zone) */
};
数组 node_zonelists 长度通过 MAX_ZONELISTS 确定:
/* file: include/linux/mmzone.h */
enum {
ZONELIST_FALLBACK, /* zonelist with fallback */
#ifdef CONFIG_NUMA
/*
* The NUMA zonelists are doubled because we need zonelists that
* restrict the allocations to a single node for __GFP_THISNODE.
*/
ZONELIST_NOFALLBACK, /* zonelist without fallback (__GFP_THISNODE) */
#endif
MAX_ZONELISTS
};
在NUMA架构下,该数组包含着两个列表,一个 fallback 列表一个 nofallback 列表,前者包含着系统中所有的 zone, 列表的排列顺序就是Buddy System 分配内存时选择 zone 的优先级顺序,后者仅包含当前 node 的 zone。fallback 的意思是当前 node 的 zone 无法满足要求时,是否需要使用其它 node 中的 zone 来顶替。 zonelist 就是一个 zone 的数组,Buddy System分配内存时就会遍历整个数组,从第一个合适的 zone 中分配内存,zonelist 的长度定义如下:
/* file: mm/page_alloc.c */
/*
* Build zonelists ordered by zone and nodes within zones.
* This results in conserving DMA zone[s] until all Normal memory is
* exhausted, but results in overflowing to remote node while memory
* may still exist in local DMA zone.
*/
static void build_zonelists(pg_data_t *pgdat)
{
static int node_order[MAX_NUMNODES];
int node, load, nr_nodes = 0;
nodemask_t used_mask = NODE_MASK_NONE;
int local_node, prev_node;
/* NUMA-aware ordering of nodes */
local_node = pgdat->node_id;
load = nr_online_nodes;
prev_node = local_node;
memset(node_order, 0, sizeof(node_order));
/* find_next_best_node 会寻找下一个距离 local_node 最近的 node */
while ((node = find_next_best_node(local_node, &used_mask)) >= 0) {
/*
* We don't want to pressure a particular node.
* So adding penalty to the first node in same
* distance group to make it round-robin.
*/
if (node_distance(local_node, node) !=
node_distance(local_node, prev_node))
node_load[node] = load;
node_order[nr_nodes++] = node;
prev_node = node;
load--;
}
build_zonelists_in_node_order(pgdat, node_order, nr_nodes);
build_thisnode_zonelists(pgdat);
}