概述
Linux 开启分页后,物理内存的管理就会交给伙伴系统。伙伴系统有如下特点
- 有点:简单,高效
 - 缺点:只能避免外碎片,不能避免内碎片。不能分配小于一页的内存
 
基本原理
伙伴系统分配内存的基本单位是 "页"。其组织结构有点类似于 "桶"
- 
桶里面有不同 order 的链表头,管理着不同数量page组成的单元, 每个单元里page数量就是 $2^{order}$. 0号链表管理的单元是 1个page, 2号链表管理的单元是2个page....
 - 
需要分配page时,根据 order 从对应链表下进行分配。若不成功,则从更高 order 链表下分配。更高order链表下的单元会被拆成更小的单元,挂到对应 order 的链表下
 - 
当释放 page 时,会考虑该 page 是否能合并。若能合并,则挂载到更高 order 的链表下

图片来源: 深入Linux内核架构 
buddy 与 zone
伙伴系统专注于某个节点的内存域。当首选节点的内存区域无法满足分配请求时,会尝试从其他备用zone 分配。
当前伙伴系统的信息可以通过如下命令查看
> cat /proc/buddyinfo
//横轴表示阶数: order-0...order11
Node 0, zone      DMA      0      0      0      0      0      0      0      0      1      2      2
Node 0, zone    DMA32      6      6      7      8      9      8      8      5      7      7    787
Node 0, zone   Normal   1999   2881   1377   5413   3054    721    234     12      3      7   1562
数据结构及API
数据结构
伙伴系统的数据结构位于 mmzone.h,可以看到非常简介。基本和前面介绍的内容一致。
#define MIGRATE_UNMOVABLE     0
#define MIGRATE_RECLAIMABLE   1
#define MIGRATE_MOVABLE       2
#define MIGRATE_RESERVE       3
#define MIGRATE_ISOLATE       4 /* can't allocate from here */
#define MIGRATE_TYPES         5
struct free_area {
    struct list_head    free_list[MIGRATE_TYPES];
    unsigned long       nr_free;
};
#define MAX_ORDER 11 //默认值是11,否则通过配置 CONFIG_FORCE_MAX_ZONEORDER 决定
struct zone {
    ...
    struct free_area    free_area[MAX_ORDER];
    ...
} ____cacheline_internodealigned_in_smp;
在 2.6.23 以前,引入 struct free_area 仅有一个 free_list。后续引入多个 MIGRATE_TYPES 是为减少物理内存碎片,更多详见 深入linux内核架构: 3.5.2
但我们本次分析会忽略多个 free_list
API
最重要的两类 API 就是分配和释放了。这里仅列出最具代表性的 API 。同时可以看到,linux 同一类的API可以在不同文件中声明。
[include/linux/gfp.h]
struct page *alloc_pages(gfp_t gfp_mask, unsigned int order);
...
[mm/page_alloc.c]
void free_pages(unsigned long addr, unsigned int order);
...
- alloc_pages: 所有分配相关的API,最后的核心都是 __alloc_pages,这也是本篇文章分析的重点
 - free_pages: 核心调用 __free_pages
 
Linux 还提供了很多标志,用于控制分配的行为,具体含义请查阅相关资料
[include/linux/gfp.h]
#define __GFP_WAIT  ((__force gfp_t)0x10u)  /* Can wait and reschedule? */
#define __GFP_HIGH  ((__force gfp_t)0x20u)  /* Should access emergency pools? */
#define __GFP_IO    ((__force gfp_t)0x40u)  /* Can start physical IO? */
...
#define GFP_DMA     __GFP_DMA
#define GFP_DMA32   __GFP_DMA32
#define GFP_NOWAIT  (GFP_ATOMIC & ~__GFP_HIGH)
#define GFP_ATOMIC  (__GFP_HIGH)
...
常用MASK分析
- 
最常用
- GFP_KERNEL: 内核内存的分配,可以睡眠
 - GFP_ATOMIC: 分配过程中不能睡眠
 - GFP_USER: 用户内存的分配,可以睡眠
 
 - 
