对于内存释放函数也可以归纳到一个主要的函数(__free_pages),只是用不同的参数调用而已,前者是通过page,后者是通过虚拟地址addr,其定义如下
extern void __free_pages(struct page *page, unsigned int order);
extern void free_pages(unsigned long addr, unsigned int order);
对于free_pages和__free_pages之间的关系,首先需要将虚拟地址转换成指向struct page的指针
void free_pages(unsigned long addr, unsigned int order)
{
if (addr != 0) {
VM_BUG_ON(!virt_addr_valid((void *)addr));
__free_pages(virt_to_page((void *)addr), order);
}
}
对于__free_pages是一个基础函数,用于实现内核API中所有涉及到内存释放的接口函数,其代码流程如下:
void __free_pages(struct page *page, unsigned int order)
{
if (put_page_testzero(page)) {//检查页框是否还有进程在使用,就是检查_count变量的值是否为0
if (order == 0) //如果是1个页框,则放回每CPU高速缓存中
free_hot_cold_page(page, false);
else //如果是多个页框,则放回伙伴系统
__free_pages_ok(page, order);
}
}
- 首先,调用put_page_testzero来查看该页是否还有其他引用(struct page结构中的_count),如果没有被引用,就走到对应的页面释放流程中,如果还被引用,就啥也不做
- 由申请页面的时候,会区分是申请的单页还是多页,那么释放的时候,就也做同样的处理。会判断所需释放的内存是单页还是较大的内存块。
- 如果释放的是单页,则不还给伙伴系统,还是放回per-cpu缓存中。
- 如果释放的时多页,就直接调用__free_pages_ok归还到伙伴系统中
1. free_hot_cold_page流程
void free_hot_cold_page(struct page *page, bool cold)
{
struct zone *zone = page_zone(page); //用于根据page得到所在zone
struct per_cpu_pages *pcp;
unsigned long flags;
unsigned long pfn = page_to_pfn(page); //根据给出页地址求出对应的页帧号
int migratetype;
//释放前pcp的准备工作,检查释放满足释放条件
if (!free_pcp_prepare(page))
return;
//获取页框所在pageblock的页框类型
migratetype = get_pfnblock_migratetype(page, pfn);
//设置页框类型为pageblock的页框类型,因为在页框使用过程中,这段pageblock可以移动到了其他类型
set_pcppage_migratetype(page, migratetype);
local_irq_save(flags);
__count_vm_event(PGFREE);
//如果不是高速缓存类型,就放回到伙伴系统中
if (migratetype >= MIGRATE_PCPTYPES) {
if (unlikely(is_migrate_isolate(migratetype))) {
free_one_page(zone, page, pfn, 0, migratetype);
goto out;
}
migratetype = MIGRATE_MOVABLE;
}
//获取当前cpu页列表的zone->pageset->pcp
pcp = &this_cpu_ptr(zone->pageset)->pcp;
if (!cold) //hot page加入list 头部,便于优先使用
list_add(&page->lru, &pcp->lists[migratetype]);
else //对于cold page,将其加入list尾部
list_add_tail(&page->lru, &pcp->lists[migratetype]);
pcp->count++;
//当前CPU高速缓存中页框数量高于最大值,将pcp->batch数量的页框放回伙伴系统
if (pcp->count >= pcp->high) {
unsigned long batch = READ_ONCE(pcp->batch);
free_pcppages_bulk(zone, batch, pcp);
pcp->count -= batch;
}
out:
local_irq_restore(flags);
}
对于冷页和热页,主要表现是当一个页被释放时,默认设置为热页的话,因为该页可能有些地址的数据还是处于映射到CPU cache,当该CPU上有进程申请单个页框时,优先把这些热页分配出去,这样能提高cache的命中率,提高效率,则软件上的实现方式也比较简单,热页,则直接加入到CPU页框高速缓存链表的链表头,冷页则直接加入到链表尾。
2. __free_pages_ok流程
再看看连续页框的释放,连续页框释放主要是__free_pages_ok()函数
static void __free_pages_ok(struct page *page, unsigned int order)
{
unsigned long flags;
int migratetype;
unsigned long pfn = page_to_pfn(page); //根据给出页地址求出对应的页帧号
if (!free_pages_prepare(page, order, true)) //释放前pcp的准备工作,检查释放满足释放条件
return;
migratetype = get_pfnblock_migratetype(page, pfn);//获取页框所在pageblock的页框类型
local_irq_save(flags);
__count_vm_events(PGFREE, 1 << order); //统计当前CPU一共释放的页框数
free_one_page(page_zone(page), page, pfn, order, migratetype); //释放函数
local_irq_restore(flags);
}
对于该接口无论是释放单页还是连续页,在释放时,会获取该页所载的pageblock的类型,然后把此页设置成pageblock一致的类型,因为有一种情况,比如一个pageblock为MIGRATE_MOVABLE类型,并且有部分页已经被使用(这些正在被使用的页都为MIGRATE_MOVABLE),然后MIGRATE_RECLAIMABLE类型的页不足,需要从MIGRATE_MOVABLE这里获取这个pageblock到MIGRATE_RECLAIMABLE类型中,这个pageblock的类型就被修改成了MIGRATE_RECLAIMABLE,这样就造成了正在使用的页的类型会与pageblock的类型不一致。最后调用free_one_page函数,其定义如下
static void free_one_page(struct zone *zone,
struct page *page, unsigned long pfn,
unsigned int order,
int migratetype)
{
unsigned long nr_scanned;
spin_lock(&zone->lock);
nr_scanned = node_page_state(zone->zone_pgdat, NR_PAGES_SCANNED);//数据更新
if (nr_scanned)
__mod_node_page_state(zone->zone_pgdat, NR_PAGES_SCANNED, -nr_scanned);
//内存隔离
if (unlikely(has_isolate_pageblock(zone) ||
is_migrate_isolate(migratetype))) {
migratetype = get_pfnblock_migratetype(page, pfn);
}
__free_one_page(page, pfn, zone, order, migratetype);//释放page开始的order次方个页框到伙伴系统,这些页框的类型时migratetype
spin_unlock(&zone->lock);
}
整个释放过程的核心函数使__free_one_page,依据申请的算法,那么释放就涉及到对页面能够进行合并的。相关的内存区被添加到伙伴系统中适当的free_area列表中,在释放时,该函数将其合并为一个连续的内存区,放置到高一阶的free_are列表中。如果还能合并一个进一步的伙伴对,那么也进行合并,转移到更高阶的列表中。该过程会一致重复下去,直至所有可能的伙伴对都已经合并,并将改变尽可能向上传播。
static inline void __free_one_page(struct page *page,
unsigned long pfn,
struct zone *zone, unsigned int order,
int migratetype)
{
unsigned long page_idx;
unsigned long combined_idx;
unsigned long uninitialized_var(buddy_idx);
struct page *buddy;
unsigned int max_order;
//则最大的order应该为MAX_ORDER与pageblock_order+1中最小那个
max_order = min_t(unsigned int, MAX_ORDER, pageblock_order + 1);
//检查
VM_BUG_ON(!zone_is_initialized(zone));
VM_BUG_ON_PAGE(page->flags & PAGE_FLAGS_CHECK_AT_PREP, page);
//
VM_BUG_ON(migratetype == -1);
if (likely(!is_migrate_isolate(migratetype)))
__mod_zone_freepage_state(zone, 1 << order, migratetype);
//将释放的页面转换成page idx
page_idx = pfn & ((1 << MAX_ORDER) - 1);
//如果被释放的页不是所释放阶的第一个页,则说明参数有误
VM_BUG_ON_PAGE(page_idx & ((1 << order) - 1), page);
VM_BUG_ON_PAGE(bad_range(zone, page), page);//检查页面是否处于zone之中
continue_merging://释放页以后,当前页面可能与前后的空闲页组成更大的空闲页面,直到放到最大阶的伙伴系统中
while (order < max_order - 1) {
buddy_idx = __find_buddy_index(page_idx, order);//找到与当前页属于同一个阶的伙伴页面索引
buddy = page + (buddy_idx - page_idx);//计算伙伴页面的页地址
//检查buddy是否描述了大小为order的空闲页框块的第一个页
if (!page_is_buddy(page, buddy, order))
goto done_merging;
//页面调试功能,页面被释放时是整个的从内核地址空间中移除的。该选项显著地降低了速度,但它也能迅速指出特定类型的内存崩溃错误,需要配置CONFIG_DEBUG_PAGEALLOC
if (page_is_guard(buddy)) {
clear_page_guard(zone, buddy, order, migratetype);
} else {
list_del(&buddy->lru);//如果能够合并,则将伙伴页从伙伴系统中摘除
zone->free_area[order].nr_free--;//同时减少当前阶中的空闲页计数
rmv_page_order(buddy);//清除伙伴页的伙伴标志,因为该页会被合并
}
combined_idx = buddy_idx & page_idx;//将当前页与伙伴页合并后,新的页面起始地址
page = page + (combined_idx - page_idx);
page_idx = combined_idx;
order++;
}
if (max_order < MAX_ORDER) {
if (unlikely(has_isolate_pageblock(zone))) {
int buddy_mt;
buddy_idx = __find_buddy_index(page_idx, order);
buddy = page + (buddy_idx - page_idx);
buddy_mt = get_pageblock_migratetype(buddy);
if (migratetype != buddy_mt
&& (is_migrate_isolate(migratetype) ||
is_migrate_isolate(buddy_mt)))
goto done_merging;
}
max_order++;
goto continue_merging;
}
done_merging:
set_page_order(page, order);//设置伙伴页中第一个空闲页的阶
/**
* 如果当前合并后的页不是最大阶的,那么将当前空闲页放到伙伴链表的最后。
* 这样,它将不会被很快被分配,更有可能与更高阶页面进行合并。
*/
if ((order < MAX_ORDER-2) && pfn_valid_within(page_to_pfn(buddy))) {
struct page *higher_page, *higher_buddy;
combined_idx = buddy_idx & page_idx;//计算更高阶的页面索引及页面地址
higher_page = page + (combined_idx - page_idx);
buddy_idx = __find_buddy_index(combined_idx, order + 1);
higher_buddy = higher_page + (buddy_idx - combined_idx);
if (page_is_buddy(higher_page, higher_buddy, order + 1)) {//更高阶的页面是空闲的,属于伙伴系统
//将当前页面合并到空闲链表的最后,尽量避免将它分配出去
list_add_tail(&page->lru,
&zone->free_area[order].free_list[migratetype]);
goto out;
}
}
//更高阶的页面已经分配出去,那么将当前页面放到链表前面
list_add(&page->lru, &zone->free_area[order].free_list[migratetype]);
out:
zone->free_area[order].nr_free++;//将当前阶的空闲计数加
}
但内核如何知道一个伙伴对的两个部分都位于空闲页的列表中呢?为将内存块放回伙伴系统,内核必须计算潜在的伙伴地址,以及在有可能合并的情况下合并后内存块的索引。内核提供辅助函数用于计算
static inline unsigned long
__find_buddy_index(unsigned long page_idx, unsigned int order)
{
return page_idx ^ (1 << order);
}
对于__free_one_page试图释放一个order的一个内存块,有可能不只是当前内存块与能够与其合并的伙伴直接合并,而且高阶的伙伴也可以合并,因此内核需要找到可能的最大分配阶。假设释放一个0阶内存块,即一页,该页的索引值为10,假设页10是合并两个3阶伙伴最后形成一个4阶的内存块,计算如下图所示
ordrr | page_idx | buddy_index-page-index | __find_combined_index |
---|---|---|---|
0 | 10 | 1 | 10 |
1 | 10 | -2 | 8 |
2 | 8 | 4 | 8 |
3 | 8 | -8 | 0 |
第一遍寻找到页10的伙伴页11,由于需要的不是伙伴的页号,而是指向对应page的实例指针,buddy_index-page_idx就派上用场了,该值表示当前页与伙伴系统的差值,page指针加上该值,即可得到伙伴page的实例。
然后通过page_is_buddy需要改指针来检查伙伴系统是否是空闲,如果恰好是空闲,那么久可以合并这两个伙伴。这时候就需要将页11从伙伴系统中移除,重新合并形成一个更大的内存块,而rmv_page_order负责清楚PG_buddy标志和private数据。然后下一遍循环工作类似,但这一次order=1,也就是说,内核试图合并两个2页的伙伴,得到一个4页的内存块,其合并图如下图所示
到此,伙伴系统的页释放流程也梳理完毕,其实现过程也比较简单。
Java 面试宝典是大明哥全力打造的 Java 精品面试题,它是一份靠谱、强大、详细、经典的 Java 后端面试宝典。它不仅仅只是一道道面试题,而是一套完整的 Java 知识体系,一套你 Java 知识点的扫盲贴。
它的内容包括:
- 大厂真题:Java 面试宝典里面的题目都是最近几年的高频的大厂面试真题。
- 原创内容:Java 面试宝典内容全部都是大明哥原创,内容全面且通俗易懂,回答部分可以直接作为面试回答内容。
- 持续更新:一次购买,永久有效。大明哥会持续更新 3+ 年,累计更新 1000+,宝典会不断迭代更新,保证最新、最全面。
- 覆盖全面:本宝典累计更新 1000+,从 Java 入门到 Java 架构的高频面试题,实现 360° 全覆盖。
- 不止面试:内容包含面试题解析、内容详解、知识扩展,它不仅仅只是一份面试题,更是一套完整的 Java 知识体系。
- 宝典详情:https://www.yuque.com/chenssy/sike-java/xvlo920axlp7sf4k
- 宝典总览:https://www.yuque.com/chenssy/sike-java/yogsehzntzgp4ly1
- 宝典进展:https://www.yuque.com/chenssy/sike-java/en9ned7loo47z5aw
目前 Java 面试宝典累计更新 400+ 道,总字数 42w+。大明哥还在持续更新中,下图是大明哥在 2024-12 月份的更新情况:
想了解详情的小伙伴,扫描下面二维码加大明哥微信【daming091】咨询
同时,大明哥也整理一套目前市面最常见的热点面试题。微信搜[大明哥聊 Java]或扫描下方二维码关注大明哥的原创公众号[大明哥聊 Java] ,回复【面试题】 即可免费领取。