简介
内核在分配一组连续的页框时,必须解决一个著名的内存管理问题,也就是所谓的外碎片。本质上说,避免外碎片有两种方法:
- 利用分页单元把一组非连续的空闲页框映射到连续的线性地址区间。
- 开发一种适当的技术来记录现存的空闲连续页框块的情况,以尽量避免为满足小块的请求而分割大的空闲块。
Linux采用伙伴系统算法来解决外碎片问题,把所有的空闲框分组为11个块链表,每个块链表分别包含大小为1,2,4……1024个连续的页框,每个块的第一个页框的物理地址是该快大小的整数倍。
该算法的原理非常简单,假设请求的一个2^n大小的块,首先会检查链表中是否有空闲块,如果没有,算法会查找下一个更大的页块,即2^(n+1),如果存在这样的块,就把它分割成两等份,一半用作满足请求,另一半插入到2^n个页框的链表中;如果在2^(n+1)的链表中也没有空闲块,就继续找更大的块2^(n+m),将2^n满足请求,其余的2^n,2^(n+1),……,2^(n+m-1)插入对应的链表中。
以上的逆过程为页框块的释放过程,内核试图把大小为b的一对空闲伙伴块合并为一个大小为2b的单独块,满足以下条件的两个块称为伙伴:
- 两个块具有相同的大小b
- 它们的物理地址是连续的
- 第一个块的第一个页框的物理地址是2*b*2^12的倍数
源码分析(Linux2.6)
数据结构
Linux2.6为每个管理区使用不同的伙伴系统,每个伙伴系统使用的主要数据结构如下:- 每个管理区都关系到mem_map(存放内存所有的页框的数组)的子集,子集中的第一个元素和元素的个数分别由管理区描述符的zone_mem_map和size字段指定。
- 包含有11个元素、元素类型为free_area的一个数组,每个元素对应一种块大小,存放在管理区描述符的free_area字段中。
分配块(mm/page_alloc.c)
static struct page *__rmqueue(struct zone *zone, unsigned int order) { struct free_area * area; unsigned int current_order; struct page *page; /** * 从所请求的order开始,扫描每个可用块链表进行循环搜索。 */ for (current_order = order; current_order < MAX_ORDER; ++current_order) { area = zone->free_area + current_order; /** * 对应的空闲块链表为空,在更大的空闲块链表中进行循环搜索。 */ if (list_empty(&area->free_list)) continue; /** * 运行到此,说明有合适的空闲块。 */ page = list_entry(area->free_list.next, struct page, lru); /** * 首先在空闲块链表中删除第一个页框描述符。 */ list_del(&page->lru); rmv_page_order(page); area->nr_free--; /** * 并减少空闲管理区的空闲页数量。 */ zone->free_pages -= 1UL << order; /** * 如果2^order空闲块链表中没有合适的空闲块,那么就是从更大的空闲链表中分配的。 * 将剩余的空闲块分散到合适的链表中去。 */ return expand(zone, page, order, current_order, area); } /** * 直到循环结束都没有找到合适的空闲块,就返回NULL。 */ return NULL; }
expand函数如下:
static inline struct page * expand(struct zone *zone, struct page *page, int low, int high, struct free_area *area) { unsigned long size = 1 << high; while (high > low) { area--; high--; size >>= 1; BUG_ON(bad_range(zone, &page[size])); /** * 插入伙伴作为链表中的第一个元素 */ list_add(&page[size].lru, &area->free_list); area->nr_free++; set_page_order(&page[size], high); } return page; }
set_page_order函数如下,其实就是设置空闲块的第一个页框的标志,表明从page开始的连续2^order个页框为空闲块:
static inline void set_page_order(struct page *page, int order) { /** * 块大小为2^order */ page->private = order; __SetPagePrivate(page); }
释放块(mm/page_alloc.c)
static inline void __free_pages_bulk (struct page *page, struct page *base, struct zone *zone, unsigned int order) { /** * page_idx包含块中第一个页框的下标。 * 这是相对于管理区中的第一个页框而言的。 */ unsigned long page_idx; struct page *coalesced; /** * order_size用于增加管理区中空闲页框的计数器。 */ int order_size = 1 << order; if (unlikely(order)) destroy_compound_page(page, order); /** * base为管理区第一个页框,即zone_mem_map */ page_idx = page - base; /** * 大小为2^k的块,它的线性地址都是2^k * 2 ^ 12的整数倍。 * 相应的,它在管理区的偏移应该是2^k倍。 */ BUG_ON(page_idx & (order_size - 1)); BUG_ON(bad_range(zone, page)); /** * 增加管理区的空闲页数 */ zone->free_pages += order_size; /** * 最多循环10 - order次。每次都将一个块和它的伙伴进行合并。 * 每次从最小的块开始,向上合并。 */ while (order < MAX_ORDER-1) { struct free_area *area; struct page *buddy; int buddy_idx; /** * buddy_idx为要合并的块的伙伴相对管理区第一个页框的下标 *下面的异或操作转换page_idx的第order位的值,因此如果这个位原先是0,buddy_idx就等于page_idx + order_size; *相反,如果是1,buddy_idx就等于page_idx - order_size。之所以可以这样运算,是因为最上面伙伴块 *的第三个条件约束:第一个块的第一个页框的物理地址是2*b*2^12的倍数 */ buddy_idx = (page_idx ^ (1 << order)); /** * 通过伙伴的下标找到页描述符的地址。 */ buddy = base + buddy_idx; if (bad_range(zone, buddy)) break; /** * 判断伙伴块是否是大小为order的空闲页框的第一个页。 * 首先,伙伴的第一个页必须是空闲的(_count == -1) * 同时,必须属于动态内存(PG_reserved被清0,PG_reserved为1表示留给内核或者没有使用) * 最后,其private字段必须是order */ if (!page_is_buddy(buddy, order)) break; /** * 运行到这里,说明伙伴块可以与当前块合并。 */ /* Move the buddy up one level. */ /** * 伙伴将被合并,将它从现有链表中取下。 */ list_del(&buddy->lru); area = zone->free_area + order; area->nr_free--; rmv_page_order(buddy); page_idx &= buddy_idx; /** * 将合并了的块再与它的伙伴进行合并。 */ order++; } /** * 伙伴不能与当前块合并。 * 将块插入适当的链表,并以块大小的order更新第一个页框的private字段。 */ coalesced = base + page_idx; set_page_order(coalesced, order); list_add(&coalesced->lru, &zone->free_area[order].free_list); zone->free_area[order].nr_free++; }