Zone 角度
- GFP_DMA: 仅在 ZONE_DMA 查找可用内存
 - GFP_NORMAL(默认): 优先考虑 ZONE_NORMAL, 其他是 ZONE_DMA
 - GFP_HIGHMEM: 优先考虑 ZONE_HIGH, 其他是 ZONE_DMA
 
 
Page-flags标志
page->flags
- __ClearPageBuddy(): PG_buddy 位,在伙伴系统中,未被使用需要设置该位。
 
page->private: 该成员表示该page对应伙伴系统中的order
- set_page_private(): 当从伙伴系统中移除时,需要设置将该位清零。
 - rmv_page_order() / set_page_order()
 
伙伴系统初始化
概述
伙伴系统初始化的源码地图如下
[linux-2.6.24]
start_kernel()
-> setup_arch()
   -> zone_sizes_init() -> free_area_init_nodes()
-> mem_init()
zone_sizes_init()
zone_sizes_init() 主要工作是初始化 zone->free_area 成员。
调用链:zone_sizes_init() -> free_area_init_nodes() ->free_area_init_node() -> free_area_init_core() -> init_currently_empty_zone() -> zone_init_free_lists()
忽略冗长的调用链中间过程,我们从 free_area_init_node 开始分析
[linux-2.6.24/mm/page_alloc.c]
void __meminit free_area_init_node(int nid, struct pglist_data *pgdat,
        unsigned long *zones_size, unsigned long node_start_pfn,
        unsigned long *zholes_size)
{
    pgdat->node_id = nid;
    pgdat->node_start_pfn = node_start_pfn;
    calculate_node_totalpages(pgdat, zones_size, zholes_size);
    alloc_node_mem_map(pgdat);
    free_area_init_core(pgdat, zones_size, zholes_size);
}
主要工作如下
- alloc_node_mem_map(): 创建 struct page 管理每一个 page
 - free_area_init_core(): 伙伴系统的核心初始化。该函数会初始化zone,以及最重要的 zone->free_area
 
alloc_node_mem_map()
[mm/memory.c]
struct page *mem_map;
[mm/page_alloc.c]
static void __init_refok alloc_node_mem_map(struct pglist_data *pgdat)
{
#ifdef CONFIG_FLAT_NODE_MEM_MAP
    /* ia64 gets its own node_mem_map, before this, without bootmem */
    if (!pgdat->node_mem_map) {   --- A
        unsigned long size, start, end;
        struct page *map;
        start = pgdat->node_start_pfn & ~(MAX_ORDER_NR_PAGES - 1);  //通常是0
        end = pgdat->node_start_pfn + pgdat->node_spanned_pages;
        end = ALIGN(end, MAX_ORDER_NR_PAGES);
        size =  (end - start) * sizeof(struct page);
        if (!map)
            map = alloc_bootmem_node(pgdat, size);
        pgdat->node_mem_map = map + (pgdat->node_start_pfn - start);
    }
    if (pgdat == NODE_DATA(0)) {
        mem_map = NODE_DATA(0)->node_mem_map;   --- B
        ...
    }
#endif /* CONFIG_FLAT_NODE_MEM_MAP */
}
该函数的主要功能是为 struct page 分配内存空间。这里主要分析平坦内存模型
- A: 计算需要分配的 size, 然后调用 alloc_bootmem_node() 分配对应的页。注意,这里所有的计算都按最大分配阶对齐(为什么呢??)
 - B: 将 struct page 记录到 全局数组 mem_map 中。
 
