简介
内核在分配一组连续的页框时,必须解决一个著名的内存管理问题,也就是所谓的外碎片。本质上说,避免外碎片有两种方法:
- 利用分页单元把一组非连续的空闲页框映射到连续的线性地址区间。
- 开发一种适当的技术来记录现存的空闲连续页框块的情况,以尽量避免为满足小块的请求而分割大的空闲块。
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++;
}