上次我们讲到page frame是物理内存的基本组成单位。那Kernel就必须要有一套机制来管理空闲的page frames。这一点不难理解。每个县长必须要把本县可用的劳动力登记在册,这样哪天皇帝要征兵了,你才不至于手忙脚乱。
这个问题看似简单,实则不然。因为这里面有一个外碎片的问题。
在物理内存中,连续的物理内存页有时是很重要的。例如在DMA操作中,由于大部分DMA处理器都没有分页机制,它们会直接访问物理内存地址,因此DMA 所用的缓冲区在物理地址空间必须连续;再例如,使用连续的物理内存页,可以保证内核页表中直接映射的页表项始终保持不变,同时连续的物理内存页可以作为一个大页(4MB)使用,这些都可以减少TLB miss,从而增加性能。
然而,系统运行一段时间后,可能会出现以下尴尬的局面:系统想申请一块连续的内存空间,同时系统中也有足够多的空闲内存页,可惜这些内存页都分散开了,导致申请失败。
你领着女朋友去坐地铁,车厢里可能会有好多空座,但却都不挨着,结果你还是没法和女朋友在一块坐下。这才是两个人的情况,假设你再带上小三小四,想同时坐在一块是不是更难了。。
为了解决外碎片的问题,Kernel采用了大名鼎鼎的buddy system算法。这个算法在内存管理中论地位,稳如泰山;论效率,快如闪电;论设计,却是极其简单优雅。
它把所有的空闲内存页按照0 ~ 10的order放在了MAX_ORDER (11)个链表中,每个链表分别包含大小为1,2,4,8,16,32,64,128,256,512和1024个连续内存页组成的页块。其中,每个页块的第一个内存页的物理地址是该块大小的整数倍。如果两个页块满足以下几个条件,那他俩就互为伙伴。
这两个页块的大小相同,记作b;
他们的物理地址是连续的;
第一个页块的第一个内存页的物理地址是 2*b*(2^12)的倍数。
注意,buddy system算法是递归的。当两个伙伴b相认后会合并成2b,然后这个2b再接着寻找自己的小伙伴。
说了这么多,让我们来看看它的具体实现吧。
1. 数据结构
在每个memory zone描述符里,有这样一个成员变量:
struct zone { ... struct free_area free_area[MAX_ORDER]; ... };
这个就是buddy system使用的主要数据结构了。MAX_ORDER一般为11,struct free_area 定义如下:
struct free_area { struct list_head free_list[MIGRATE_TYPES]; unsigned long nr_free; };
MIGRATE_TYPES是buddy system进一步减少碎片的机制,该机制从2.6.24才开始引入。我们这里暂且忽略,等以后专门讲一下这个机制。所以可以先把MIGRATE_TYPES当作1,即每个free_area结构体只包含一个链表。
我们以free_area数组中第k个元素为例,看看这个数据结构是怎样使用的。
第k个元素标识了所有大小为 2^k 的空闲页块。这些空闲页块以链表的形式连在一起,链表的头部即为第k个元素中的 free_list。实际上,这个链表中链接的是每一个空闲页块的起始内存页的描述符(struct page)。页块之间通过lru字段互连。
第k个元素还包含了一个nr_free字段,它指定了该链表中空闲页块的个数。
对于每一个大小为 2^k 的空闲页块的起始内存页,还会做两个标志:页描述符(struct page)的private字段存放该页块的order,也就是数字k;页描述符flags中的PG_buddy置位。
2. 分配一个页块
从buddy system中分配一个页块主要是由函数 __rmqueue_smallest 来完成的。
/* * Go through the free lists for the given migratetype and remove * the smallest available page from the freelists */ 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])) continue; page = list_entry(area->free_list[migratetype].next, struct page, lru); list_del(&page->lru); rmv_page_order(page); area->nr_free--; __mod_zone_page_state(zone, NR_FREE_PAGES, - (1UL << order)); expand(zone, page, order, current_order, area, migratetype); return page; } return NULL; }
该函数从参数指定的 order 开始寻找页块,如果当前current_order对应的链表为空,则继续向下一级寻找。一个 2^(k+1) 页的页块中肯定包含 2^k 页的页块,对吧。
如果找到了一个页块,则把它从current_order链表中摘下来,相应的递减nr_free的值,更新该zone的统计信息中空闲页的计数。
清除该页块的起始内存页中的两个标志:PG_buddy标志和private字段。
如果当前current_order比参数指定的 order 大,则从buddy system中摘下链表的这个页块就要被分成若干小的页块,除去要分配的这一块,其他的还得放回buddy system。这个工作是通过函数 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) { area--; high--; size >>= 1; VM_BUG_ON(bad_range(zone, &page[size])); list_add(&page[size].lru, &area->free_list[migratetype]); area->nr_free++; set_page_order(&page[size], high); } }
背后的逻辑不难理解:假设一个请求申请分配大小为 2^h 的页块,最后拿到了一个大小为 2^k 的空闲页块 (k > h),则该空闲页块前面 2^h 页会被分配出去,剩下的后面 (2^k - 2^h)页还需要再重新放回buddy system。
假设要分配一个2页的内存,现在取下了一个16页的页块。
16页的页块分成两个8页的页块,后面的8页放回order为3的链表中;
剩下的8页分成两个4页,后面的4页放回到order为2的链表中;
剩下的4页分成两个2页,后面的2页放回到order为1的链表中,前面的2页就是分配出去的页块了。
至此,分配完成,一个满足要求的页块不得不与它的小伙伴分离,离开buddy system。这个页块可能被用作page cache,可能被用作slab,可能被用来存放内核进程或用户进程的代码或数据。。。
从此,天涯沦落,漂泊江湖,每当夜雨潇潇漏尽残灯之时,总会想起当年,春风拂面,与小伙伴一起在盛开的桃李花下举杯畅饮,调笑风生。正所谓:桃李春风一杯酒,江湖夜雨十年灯。
3. 释放一个页块
要释放一个页块,首先要先解决两个问题,给定一个页块,其order为O,其起始内存页的索引为B1,那么:
它的buddy页块的起始内存页索引为: B2 = B1 ^ (1 << O) (这里是异或操作)
它和它的buddy合并,形成的新的页块的起始内存页索引为:P = B & ~(1 << O)
这两个操作由以下两个函数完成:
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); }
static inline unsigned long __find_combined_index(unsigned long page_idx, unsigned int order) { return (page_idx & ~(1 << order)); }
一旦找到了自己的buddy,如何进行身份验证:
一个页块的buddy不能在memory hole中;
一个页块的buddy必须是在buddy system中;
页块和其buddy有相同的order
页块和其buddy在相同的zone中。
验证工作是由函数 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; }
好了,万事俱备。释放一个页块是由函数 __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); __mod_zone_page_state(zone, NR_FREE_PAGES, order_size); while (order < MAX_ORDER-1) { unsigned long combined_idx; struct page *buddy; buddy = __page_find_buddy(page, page_idx, order); if (!page_is_buddy(page, buddy, order)) break; /* Move the buddy up one level. */ list_del(&buddy->lru); zone->free_area[order].nr_free--; rmv_page_order(buddy); combined_idx = __find_combined_index(page_idx, order); page = page + (combined_idx - page_idx); page_idx = combined_idx; order++; } set_page_order(page, order); list_add(&page->lru, &zone->free_area[order].free_list[migratetype]); zone->free_area[order].nr_free++; }
页块b一旦找到自己的buddy,便会与其合并成页块2b。这个2b再去寻找自己的buddy,合并成4b。。。直到某个页块发现 page_is_buddy 不满足,或是已经合并到了最大页块。
最后,合并完成之后的页块被放回到buddy system中与order对应的链表上。
当年分配页块时不得不分离的伙伴,最终又重新聚在了一起。
电影《一代宗师》中,宫二×××有句台词:世间所有的相遇,都是久别重逢。用这句话来概括Kernel中的buddy system算法,再合适不过了。