free_area_init_core()
free_area_init_core 会对 zone 进行系统的初始化。我们暂时仅关注和 zone->frea_area相关的,直接来看 zone_init_free_lists 函数
static void __meminit zone_init_free_lists(struct pglist_data *pgdat,
				struct zone *zone, unsigned long size)
{
	int order, t;
	for_each_migratetype_order(order, t) {
		INIT_LIST_HEAD(&zone->free_area[order].free_list[t]);
		zone->free_area[order].nr_free = 0;
	}
}
代码比较简介。就是初始化 zone->free_area 链表头和 nr_free
mem_init
mem_init 所有的重点就一个,将 bootmem 管理的内存,释放到 buddy 子系统。
void __init mem_init(void)
{
    ...
    /* this will put all low memory onto the freelists */
    totalram_pages += free_all_bootmem();
    ...
}
free_all_bootmem
free_all_bootmem() 实现是调用 free_all_bootmem_core()
static unsigned long __init free_all_bootmem_core(pg_data_t *pgdat)
{
    struct page *page;
    unsigned long pfn;
    bootmem_data_t *bdata = pgdat->bdata;
    unsigned long i, count, total = 0;
    unsigned long idx;
    unsigned long *map;
    int gofast = 0;
    count = 0;
    /* first extant page of the node */
    pfn = PFN_DOWN(bdata->node_boot_start);
    idx = bdata->node_low_pfn - pfn;
    map = bdata->node_bootmem_map;
    /* Check physaddr is O(LOG2(BITS_PER_LONG)) page aligned */
    if (bdata->node_boot_start == 0 ||
        ffs(bdata->node_boot_start) - PAGE_SHIFT > ffs(BITS_PER_LONG))
        gofast = 1;   --- A
    for (i = 0; i < idx; ) {   --- B
        unsigned long v = ~map[i / BITS_PER_LONG];   --- B1
        if (gofast && v == ~0UL) {   --- B2
            int order;
            page = pfn_to_page(pfn);
            count += BITS_PER_LONG;
            order = ffs(BITS_PER_LONG) - 1;
            __free_pages_bootmem(page, order);
            i += BITS_PER_LONG;
            page += BITS_PER_LONG;
        } else if (v) {   --- B3
            unsigned long m;
            page = pfn_to_page(pfn);
            for (m = 1; m && i < idx; m<<=1, page++, i++) {  --- B3.1
                if (v & m) {
                    count++;
                    __free_pages_bootmem(page, 0);
                }
            }
        } else {
            i += BITS_PER_LONG;
        }
        pfn += BITS_PER_LONG;
    }
    total += count;
    /* Now free the allocator bitmap itself, it's not needed anymore */
    page = virt_to_page(bdata->node_bootmem_map);   --- C
    count = 0;
    idx = (get_mapsize(bdata) + PAGE_SIZE-1) >> PAGE_SHIFT;
    for (i = 0; i < idx; i++, page++) {
        __free_pages_bootmem(page, 0);
        count++;
    }
    total += count;
    bdata->node_bootmem_map = NULL;
    return total;
}
将 bootmem 内存释放到伙伴系统,最重要的函数是 __free_pages_bootmem():
- A & B2: 是否能快速释放到 zone->free_area 指定 order 的链表。这里为什么会有这样算法后续分析
 - B: index 就是 bit 的索引,每一个 bit 代表一个 page.
 - B1: 这里以 Long 数据长度为基本单位处理。由于仅释放 bootmem 中未分配的页,故取反就可以知道该组page有没有空闲 page 需要释放。若该组page都已经被使用,取反就是0
 - B3: 遍历该组 page 中哪个bit 为 0,然后调用 __free_pages_bootmem 进行释放。这里空闲的page都会释放到 order=0 的阶数。然后根据让伙伴算法自己来选择是否合并。
 - C: 释放 bootmem bitmap 本身所在的页。
 
__free_pages_bootmem()
void fastcall __init __free_pages_bootmem(struct page *page, unsigned int order)
{
	if (order == 0) {
		__ClearPageReserved(page);
		set_page_count(page, 0);
		set_page_refcounted(page);
		__free_page(page);
	} else {
		int loop;
		prefetchw(page);
		for (loop = 0; loop < BITS_PER_LONG; loop++) {
			struct page *p = &page[loop];
			if (loop + 1 < BITS_PER_LONG)
				prefetchw(p + 1);
			__ClearPageReserved(p);
			set_page_count(p, 0);
		}
		set_page_refcounted(page);
		__free_pages(page, order);
	}
}
参考 页释放[^1],核心是调用 __free_page()
页分配
概述
根据需求,页的分配提供一些经过 wrapped APIs, 本章仅分析所有API都会调用的核心函数:__alloc_pages
struct page *__alloc_pages(gfp_t gfp_mask, unsigned int order, struct zonelist *zonelist);
分配页的过程需要考虑很多复杂的情况
- 正常分配:相对简单,也是初始学习重点分析的流程
 - 内存快耗尽时分配动作,需要和 kswap 配合
 
