《Linux6.5源码分析:内存管理系列文章》
本系列文章将对内存管理相关知识进行梳理与源码分析,重点放在linux源码分析上,并结合eBPF程序对内核中内存管理机制进行数据实时拿取与分析。
在进行正式介绍之前,有必要对文章引用进行提前说明。本系列文章参考了大量的博客、文章以及书籍:
-
《深入理解Linux内核》
-
《Linux操作系统原理与应用》
-
《奔跑吧Linux内核》
-
《深入理解Linux进程与内存》
-
《基于龙芯的Linux内核探索解析》
Linux内存管理:(三)物理页面分配流程 及 Linux6.5源码分析(下)——释放物理页面到伙伴系统
在上篇文章中,我们介绍了从伙伴系统中申请物理页面的快速路径,在上上篇文章中,我们介绍了分配和释放物理页面的核心接口、gfp_mask掩码以及在Linux6.5内核中是如何实现物理页面分配的。具体内容详见:
Linux内存管理:(一)物理页面分配流程 及 Linux6.5源码分析(上)-优快云博客
Linux内存管理:(二)物理页面分配流程 及 Linux6.5源码分析(中)-优快云博客
本篇文章是该部分的 (下)章节,主要介绍如何将物理页面释放到伙伴系统中
1 释放页面 _free_pages()
书接上文, 在【3. 物理页面分配alloc_pages源码初步解析】中介绍了实现物理页面分配的内核函数alloc_pages
, 并在【快速路径分配物理页面流程】中介绍了该函数的具体实现方法:快速分配路径以及慢速分配路径;既然有物理页面的分配,必然有物理页面的释放,本篇文章围绕_free_pages
进行更进一步的实现分析。
_free_pages()
会检查需要释放页框的引用次数,确保该页框没有被引用的情况下,一次行释放整个页块;当有任务在引用该页块时,则需要递归逐级释放页块; 页块释放工作主要是通过free_the_page
, 他会判断将页块释放到pcp链表还是释放到伙伴系统中;
void __free_pages(struct page *page, unsigned int order)
{
/* 1.获取复合页的头,检查是否是pagehead页 */
int head = PageHead(page);
/* 2. 获取页框引用次数
* 减少一个页框的引用次数;
* 2.1检查引用次数是否归零,若归零则可以安全释放该页块;
* 2.2否则尝试递归逐级释放页块;
*/
if (put_page_testzero(page))
/*释放页框*/
free_the_page(page, order);
else if (!head)
/*3.递归释放当前页和其余高阶页*/
while (order-- > 0)
free_the_page(page + (1 << order), order);
}
EXPORT_SYMBOL(__free_pages);
1.1 free_the_page 释放页框
free_the_page
函数实现了具体的页面释放工作,该函数会根据要释放页块的地址及大小选择释放到pcp链表还是伙伴系统对应链表中; 这里的思路与页面分配时非常类似,优先考虑pcp页面缓存, 这样即省去了繁琐的锁操作, 又扩充了pcp页面缓存链表; 若页块大小不符合pcp链表,则考虑将页块释放到伙伴系统相应空闲链表;
- 其中负责检测页块大小是否满足pcp的函数在前面的章节见过
pcp_allowed_order
; free_unref_page
负责将页块放入pcp链表;__free_pages_ok
负责将页块放入伙伴系统中;
/*根据页面大小和page首地址,释放物理页面*/
static inline void free_the_page(struct page *page, unsigned int order)
{
/*1.大小是否满足pcp链表
* 若满足pcp要求,调用 free_unref_page 尝试将页面释放进pcp链表中作为页面缓存;
*/
if (pcp_allowed_order(order)) /* Via pcp? */
free_unref_page(page, order);
else
/*2. 不满足pcp链表要求,尝试将空闲页块放入伙伴系统*/
__free_pages_ok(page, order, FPI_NONE);
}
1.1.1 释放到pcp链表
free_unref_page
负责将物理页块放入pcp缓存链表中,如果页块是隔离页块,则将页块放入伙伴系统中;这里的思维逻辑是:能放回pcp链表,则尽量放入pcp链表中,如果因为页面迁移类型无法放入pcp链表,则退一步,尝试放入伙伴系统中;
该函数考虑到了隔离页块的情况,隔离页面通常用于内存热插拔或其他需要独占页面的场景,不适合频繁的迁移,而pcp链表的高效性和快捷性则是通过频繁的分配释放页面;二者是相悖的;
/*
* 负责将页面块优先释放到 Per-CPU Pages(PCP)链表 中以提高性能
* 如果 PCP 不可用,则直接释放到伙伴系统中
*/
void free_unref_page(struct page *page, unsigned int order)
{
unsigned long __maybe_unused UP_flags;
struct per_cpu_pages *pcp;
struct zone *zone;
unsigned long pfn = page_to_pfn(page);//page首地址
int migratetype;
/*1. 检查页块是否可安全释放*/
if (!free_unref_page_prepare(page, pfn, order))
return;
/*2. 确定页面迁移类型*/
migratetype = get_pcppage_migratetype(page);
/*3. 如果迁移类型超出pcp支持范围,则考虑隔离页面的情况*/
if (unlikely(migratetype >= MIGRATE_PCPTYPES)) {
/*3.1 如果该页面是隔离页面,则不能放入pcp链表,需放入伙伴系统
* 这是因为,隔离页面 需要避免频繁的迁移操作,
* 而pcp链表的高效性就依赖于频繁分配和释放的页面
*/
if (unlikely(is_migrate_isolate(migratetype))) {
free_one_page(page_zone(page), page, pfn, order, migratetype, FPI_NONE);
return;
}
/*3.2 将迁移类型改为 MIGRATE_MOVABLE
* 不是隔离页面,则修改迁移类型,尝试放入pcp链表
*/
migratetype = MIGRATE_MOVABLE;
}
zone = page_zone(page);
/*4. 释放到pcp链表
* 如果成功锁定pcp链表,则直接释放到pcp链表;
* 如果未锁定,则释放到伙伴系统;
*/
pcp_trylock_prepare(UP_flags);
pcp = pcp_spin_trylock(zone->per_cpu_pageset);
if (pcp) {
/*4.1 释放到pcp链表*/
free_unref_page_commit(zone, pcp, page, migratetype, order);
pcp_spin_unlock(pcp);
} else {
/*4.2 释放到伙伴系统*/
free_one_page(zone, page, pfn, order, migratetype, FPI_NONE);
}
pcp_trylock_finish(UP_flags);
}
1.1.2 __free_pages_ok释放到伙伴系统中
__free_pages_ok
函数在将页块释放到伙伴系统时,也考虑到了隔离页块的情况,如果页块中包含隔离页面,则需要再次确认页面迁移类型;最终会通过__free_one_page
函数将页块释放到伙伴系统;
/*将页块释放到伙伴系统中*/
static void __free_pages_ok(struct page *page, unsigned int order,
fpi_t fpi_flags)
{
/*1. 页面释放前的准备*/
if (!free_pages_prepare(page, order, fpi_flags))
return;
/*2. 获得页面迁移类型*/
migratetype = get_pfnblock_migratetype(page, pfn);
/*3. 开始页面释放:
*3.1 给当前zone区域的伙伴系统上锁;
*3.2 隔离页块判断;
*3.3 __free_one_page释放页块;
*3.4 释放锁;
*/
/*3.1 给当前zone区域的伙伴系统上锁*/
spin_lock_irqsave(&zone->lock, flags);
/*3.2 如果有隔离页块,需要再次确定页面迁移类型*/
if (unlikely(has_isolate_pageblock(zone) ||
is_migrate_isolate(migratetype))) {
migratetype = get_pfnblock_migratetype(page, pfn);
}
/*3.3 释放页块*/
__free_one_page(page, pfn, zone, order, migratetype, fpi_flags);
/*3.4 释放锁*/
spin_unlock_irqrestore(&zone->lock, flags);
__count_vm_events(PGFREE, 1 << order);
}
我们看一下__free_one_page
的逻辑 ,该函数用于将需要释放的页块释放到伙伴系统, 并尝试逐阶查找并合并伙伴页块,直至没有空闲伙伴页块为止;
关于如何逐阶合并伙伴页块, 介绍如下: 会从目标页块阶数order开始寻找相邻的空闲伙伴页块, 如果找到满足要求的伙伴页块,则将两个页块(目标页块与伙伴页块)合并为一个order+1阶的页块; 执行这样一次合并之后, 将以新页块(order+1阶)为目标页块,去寻找其伙伴页块, 并进行合并工作直至找不到伙伴页块为止;
其具体步骤及源码实现如下:
- 先进行条件检查;
- 逐阶查找并合并伙伴页块;
- 通过
find_buddy_page_pfn
找到满足要求的伙伴页块; - 检查目标页块与伙伴页块迁移类型是否一致,是否可以合并;
- 将伙伴页块从链表中移除
del_page_from_free_list
, 并更新页块信息;
- 通过
- 合并操作: 标记合并后页块阶数, 并将页块插入对应伙伴系统空闲链表中;
/**
* @brief a.将页面释放到伙伴系统
* @brief b.尝试合并相邻的空闲页面块以减少内存碎片
* @brief c.额外操作:根据页面类型及页块隔离等标志,执行额外操作
*
* @param page 要释放的页面块的起始页面
* @param pfn 页面帧号,页面的物理地址
* @param fpi_flags 用于控制页面释放的特殊行为
*/
static inline void __free_one_page(struct page *page,
unsigned long pfn,
struct zone *zone, unsigned int order,
int migratetype, fpi_t fpi_flags)
{
/*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);
VM_BUG_ON_PAGE(pfn & ((1 << order) - 1), page);
VM_BUG_ON_PAGE(bad_range(zone, page), page);
/*2.循环寻找相邻的伙伴空闲页块
* 第一次找与order相同大小的相邻空闲页块并合并,变成阶数为order+1的空闲块
* 第二次找order+1阶大小的相邻空闲块;
* 以此类推直至找不到相邻的空闲页块为止;
*/
while (order < MAX_ORDER) {
if (compaction_capture(capc, page, order, migratetype)) {
__mod_zone_freepage_state(zone, -(1 << order),
migratetype);
return;
}
/*2.1 找到当前页块的伙伴页块
* 伙伴页块的要求:a同属一个zone; b大小相同; c处于空闲状态;
* 没找到就直接去合并;
*/
buddy = find_buddy_page_pfn(page, pfn, order, &buddy_pfn);
if (!buddy)
goto done_merging;
/*2.2 大阶页块情况, 检查迁移类型的可合并性
* 考虑页块迁移类型是否相同,
* 以及两个页块是否均可以合并;
*/
if (unlikely(order >= pageblock_order)) {
int buddy_mt = get_pageblock_migratetype(buddy);
if (migratetype != buddy_mt
&& (!migratetype_is_mergeable(migratetype) ||
!migratetype_is_mergeable(buddy_mt)))
goto done_merging;
}
/*2.3 合并操作*/
if (page_is_guard(buddy))//清理guard标志
clear_page_guard(zone, buddy, order, migratetype);
else
/*从当前阶数的空闲链表中删除,准备合并*/
del_page_from_free_list(buddy, zone, order);
/*2.4 新页块信息更新*/
combined_pfn = buddy_pfn & pfn;
page = page + (combined_pfn - pfn);
pfn = combined_pfn;
order++;
}
/*4. 执行合并处理*/
done_merging:
/*4.1 标记合并后页块的阶数,一遍后续伙伴系统识别*/
set_buddy_order(page, order);
/*4.2 页面块插入空闲链表的头部或尾部*/
if (to_tail)
add_to_free_list_tail(page, zone, order, migratetype);
else
add_to_free_list(page, zone, order, migratetype);
if (!(fpi_flags & FPI_SKIP_REPORT_NOTIFY))
page_reporting_notify_free(order);
}
该函数的重点就在通过while循环去逐阶寻找伙伴页块, 最终通过尾插法或头插法将合并好的页块插入对应的伙伴系统链表中; 那么如何找到空闲的伙伴页块是重点, 下面将根据源码分析函数find_buddy_page_pfn
;
1.1.2.1 查找伙伴页块 find_buddy_page_pfn
首先根据目标页块的物理地址和阶数,可以计算相邻的伙伴页块物理地址(__find_buddy_pfn(pfn, order)
), 通过page_is_buddy(page, buddy, order)
检查该伙伴页块是否满足以下要求:
-
- 目标页块与伙伴页块同属一个zone区域;
- 目标页块与伙伴页块大小一致, 即order一致;
- 伙伴页块是空闲页块;
/*找到一个空闲的伙伴页块*/
static inline struct page *find_buddy_page_pfn(struct page *page,
unsigned long pfn, unsigned int order, unsigned long *buddy_pfn)
{
/*1. 找到伙伴页块的物理首地址*/
unsigned long __buddy_pfn = __find_buddy_pfn(pfn, order);
struct page *buddy;
/*2. 计算page指针地址*/
buddy = page + (__buddy_pfn - pfn);
if (buddy_pfn)
*buddy_pfn = __buddy_pfn;
/*3. 检查是否是一个空闲且符合要求的伙伴页块
* 同属一个zone,页块大小相同,且空闲;
*/
if (page_is_buddy(page, buddy, order))
return buddy;
return NULL;
}
A: 通过__find_buddy_pfn
获得伙伴页块物理地址;
/*返回伙伴页块的物理首地址*/
static inline unsigned long
__find_buddy_pfn(unsigned long page_pfn, unsigned int order)
{
return page_pfn ^ (1 << order);
}
B: 通过page_is_buddy
检查伙伴页块是否满足要求;
/**
* @brief 检查page,buddy页块是否满足伙伴页块条件
* 1. 两页块是否在伙伴系统中;
* 2. 两页块大小是否相同;
* 3. 两页块是否属于同一zone;
* */
static inline bool page_is_buddy(struct page *page, struct page *buddy,
unsigned int order)
{
/*1. 检查伙伴页块是否在伙伴系统中,且空闲支持合并*/
if (!page_is_guard(buddy) && !PageBuddy(buddy))
return false;
/*2. 页面块的大小是否相同*/
if (buddy_order(buddy) != order)
return false;
/*3. 检查目标页块与伙伴页块是否在一个zone内*/
if (page_zone_id(page) != page_zone_id(buddy))
return false;
VM_BUG_ON_PAGE(page_count(buddy) != 0, buddy);
return true;
}
至此,我们详细分析了如何将页块释放到伙伴系统或pcp中;