Linux中伙伴系统页面回收free_pages_bulk和分配函数__rmqueue的实现
伙伴系统核心设计思想
分层管理架构
- 按
order(2的幂次)分层管理内存块 - 每个order维护独立空闲链表和位图
- 支持从4KB到最大块的高效分配
伙伴关系定义
- 相邻且大小相同的块互为伙伴
- 伙伴块可以合并成更大的块
- 通过位运算快速定位伙伴:
page_idx ^ (1 << order)
位图精妙设计
index = page_idx >> (1 + order)
- 每个位对应两个相邻块的合并状态
- 位值=0:两个块状态相同(都空闲或都分配)
- 位值=1:一个空闲一个分配(不能合并)
- 通过
1 + order位移确保伙伴共享同一位(area->map)
内存分配机制(__rmqueue)
最佳适配策略
- 从请求order开始向上搜索第一个可用块
- 避免小order占用大块造成的碎片
拆分机制(expand)
while (high > low) {// 将后半块加入低order空闲链表list_add(&page[size].lru, &area->free_list);
}
- 大块拆分成需要大小 + 剩余小块
- 剩余部分返回相应order空闲链表
- 保持内存池完整性
内存释放机制(__free_pages_bulk)
合并算法
while (order < MAX_ORDER-1) {if (!__test_and_change_bit(index, area->map))// 原来为0,这里必定表示两个相邻块都是分配的// 所以回收当前块以后变成一个空闲,一个已分配,不能合并break; // 合并并升级到下一order
}
- 原子检查伙伴位图
- 逐级向上合并直到遇到不能合并的伙伴,即一个空闲,一个已分配
- 有效减少外部碎片
总结
Linux伙伴系统的精妙之处在于:
- 数学完备性:基于2的幂次的数学特性确保算法正确性
- 空间效率:用最少位图空间跟踪合并可能性
- 时间效率:O(1)的分配和释放平均时间复杂度
- 并发安全:细粒度锁+原子操作支持多处理器
- 自愈能力:通过合并机制自动减少内存碎片
批量释放物理内存页面到指定的内存区域free_pages_bulk
static int
free_pages_bulk(struct zone *zone, int count,struct list_head *list, unsigned int order)
{unsigned long flags;struct free_area *area;struct page *base, *page = NULL;int ret = 0;base = zone->zone_mem_map;area = zone->free_area + order;spin_lock_irqsave(&zone->lock, flags);zone->all_unreclaimable = 0;zone->pages_scanned = 0;while (!list_empty(list) && count--) {page = list_entry(list->prev, struct page, lru);/* have to delete it as __free_pages_bulk list manipulates */list_del(&page->lru);__free_pages_bulk(page, base, zone, area, order);ret++;}spin_unlock_irqrestore(&zone->lock, flags);return ret;
}
函数功能概述
free_pages_bulk 函数用于批量释放物理内存页面到指定的内存区域(zone)的空闲列表中,是内核页面分配器的核心组成部分
代码逐段详解
变量声明和初始化
static int
free_pages_bulk(struct zone *zone, int count,struct list_head *list, unsigned int order)
{unsigned long flags;struct free_area *area;struct page *base, *page = NULL;int ret = 0;
- zone: 目标内存区域,页面将被释放到这里
- count: 要释放的最大页面块数量
- list: 包含待释放页面的链表
- order: 页面块的数量(2^order 个页面)
- flags: 保存中断状态的变量
- area: 指向对应order的空闲区域描述符
- base: 内存区域的基础页面指针
- page: 当前正在处理的页面指针
- ret: 返回值,记录实际释放的页面块数量
基础设置
base = zone->zone_mem_map;area = zone->free_area + order;
base = zone->zone_mem_map: 获取该内存区域的起始页面描述符数组area = zone->free_area + order: 计算对应order的空闲区域地址free_area是不同order空闲链表的数组- 通过指针运算找到特定order的空闲区域
临界区保护
spin_lock_irqsave(&zone->lock, flags);zone->all_unreclaimable = 0;zone->pages_scanned = 0;
spin_lock_irqsave(&zone->lock, flags):- 获取zone的自旋锁,防止并发访问
- 保存当前中断状态到flags,并禁用本地CPU中断
zone->all_unreclaimable = 0: 标记该zone为可回收状态zone->pages_scanned = 0: 重置页面扫描计数器,为后续内存回收做准备
核心释放循环
while (!list_empty(list) && count--) {page = list_entry(list->prev, struct page, lru);/* have to delete it as __free_pages_bulk list manipulates */list_del(&page->lru);__free_pages_bulk(page, base, zone, area, order);ret++;}
- 循环条件: 链表不为空且还有释放配额(count–)
page = list_entry(list->prev, struct page, lru):- 从链表尾部获取页面
list_entry宏通过成员指针获取包含它的结构体指针
list_del(&page->lru): 将页面从输入链表中移除__free_pages_bulk(page, base, zone, area, order):- 核心释放函数,处理页面合并等复杂逻辑
ret++: 统计成功释放的页面块数量
清理和返回
spin_unlock_irqrestore(&zone->lock, flags);return ret;
}
spin_unlock_irqrestore(&zone->lock, flags):- 释放zone锁
- 恢复之前的中断状态
return ret: 返回实际释放的页面块数量
将物理页面释放到空闲链表__free_pages_bulk
static void destroy_compound_page(struct page *page, unsigned long order)
{int i;int nr_pages = 1 << order;if (!PageCompound(page))return;if (page[1].index != order)bad_page(__FUNCTION__, page);for (i = 0; i < nr_pages; i++) {struct page *p = page + i;if (!PageCompound(p))bad_page(__FUNCTION__, page);if (p->private != (unsigned long)page)bad_page(__FUNCTION__, page);ClearPageCompound(p);}
}
static inline void __free_pages_bulk (struct page *page, struct page *base,struct zone *zone, struct free_area *area, unsigned int order)
{unsigned long page_idx, index, mask;if (order)destroy_compound_page(page, order);mask = (~0UL) << order;page_idx = page - base;if (page_idx & ~mask)BUG();index = page_idx >> (1 + order);zone->free_pages += 1 << order;while (order < MAX_ORDER-1) {struct page *buddy1, *buddy2;BUG_ON(area >= zone->free_area + MAX_ORDER);if (!__test_and_change_bit(index, area->map))/** the buddy page is still allocated.*/break;/* Move the buddy up one level. */buddy1 = base + (page_idx ^ (1 << order));buddy2 = base + page_idx;BUG_ON(bad_range(zone, buddy1));BUG_ON(bad_range(zone, buddy2));list_del(&buddy1->lru);mask <<= 1;order++;area++;index >>= 1;page_idx &= mask;}list_add(&(base + page_idx)->lru, &area->free_list);
}
函数功能概述
__free_pages_bulk 是伙伴系统的核心释放函数,负责将物理页面释放到空闲链表,并尝试与相邻的空闲块合并形成更大的连续块
代码逐段详解
复合页面销毁检查
static inline void __free_pages_bulk (struct page *page, struct page *base,struct zone *zone, struct free_area *area, unsigned int order)
{unsigned long page_idx, index, mask;if (order)destroy_compound_page(page, order);
- 参数:
page: 要释放的页面起始地址base: zone的起始页面地址zone: 所属内存区域area: 对应的free_areaorder: 页面块大小级别
if (order) destroy_compound_page(page, order): 如果order>0(复合页面),先销毁复合页面结构
destroy_compound_page 函数详解
static void destroy_compound_page(struct page *page, unsigned long order)
{int i;int nr_pages = 1 << order;if (!PageCompound(page))return;if (page[1].index != order)bad_page(__FUNCTION__, page);for (i = 0; i < nr_pages; i++) {struct page *p = page + i;if (!PageCompound(p))bad_page(__FUNCTION__, page);if (p->private != (unsigned long)page)bad_page(__FUNCTION__, page);ClearPageCompound(p);}
}
nr_pages = 1 << order: 计算总页面数(2^order)if (!PageCompound(page)) return: 检查页面标记为复合页面page[1].index != order: 验证第二个页面中存储的order值- 循环检查每个子页面:
if (!PageCompound(p)): 每个子页面都应有复合标记p->private != (unsigned long)page: private字段应指向首页面ClearPageCompound(p): 清除复合页面标记
页面索引计算和验证
mask = (~0UL) << order;page_idx = page - base;if (page_idx & ~mask)BUG();index = page_idx >> (1 + order);
mask = (~0UL) << order: 创建掩码,低位order位为0page_idx = page - base: 计算页面在zone中的索引if (page_idx & ~mask) BUG(): 验证页面对齐- 索引必须能被2^order整除
- 如果希望批量释放页面的话,需要确保首页地址是对应order阶层的页面
index = page_idx >> (1 + order): 计算在位图中的索引- 这里加1获取的是上一阶层的索引,用于后面判断当前层级的相邻块的使用状态
更新zone空闲页面计数
zone->free_pages += 1 << order;
- 增加zone的空闲页面计数器,增加数量为 2^order 个页面
伙伴合并核心循环
while (order < MAX_ORDER-1) {struct page *buddy1, *buddy2;BUG_ON(area >= zone->free_area + MAX_ORDER);if (!__test_and_change_bit(index, area->map))/** the buddy page is still allocated.*/break;
-
循环条件: 当前order小于最大order-1
-
BUG_ON(area >= zone->free_area + MAX_ORDER): 确保area指针有效 -
if (!__test_and_change_bit(index, area->map)) break:__test_and_change_bit原子地测试并翻转位图位- 位图代表的意思如下
块大小: 2页 = 2^1 页 相邻块数: 2个块 位图粒度: 4页 = 2^(1+1) 页页面索引: 0 1 | 2 3 | 4 5 | 6 7 | ... 位图索引: 0 1 ...计算: index >> (1 + 1) = index >> 2 index=0: 0 >> 2 = 0 index=2: 2 >> 2 = 0 (与块0共享同一位) index=4: 4 >> 2 = 1 index=6: 6 >> 2 = 1 (与块4共享同一位) 位为0表示两个相邻块都是空闲或者都是已分配 位为1表示两个相邻块一个空闲一个已分配- 这里index对应的位如果原来是0,只可能是下面两个相邻块都是已分配,不可能都是空闲,不然怎么会回收页面
- 所以当这个块回收以后就变成一个空闲,一个已分配,那么index对应的位肯定要变1,符合翻转后的逻辑
- 这里index对应的位如果原来是1,说明下面两个相邻块一个空闲,一个已分配
- 那么这个块回收以后就变成都是空闲了,可以继续下面的合并块操作
伙伴块查找和验证
/* Move the buddy up one level. */buddy1 = base + (page_idx ^ (1 << order));buddy2 = base + page_idx;BUG_ON(bad_range(zone, buddy1));BUG_ON(bad_range(zone, buddy2));list_del(&buddy1->lru);
buddy1 = base + (page_idx ^ (1 << order)): 计算伙伴块的地址- 使用XOR运算切换第order位来找到伙伴
buddy2 = base + page_idx: 当前块的地址BUG_ON(bad_range(zone, buddy1/buddy2): 验证伙伴块在有效范围内list_del(&buddy1->lru): 从当前order的空闲链表中删除伙伴块
升级到下一级
mask <<= 1;order++;area++;index >>= 1;page_idx &= mask;}
mask <<= 1: 掩码左移一位,扩大对齐范围order++: 提升到更大的order级别area++: 移动到下一级的free_areaindex >>= 1: 位图索引减半(因为块大小加倍)page_idx &= mask: 对齐页面索引到新的order级别
最终插入空闲链表
list_add(&(base + page_idx)->lru, &area->free_list);
}
- 将合并后的块(或原始块)插入对应order的空闲链表
测试位并翻转__test_and_change_bit
static inline int __test_and_change_bit(int nr, volatile unsigned long *addr)
{int oldbit;__asm__ __volatile__("btcl %2,%1\n\tsbbl %0,%0":"=r" (oldbit),"=m" (ADDR):"Ir" (nr) : "memory");return oldbit;
}
函数功能概述
__test_and_change_bit 原子地测试指定位的当前值,然后翻转该位(0变1,1变0),并返回翻转前的旧值
代码逐段详解
函数声明和变量
static inline int __test_and_change_bit(int nr, volatile unsigned long *addr)
{int oldbit;
static inline: 内联函数,避免函数调用开销int nr: 要操作的位号(0-31等)volatile unsigned long *addr: 指向位图的内存地址int oldbit: 存储操作前的位值
汇编指令
"btcl %2,%1\n\tsbbl %0,%0"
第一条:btcl %2,%1
btc: Bit Test and Complement(位测试并取反)l: Long操作(32位)%2: 第三个操作数(nr - 位号)%1: 第二个操作数(*addr - 位图地址)- 功能: 测试第nr位的值(存入CF标志位),然后翻转该位
第二条:sbbl %0,%0
sbb: Subtract with Borrow(带借位减法)l: Long操作%0,%0: 对第一个操作数(oldbit)自身相减- 功能:
oldbit = oldbit - oldbit - CF- 等价于
oldbit = -CF - 如果CF=1,结果就是-1
- 如果CF=0,结果就是0
- 等价于
输出操作数
:"=r" (oldbit),"=m" (ADDR)
"=r" (oldbit): 输出操作数0=r表示输出到通用寄存器- 结果存储到
oldbit变量
"=m" (ADDR): 输出操作数1=m表示输出到内存ADDR是位图地址
输入操作数
:"Ir" (nr) : "memory");
"Ir" (nr): 输入操作数2I表示立即数r表示寄存器nr是位号参数
"memory": 编译器内存屏障- 告诉编译器内存可能被修改,不要缓存寄存器中的值
return oldbit;
}
- 返回操作前的位值(1或0)
分配页并拆分__rmqueue/expand
static struct page *__rmqueue(struct zone *zone, unsigned int order)
{struct free_area * area;unsigned int current_order;struct page *page;unsigned int index;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);index = page - zone->zone_mem_map;if (current_order != MAX_ORDER-1)MARK_USED(index, current_order, area);zone->free_pages -= 1UL << order;return expand(zone, page, index, order, current_order, area);}return NULL;
}
static inline struct page *
expand(struct zone *zone, struct page *page,unsigned long index, 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);MARK_USED(index + size, high, area);}return page;
}
#define MARK_USED(index, order, area) \__change_bit((index) >> (1+(order)), (area)->map)
static inline void __change_bit(int nr, volatile unsigned long * addr)
{__asm__ __volatile__("btcl %1,%0":"=m" (ADDR):"Ir" (nr));
}
函数功能概述
__rmqueue 是伙伴系统的核心分配函数,负责从指定order开始查找可用的内存块,如果找到的块比请求的大,就通过expand将其拆分。
__rmqueue 函数详解
变量声明和初始化
static struct page *__rmqueue(struct zone *zone, unsigned int order)
{struct free_area * area;unsigned int current_order;struct page *page;unsigned int index;
- zone: 目标内存区域
- order: 请求的内存块大小级别
- area: 当前搜索的区域阶层指针
- current_order: 当前搜索的order级别
- page: 找到的内存页面
- index: 页面在zone中的索引
搜索循环
for (current_order = order; current_order < MAX_ORDER; ++current_order) {area = zone->free_area + current_order;if (list_empty(&area->free_list))continue;
- 从请求的order开始,向更大块搜索
area = zone->free_area + current_order: 获取对应order的free_arealist_empty(&area->free_list): 检查该order是否有空闲块- 如果没有,继续搜索更大的块
找到可用块
page = list_entry(area->free_list.next, struct page, lru);list_del(&page->lru);index = page - zone->zone_mem_map;
list_entry(area->free_list.next, struct page, lru): 从链表头部获取页面list_del(&page->lru): 从空闲链表中删除该页面index = page - zone->zone_mem_map: 计算页面在zone中的索引
更新位图和计数器
if (current_order != MAX_ORDER-1)MARK_USED(index, current_order, area);zone->free_pages -= 1UL << order;return expand(zone, page, index, order, current_order, area);}return NULL;
}
MARK_USED(index, current_order, area): 标记该块在位图中为已使用zone->free_pages -= 1UL << order: 减少zone的空闲页面计数expand(zone, page, index, order, current_order, area): 如果找到的块比请求的大,进行拆分
MARK_USED 和 __change_bit 详解
MARK_USED 宏
#define MARK_USED(index, order, area) \__change_bit((index) >> (1+(order)), (area)->map)
- 计算位图位置:
(index) >> (1+(order))(area)->map的位表示两个相邻块是否都分配或者空闲- 这里在order层级分配了一个块出去,那么就需要到上一层级的对应位进行翻转
- 如果之前为1,表示一个块分配,一个块空闲
- 现在分配了一个块,说明两个相邻块都分配了,就可以翻转为0了,反之亦然
- 调用
__change_bit翻转指定位
__change_bit 函数
static inline void __change_bit(int nr, volatile unsigned long * addr)
{__asm__ __volatile__("btcl %1,%0":"=m" (ADDR):"Ir" (nr));
}
btcl %1,%0: Bit Test and Complement Long- 测试并翻转指定位
%1是位号(nr),%0是位图地址(addr)
- 原子操作,不需要返回值
expand 函数详解
函数参数和初始化
static inline struct page *
expand(struct zone *zone, struct page *page,unsigned long index, int low, int high, struct free_area *area)
{unsigned long size = 1 << high;
- low: 请求的order
- high: 找到的块的order
- size: 当前块的大小(页面数)
拆分循环
while (high > low) {area--;high--;size >>= 1;
high > low: 当找到的块比需要的大时继续拆分area--: 移动到更低order的free_areahigh--: 降低order级别size >>= 1: 块大小减半
处理后半部分块
BUG_ON(bad_range(zone, &page[size]));list_add(&page[size].lru, &area->free_list);MARK_USED(index + size, high, area);}return page;
}
BUG_ON(bad_range(zone, &page[size])): 验证块范围有效性list_add(&page[size].lru, &area->free_list): 将后半块加入空闲链表MARK_USED(index + size, high, area): 对上一层级的指定位进行翻转- 返回前半块给调用者
关键设计要点
- 最佳适配: 从请求大小开始搜索,找到第一个可用的块
- 块拆分: 将大块拆分成需要的大小,剩余部分放回空闲链表
- 位图管理: 精确跟踪每个块的使用状态