最简流程
本节主要从 "最简单" 分配流程入手,研究伙伴系统的核心
调用链:get_page_from_freelist() ->buffered_rmqueue() -> __rmqueue()
get_page_from_freelist
[linux-2.6.24/mm/page_alloc.c]
struct page * fastcall __alloc_pages(gfp_t gfp_mask, unsigned int order,
        struct zonelist *zonelist)
{
    ......
restart:
    z = zonelist->zones;  /* the list of zones suitable for gfp_mask */
    page = get_page_from_freelist(gfp_mask|__GFP_HARDWALL, order,
                zonelist, ALLOC_WMARK_LOW|ALLOC_CPUSET);
    if (page)
        goto got_pg;
got_pg:
    return page;
}
get_page_from_freelist
static struct page * get_page_from_freelist(gfp_t gfp_mask, unsigned int order,
        struct zonelist *zonelist, int alloc_flags)
{
zonelist_scan:
    z = zonelist->zones;
    do {
        ... //忽略 NUMA, watermark 相关检测
        zone = *z;
        page = buffered_rmqueue(zonelist, zone, order, gfp_mask);
        if (page)
            break;
        ...
    } while (*(++z) != NULL);
    return page;
}
buffered_rmqueue
[mm/page_alloc.c]
static struct page *buffered_rmqueue(struct zonelist *zonelist,
            struct zone *zone, int order, gfp_t gfp_flags)
{
    ...
again:
    cpu  = get_cpu();
    if (likely(order == 0)) {
        ... //先尝试从本地CPU cache 中分配,失败在调用 rmqueue_bulk
    } else {
        spin_lock_irqsave(&zone->lock, flags);  //关中断
        page = __rmqueue(zone, order, migratetype);
        spin_unlock(&zone->lock);
    }
    ...
    if (prep_new_page(page, order, gfp_flags)) //设置page->flags
        goto again;
    return page;
}
对于单页的分配(order == 0),优先考虑从本地 CPU Cache 中获取。多个页的分配则调用 __rmqueue
__rmqueue 是伙伴系统的核心,我们单独开一小节分析
__rmqueue
__rmqueue 是进入伙伴系统的核心入口
static struct page *__rmqueue(struct zone *zone, unsigned int order,
						int migratetype)
{
	struct page *page;
	page = __rmqueue_smallest(zone, order, migratetype);
	if (unlikely(!page))
		page = __rmqueue_fallback(zone, order, migratetype);
	return page;
}
代码比较简洁
- __rmqueue_smallest: 在最适合的 zone 分配
 - __rmqueue_fallback: 合适的 zone 不满足分配请求,fallback到备用列表
 
__rmqueue_smallest
static struct page *__rmqueue_smallest(struct zone *zone, unsigned int order,
                        int migratetype)
{
    unsigned int current_order;
    struct free_area * area;
    struct page *page;
    /* Find a page of the appropriate size in the preferred list */
    for (current_order = order; current_order < MAX_ORDER; ++current_order) {
        area = &(zone->free_area[current_order]);
        if (list_empty(&area->free_list[migratetype]))   --- A
            continue;
        page = list_entry(area->free_list[migratetype].next, struct page, lru);  --- B
        list_del(&page->lru);
        rmv_page_order(page);  //page->flag 相关设置
        area->nr_free--;
        __mod_zone_page_state(zone, NR_FREE_PAGES, - (1UL << order)); //统计信息更新
        expand(zone, page, order, current_order, area, migratetype);   --- C
        return page;
    }
    return NULL;
}
代码会从低到高,遍历 free_area[] 直到找到合适的 page
- A: 通过判断链表为空,来确认该 order 下,有没有充足的页可供分配
 - B: 从原有的链表下取出第一个单元。并更新 free_area
 - C: 对取出的单元,可能对应 order 更高。需要考虑将更高 order 拆分,加入到低阶的链表。
 
expand
继续研究伙伴系统细节,如何拆分
static inline void expand(struct zone *zone, struct page *page,
	int low, int high, struct free_area *area,
	int migratetype)
{
	unsigned long size = 1 << high;
	while (high > low) {  --- A
		area--;  -- A //移动到更低阶的 order 链表
		high--;
		size >>= 1;   --- C // order 阶数就是两倍的关系
		list_add(&page[size].lru, &area->free_list[migratetype]);
		area->nr_free++;
		set_page_order(&page[size], high);
	}
}
代码很简介:
- 
A:若 order 相等 (high == low), 不用拆分合并,直接返回。进入循环,则是 high > low, 需要拆分
 - 
B: 找到合适的链表头。
- area--: 先移动到低阶的 order 链表, 传入的 area 是高阶
 
 - 
C: 拆分合并
- size/2: 因为 order 阶数就是两倍的关系。从高逐级到低,必然是不停除以 2 的过程
 - list_add(): 注意这里是 page[size], 意思是地址较低的page,继续执行该循环、拆分。地址较高的page,就加入到当前 order 链表中了
 - set_page_order(): 同样 page[size]。对于高地址page, 更新其page对应的flag。建立和伙伴系统的联系
 
 
页释放
概述
页的释放函数核心入口是__free_pages
__free_pages
__free_pages
fastcall void __free_pages(struct page *page, unsigned int order)
{
	if (put_page_testzero(page)) {
		if (order == 0)
			free_hot_page(page);
		else
			__free_pages_ok(page, order);
	}
}
和分配函数 buffered_rmqueue 相呼应
- 当释放的数量位1页时,调用 free_hot_page 释放到 per-cpu 的缓存中。这种策略被称之为惰性合并,用于防止大量可能白费时间的合并
 - 大于 1 页调用 __free_pages_ok() 释放
 
__free_pages_ok
核心是调用 free_one_page() 里面的 __free_one_page()
static void __free_pages_ok(struct page *page, unsigned int order)
{
    ...
    local_irq_save(flags);
    free_one_page(page_zone(page), page, order);
    local_irq_restore(flags);
    ...
}
static void free_one_page(struct zone *zone, struct page *page, int order)
{
	spin_lock(&zone->lock);
	zone_clear_flag(zone, ZONE_ALL_UNRECLAIMABLE);
	zone->pages_scanned = 0;
	__free_one_page(page, zone, order);
	spin_unlock(&zone->lock);
}
__free_one_page
static inline void __free_one_page(struct page *page,
        struct zone *zone, unsigned int order)
{
    unsigned long page_idx;
    int order_size = 1 << order;
    int migratetype = get_pageblock_migratetype(page);
    ...
    page_idx = page_to_pfn(page) & ((1 << MAX_ORDER) - 1);   --- A
    while (order < MAX_ORDER-1) {  --- B
        unsigned long combined_idx;
        struct page *buddy;
        buddy = __page_find_buddy(page, page_idx, order);   --- B1
        if (!page_is_buddy(page, buddy, order))
            break;      /* Move the buddy up one level. */
        list_del(&buddy->lru);  --- B2
        zone->free_area[order].nr_free--;
        rmv_page_order(buddy);
        combined_idx = __find_combined_index(page_idx, order);  --- B3
        page = page + (combined_idx - page_idx);
        page_idx = combined_idx;
        order++;
    }
    set_page_order(page, order);   --- C
    list_add(&page->lru,
        &zone->free_area[order].free_list[migratetype]);
    zone->free_area[order].nr_free++;
}
代码主要工作如下:
- 
A: 根据 page 寻找 pfn。对于
 - 
B: 释放时要考虑合并操作。及将连续低阶 order 组合,放到高阶链表中。
- B1: 调用 __page_find_buddy 寻找该 page 对应的伙伴。这里寻找仅仅是根据 page_idx。故找到的伙伴未必可以合并,需要调用 page_is_buddy 验明真身。
 - B2: 找到伙伴后,需要将伙伴从伙伴系统中移除
 - B3: 将伙伴和需要释放的 page 合并
 
 - 
C: 当找到合适 order 的area, 且合并工作已经完成,则将该单元加入的对应的链表,并设置属性。
 
Pfn, Page, Buddy
Page 和 Buddy 相关的辅助函数主要在 free_page 时使用用到
page_to_pfn
#if defined(CONFIG_FLATMEM)
#define __pfn_to_page(pfn)	(mem_map + ((pfn) - ARCH_PFN_OFFSET))
#define __page_to_pfn(page)	((unsigned long)((page) - mem_map) + \
				 ARCH_PFN_OFFSET)
#else if....
page 和 pfn 之间的转换和内存模型有很大的关系。
- 平坦内存模型:所有 page 会存放到数组 mem_map 中。pfn 即是相对于 mem_map 的偏移
 
__page_find_buddy()
/*
 * Locate the struct page for both the matching buddy in our
 * pair (buddy1) and the combined O(n+1) page they form (page).
 *
 * 1) Any buddy B1 will have an order O twin B2 which satisfies
 * the following equation:
 *     B2 = B1 ^ (1 << O)
 * For example, if the starting buddy (buddy2) is #8 its order
 * 1 buddy is #10:
 *     B2 = 8 ^ (1 << 1) = 8 ^ 2 = 10
 *
 * 2) Any buddy B will have an order O+1 parent P which
 * satisfies the following equation:
 *     P = B & ~(1 << O)
 *
 * Assumption: *_mem_map is contiguous at least up to MAX_ORDER
 */
static inline struct page *
__page_find_buddy(struct page *page, unsigned long page_idx, unsigned int order)
{
	unsigned long buddy_idx = page_idx ^ (1 << order);
	return page + (buddy_idx - page_idx);
}
这里的核心是异或运算: 0 ^ A = A; A ^ A = 0, 故异或可以使对应 bit 位翻转
- 
buddy 的 page_index 一定和该 page 相邻:前 或 后。且对应 index 一定是在 order 对应 bit 上 +1 或 -1
 - 
通过翻转 order 对应 bit 位。可以快速找到 buddy。例如
- 若 page_idx 对应 order 位为1, 其 buddy 对应的 order 为0。例如page_idx=0b101,order=1, buddy_idx=0b100
 - 若 page_idx 对应 order 位为0, 其 buddy 对应的 order 为1。例如page_idx=0b100,order=1, buddy_idx=0b101
 
 - 
从上述案例可以看到
- buddy 并不是任何 page_index 都行。buddy_idx 和 page_index 只能在对应 order 所在 bit 不一样。例如page_idx=0b101,order=1, buddy_idx不能是0b110。
 
 
page_is_buddy
这个函数比较简单。不做过多分析
static inline int page_is_buddy(struct page *page, struct page *buddy, int order)
{
	if (!pfn_valid_within(page_to_pfn(buddy)))
		return 0;
	if (page_zone_id(page) != page_zone_id(buddy))
		return 0;
	if (PageBuddy(buddy) && page_order(buddy) == order) {
		BUG_ON(page_count(buddy) != 0);
		return 1;
	}
	return 0;
}
__find_combined_index
__find_combined_index
对应 order 所在 bit 清零。即选择page_index 较低的页
static inline unsigned long __find_combined_index(unsigned long page_idx, unsigned int order)
{
	return (page_idx & ~(1 << order));
}
总结
buddy 并不是任何 page_index 都行。buddy_idx 和 page_index 只能在对应 order 所在 bit 不一样。例如page_idx=0b101,order=1, buddy_idx不能是0b110。
参考
《深入linux内核架构》