《Linux6.5源码分析:内存管理系列文章》
本系列文章将对内存管理相关知识进行梳理与源码分析,重点放在linux源码分析上,并结合eBPF程序对内核中内存管理机制进行数据实时拿取与分析。
在进行正式介绍之前,有必要对文章引用进行提前说明。本系列文章参考了大量的博客、文章以及书籍:
- 《深入理解Linux内核》
- 《Linux操作系统原理与应用》
- 《奔跑吧Linux内核》
- 《深入理解Linux进程与内存》
- 《基于龙芯的Linux内核探索解析》
- Linux内存管理 - 随笔分类 - LoyenWang - 博客园
- 专栏文章目录 - 知乎 (zhihu.com)
- albertxu216/LinuxKernel_Learning/Linux6.5源码注释
Linux内存管理:(一)物理页面分配流程 及 Linux6.5源码分析(上)
分配物理内存是内存管理中最复杂的部分,涉及到页面回收、内存规整、直接回收内存等。本篇文章将关注在内存充足情况下,分配与释放连续物理内存的情况,涉及到以下内容:
- 1.分配和释放物理页面的核心接口;
- 2.gfp_mask掩码介绍;
- 3.Linux6.5 内核分配物理页面的源码初步分析(alloc_pages);
物理页面分配源码深入分析见 Linux内存管理:(一)物理页面分配流程 及 Linux6.5源码分析(中)
1. 核心接口介绍
内核中用于申请与释放物理内存的函数有很多,申请或释放不同的物理内存可以使用不同的接口函数,本小节将先对这些核心接口进行初步介绍,便于读者在阅读源码时有个大致的函数依赖关系网。
1.1. 申请物理页面
首先是用于申请物理页面的函数,alloc_pages()、alloc_page()、__get_free_pages()、__get_free_page()、get_zeroed_page()
等,这些函数最终都会调用alloc_pages()
进行物理内存的申请。其调用关系图如下:
[alloc_pages()初解]
alloc_pages()
函数是内核中分配物理内存页面的常用接口函数,用于分配一个或多个连续的物理页面,分配的页数是2的order次幂,上面所有用于申请物理内存的接口函数最终都会调用该函数。
alloc_pages()
函数的源码实现在include/linux/gfp.h
文件中:
/*
*分配2的order次幂个连续物理页面
*1.gfp_mask 分配掩码,描述页面分配方法的标志
*2.order 分配页面的结束,order必须小于MAX_ORDER
*3.返回值是第一个物理页面的page数据结构;
*/
static inline struct page *alloc_pages(gfp_t gfp_mask, unsigned int order)
{
return alloc_pages_node(numa_node_id(), gfp_mask, order);
}
可以看出来,该函数最后调用alloc_pages_node()
函数实现具体的物理页面分配;
[alloc_page()]
该函数用于申请一个物理页面, 通过宏定义对alloc_pages()的参数order进行固定,来实现分配一个物理页面。此处分配的物理页面可以是高端内存,也可以是低端内存。
/*对alloc_pages函数的宏定义,通过将参数order=0,来分配一个物理页面,可分配高端内存*/
#define alloc_page(gfp_mask) alloc_pages(gfp_mask, 0)
[__get_free_pages()]
__get_free_pages()
函数会调用alloc_pages()
函数,用于分配指定数量的连续低地址内存页面,返回其内核虚拟地址。该函数不能用于申请高端内存,通过~__GFP_HIGHMEM
实现;
/*
* 用于分配指定数量的连续低地址内存页面,返回其内核虚拟地址
* 通过alloc_pages(gfp_mask & ~__GFP_HIGHMEM, order)实现;
* 禁止申请高端内存
*/
unsigned long __get_free_pages(gfp_t gfp_mask, unsigned int order)
{
struct page *page;
/*1.申请低端内存
* gfp_mask & ~__GFP_HIGHMEM:禁用高端内存
*/
page = alloc_pages(gfp_mask & ~__GFP_HIGHMEM, order);
if (!page)
return 0;
/*2.通过page_address获得page结构体的虚拟地址,返回page页的虚拟地址*/
return (unsigned long) page_address(page);
}
注:该函数不能用于申请高内存
- 高内存是无法直接通过
page_address
映射的,因此该函数禁止使用__GFP_HIGHMEM
标志。 - 如果需要访问高内存,需要使用
alloc_pages
分配并通过kmap
进行临时映射。
[__get_free_page()]申请一个物理页面
如果要申请一个物理页面,内核提供了一个接口函数_get_free_page()]
实现,其实质就是对__get_free_pages()
的宏定义, 即默认order为0,申请2^0=1个低地址物理页面;
/*默认order为0,即申请1个物理页面*/
#define __get_free_page(gfp_mask) \
__get_free_pages((gfp_mask), 0)
[get_zeroed_page()] 申请一个全填充为0的物理页面
get_zeroed_page()
用于申请一个全填充为0的物理页面,该函数也是对__get_free_page()
函数的进一步封装,主要是通过添加 __GFP_ZERO修饰符到 gfp_mask中,来确保返回一个全填充为0的物理页面;
/*分配一个全填充为0的物理页面*/
unsigned long get_zeroed_page(gfp_t gfp_mask)
{
/*1.调用__get_free_page申请*/
return __get_free_page(gfp_mask | __GFP_ZERO);
}
1.2. 释放物理页面
内核为释放物理页面也提供了很多接口,__free_pages()
、__free_page()
、free_pages()
、free_page()
,最终都是通过_free_pages()函数实现。
注:需要注意传惨,传递错误的page指针、或错误的order值会引起系统崩溃。
[__free_pages()初解]
__free_pages
函数用于释放指定页框(struct page
)及其后续的多个连续页框(当 order > 0
时)。其传参是物理页面的page结构体指针以及order
void __free_pages(struct page *page, unsigned int order)
{
/* 1.获取复合页的头,检查是否是pagehead页 */
int head = PageHead(page);
/* 2.获取页框引用次数
* 减少一个页框的引用次数;
* 检查引用次数是否归零,若为零则可以安全释放该页
*/
if (put_page_testzero(page))
/*释放页框*/
free_the_page(page, order);
else if (!head)
/*2.递归释放当前页和其余高阶页*/
while (order-- > 0)
free_the_page(page + (1 << order), order);
}
[__free_page()] 释放单个物理页面
该函数是用通过宏定义对__free_pages()
函数的参数order进行固定, 实现释放单个物理页面的功能:
/*释放一个物理页面*/
#define __free_page(page) __free_pages((page), 0)
[free_pages()]通过虚拟地址释放物理页面
该函数区别于__free_pages()
,他可以通过虚拟地址来释放对应的物理页表,主要是使用virt_to_page()函数将虚拟地址转化为与之对应struct page结构体指针,再调用__free_pages()
函数释放对应的物理页面。
/*释放指定虚拟地址 addr 处的一组连续的物理页框*/
void free_pages(unsigned long addr, unsigned int order)
{
if (addr != 0) {
/*1.使用宏 VM_BUG_ON 验证地址的合法性*/
VM_BUG_ON(!virt_addr_valid((void *)addr));
/*2.__free_pages释放具体的物理页面
* 这里使用到了virt_to_page将虚拟地址转换为与之对应的 struct page
*/
__free_pages(virt_to_page((void *)addr), order);
}
}
[free_page()]根据虚拟地址释放单个物理页面
该函数通过宏定义来固定free_pages()的参数order=0,来实现释放单个物理页面。
/*通过虚拟地址释放单个物理页面*/
#define free_page(addr) free_pages((addr), 0)
2. gfp_mask分配掩码
在上一小节中,我们介绍了若干个分配和释放物理页面的接口函数,以及在不同情况下的延伸函数,这些接口函数都是通过一个分配掩码来判断页面的分配方法,通过order来确定分配或释放多少页面。本小节将介绍分配掩码的作用以及不同分配掩码的含义。
首先简单介绍一下分配掩码:分配掩码gfp_mask
是描述页面分配方法的标志,他影响着页面的整个分配流程。对于gfp_t
的定义如下:
typedef unsigned int __bitwise gfp_t;/*其实是u32类型*/
在文件include/linux/gfp.h中定义了所有的gfp分配掩码类型:
#define ___GFP_DMA 0x01u
#define ___GFP_HIGHMEM 0x02u
#define ___GFP_DMA32 0x04u
#define ___GFP_MOVABLE 0x08u
#define ___GFP_RECLAIMABLE 0x10u
#define ___GFP_HIGH 0x20u
#define ___GFP_IO 0x40u
#define ___GFP_FS 0x80u
#define ___GFP_ZERO 0x100u
....
分配掩码主要有以下几大类:
- 内存管理区修饰符
- 移动修饰符
- 水位修饰符
- 页面回收修饰符
- 行为修饰符
2.1. 内存管理区修饰符
内存管理区修饰符:表示应当从哪个内存管理区来分配物理内存;
2.2. 移动修饰符的标志
移动修饰符主要是用于指示分配出来的页面的移动属性,在分配内存时,需要指定所分配的页面具有哪些迁移属性。
2.3. 水位修饰标志
水位修饰符是用于控制是否可以访问系统预留的内存(即最低警戒水位以下的内存,只有高优先级的分配请求才可以访问);
2.4. 页面回收修饰符
页面回收修饰符主要用于描述在内存不足时,内核如何从不同类型的内存区域中回收页面;
2.5. 行为修饰符
2.6. 常用类型
内核定义一些常用的标志组合供开发者使用:
3. 物理页面分配alloc_pages源码初步解析
在第一小节中简单介绍了一下alloc_pages()
函数, 该函数会调用alloc_pages_node()
去实现物理页面的分配, 通过一些列的函数调用,最终会在__alloc_pages()
中通过 get_page_from_freelist()
、__alloc_pages_slowpath()
实现物理页面的分配工作。其调用关系图如下:
/* alloc_pages函数调用关系:
* alloc_pages()
* -->alloc_pages_node()
* -->__alloc_pages_node()
* -->__alloc_pages()
*/
/*include/linux/gfp.h*/
static inline struct page *alloc_pages(gfp_t gfp_mask, unsigned int order)
{
return alloc_pages_node(numa_node_id(), gfp_mask, order);
}
/**
* @brief 在指定 NUMA 节点上分配连续页面,如果调用者未指定节点
* @brief (nid == NUMA_NO_NODE),函数会选择当前 CPU 所属的节点
*
* @param nid: NUMA 节点 ID
* @param gfp_mask 分配掩码,控制分配行为
* @param order 要分配物理页面的页面数
**/
static inline struct page *alloc_pages_node(int nid, gfp_t gfp_mask,
unsigned int order)
{
/*1.若未指定NUMA节点,则通过numa_mem_id获取当前cpu所属节点*/
if (nid == NUMA_NO_NODE)
nid = numa_mem_id();
/*2.根据指定的 NUMA 节点执行页面分配*/
return __alloc_pages_node(nid, gfp_mask, order);
}
/**
* @brief 用于在指定 NUMA 节点上分配内存页的辅助函数
*
* @param nid 指定的NUMA id ,用于从该节点分配内存
* @param gfp_mask GFP标志掩码;
* @param order 分配物理页数
**/
static inline struct page *
__alloc_pages_node(int nid, gfp_t gfp_mask, unsigned int order)
{
/* 1.合法检查
* 1.1 确保nid NUMA id 在合法范围内
* 1.2 检查目标 NUMA 节点是否在线
*/
VM_BUG_ON(nid < 0 || nid >= MAX_NUMNODES);
warn_if_node_offline(nid, gfp_mask);
/* 2.调用__alloc_pages()函数分配物理页面*/
return __alloc_pages(gfp_mask, order, nid, NULL);
}
这里说一个不重要的“知识点”,在linux5.0内核中,__alloc_pages()函数还会在进入一层函数调用,去调用函数__alloc_pages_nodemask()
,而在linux5.15中则是省掉了这部分冗余的调用,我们对比了linux5.0中的__alloc_pages_nodemask()
与linux5.15中的__alloc_pages()
函数,二者基本一致;
3.1. alloc_context结构体介绍
在进行__alloc_pages()
源码分析之前,需要对alloc_context
结构体进行简单的介绍,alloc_context
结构体用于描述内存分配的上下文环境,包含分区、节点掩码、迁移类型等信息。它在分配操作的各个阶段中传递,以确保参数的一致性和便捷性。
struct alloc_context {
/*1.指向内存节点node的zonelist成员*/
struct zonelist *zonelist;
/*2.内存节点node的掩码*/
nodemask_t *nodemask;
/*3.表示首选zone对应的zoneref结构体*/
struct zoneref *preferred_zoneref;
/*4.迁移类型*/
int migratetype;
/*5.允许内存分配的最高zone*/
enum zone_type highest_zoneidx;
/*6.是否将脏页在各个zone之间分散分配*/
bool spread_dirty_pages;
};
下面是对aloc_context
结构体成员更详尽的介绍:
zonelist
: 定义了内存分配时需要考虑的内存区域(zone)顺序。该字段帮助分配器决定从哪个 zone 中分配内存。后面将对zonelist进行更进一步的介绍;nodemask
: 表示可用节点的位掩码(bitmask)。在 NUMA(非统一内存访问)系统中,该字段用于限制分配内存的节点范围,确保分配满足 NUMA 的节点约束。preferred_zoneref
:指向zonelist
中的首选 zone 引用(zoneref)。具体首选 zone 的选择通常基于内存分配策略或性能优先级(如本地节点的优先性)。migrate_type
: 指定分配的页面迁移类型(例如:可移动、不可移动或可回收)。这决定了页面在内存中的组织和管理方式。highest_zoneidx
: 定义了本次分配请求的最高可用内存 zone 索引(zone index)。它的作用包括:- **保护低内存区域:**通过
lowmem_reserve[highest_zoneidx]
保护比highest_zoneidx
更低的内存区域,防止这些区域被过度分配。 - **限制目标 zone:**确保回收或压缩(reclaim/compaction)操作不会针对高于
highest_zoneidx
的 zone,这样避免浪费计算资源。
- **保护低内存区域:**通过
spread_dirty_pages
:表示是否将脏页(已修改但尚未写回磁盘的页)在各个 zone 之间分散分配,避免特定 zone 承受过大的负载或过早耗尽。
3.2. node、zone、zonelist间关系
在一个NUMA系统中存在着多个node节点,struct pglist_data
表示 单个 NUMA 节点 的信息,在这个结构体中维护着两个数组:struct zone node_zones[]
和struct zonelist node_zonelists[]
,前者表示当前内存节点下所包含的不同的zone区域(橙色箭头代表了该数组所维护的zone区域),后者则表示当前内存节点下所有可用zone的链表(蓝色箭头表示了该数组所维护的关系)。
针对于struct zonelist node_zonelists[]
数组,是由两个struct zonelist
的链表组成:ZONELIST_FALLBACK
以及ZONELIST_NOFALLBACK
,前者指向本地zone,后者用于NUMA系统,指向远端的内存节点的zone;
结构体 zonelist
管理着当前node下所有可用zone,其中排在第一个的zone是分配器首选zone;该结构体下有一个zoneref数据结构的数组,每个zoneref描述一个zone;
struct zonelist {
struct zoneref _zonerefs[MAX_ZONES_PER_ZONELIST + 1];
};
struct zoneref {
struct zone *zone; /* 指向真正zone结构体的指针 */
int zone_idx; /* zone_idx(zoneref->zone) */
};
三者间关系可以通过下面这张图反应出来。
3.4 __alloc_pages()源码分析
经过一系列的调用关系、数据结构预热,终于来到了真正分配物理页面的__alloc_pages()
函数。该函数的主要可以概括为以下几步:
- 1.order参数范围合法的检查,确保order始终小于系统规定的最大值MAX_ORDER;
- 2.调用
current_gfp_context()
函数,根据当前上下文,修改gfp掩码; - 3.调用
prepare_alloc_pages()
函数,初始化分配器所需参数,并放入结构体alloc_context
中; - 4.调用
alloc_flags_nofragment()
函数,调整初次分配策略,避免使用会导致碎片化的内存区域; - 5.初次尝试分配物理内存,调用
get_page_from_freelist()
函数,从伙伴系统的空闲链表中快速分配物理页面; - 6.慢路径分配物理页面,调用
__alloc_pages_slowpath()
函数尝试使用更多的资源策略进行页面分配;这一点会在后面章节中涉及到 - 7.如果分配成功,则调用
__memcg_kmem_charge_page
函数对页面进行cgroup内存计费; - 8.调用
kmsan_alloc_page()
函数,初始化页面内存状态。
我们可以在下面这张图中看到__alloc_pages()
函数的代码逻辑;
其中get_page_from_freelist()
快速路径分配以及_alloc_pages_slowpath()
慢速路径分配,将会在后面的章节深入展开说明,本篇文章将针对物理页面分配过程中前期准备(alloc_context
结构体的初始化)、分配的宏观流程进行分析;
为了更详尽的展示该函数的实现过程,我们对源码没有进行省略,为了突出重点,我们在相应的代码块前进行了步骤注释:
/**
* @brief 伙伴分配器的核心
* @brief 尝试分配一组连续的物理内存页,返回对应的 struct page 结构指针。
* @brief 它处理了多种内核分配情景,包括页框分配策略、NUMA 节点优先级,
* @brief 以及内存不足时的回退策略。
*
* @param gfp 分配标志,用于指定分配行为
* @param order 指定要分配的连续物理页面数量,2^order个页面
* @param preferred_nid 首选的NUMA节点
* @param nodemask 指定可以分配页面的节点掩码
* @return struct page 所申请的page结构体指针
**/
struct page *__alloc_pages(gfp_t gfp, unsigned int order, int preferred_nid,
nodemask_t *nodemask)
{
struct page *page;
unsigned int alloc_flags = ALLOC_WMARK_LOW;/*页面分配的行为与属性,可分配低水位的内存*/
gfp_t alloc_gfp; /* The gfp_t that was actually used for allocation */
struct alloc_context ac = { };//alloc_context是伙伴系统分配函数中用于保存相关的参数
/* 1. 检查 order 是否超出 MAX_ORDER 的限制*/
if (WARN_ON_ONCE_GFP(order > MAX_ORDER, gfp))
return NULL;
gfp &= gfp_allowed_mask;//检差gfp是否合法
/*
* 2.根据当前的分配上下文(如是否禁止文件系统操作或 IO 操作)修改 GFP 标志。
* 例如,memalloc_no{fs,io}_{save,restore} 会设置 GFP_NOFS 或 GFP_NOIO。
*/
gfp = current_gfp_context(gfp);
alloc_gfp = gfp;
/* 3.初始化页面分配器中用到的参数
* 3.1 将初始化分配器中的相关信息,并放入alloc_context结构体中;
* 3.2 根据 gfp 标志调整分配策略
*/
if (!prepare_alloc_pages(gfp, order, preferred_nid, nodemask, &ac,
&alloc_gfp, &alloc_flags))
return NULL;
/* 4.初次尝试分配内存时,避免使用会导致碎片化的内存区域*/
alloc_flags |= alloc_flags_nofragment(ac.preferred_zoneref->zone, gfp);
/* 5.从伙伴系统的空闲链表中,快速分配物理页面*/
page = get_page_from_freelist(alloc_gfp, order, alloc_flags, &ac);
if (likely(page))
/*5.1分配成功*/
goto out;
/* 6.分配不成功,则调整gfp标志,并尝试慢速路径*/
alloc_gfp = gfp;
ac.spread_dirty_pages = false;//禁止慢速路径中传播脏页
ac.nodemask = nodemask;
/* 6.1 __alloc_pages_slowpath慢速路径分配物理页面,
* 尝试使用更多的资源或策略进行页面分配
*/
page = __alloc_pages_slowpath(alloc_gfp, order, &ac);
out:
/* 7. 如果分配成功,对页面进行 cgroup 相关的内存计费*/
if (memcg_kmem_online() && (gfp & __GFP_ACCOUNT) && page &&
unlikely(__memcg_kmem_charge_page(page, gfp, order) != 0)) {
__free_pages(page, order);
page = NULL;
}
trace_mm_page_alloc(page, order, alloc_gfp, ac.migratetype);
/* 8.初始化页面的内存的状态*/
kmsan_alloc_page(page, order, alloc_gfp);
return page;
}
这里我们可以看到存在一个tracepoint挂载点trace_mm_page_alloc(page, order, alloc_gfp, ac.migratetype);
,可用于跟踪物理内存分配成功时的页面信息;
3.4.1 prepare_alloc_pages准备工作
该函数用于初始化页面分配器重用到的参数,将其保存在alloc_context
结构体(4.1.3.1中有详细介绍)中,是__alloc_pages()内存分配的预处理阶段。
- 根据分配需求(
gfp_mask
、order
等)初始化struct alloc_context
,包括最高 zone 索引、分配顺序、节点掩码等。 - 为内存分配设置额外标志和条件(如 CPU 集合、脏页扩散)。
- 检查是否满足内存分配条件,并返回结果。
/**
* @brief 初始化与内存分配相关的上下文参数,将其放在alloc_context结构体中
*
* @param gfp_mask 分配标志掩码,描述分配的类型和方式
* @param order 分配多少页面
* @param preferred_nid 表示首选的 NUMA 节点,用于优先从该节点分配内存
* @param ac 内存分配上下文,用于存储分配相关的参数和配置;
* @param alloc_gfp 指向实际分配时使用的 gfp_mas
* @param alloc_flags 存储额外的分配标志,用于影响内存分配的行为
**/
static inline bool prepare_alloc_pages(gfp_t gfp_mask, unsigned int order,
int preferred_nid, nodemask_t *nodemask,
struct alloc_context *ac, gfp_t *alloc_gfp,
unsigned int *alloc_flags)
{
/*1.通过gfp_zone() 获取最高可用内存区域的索引*/
ac->highest_zoneidx = gfp_zone(gfp_mask);
/*2.通过node_zonelist()获取首选节点对应的zonelist;*/
ac->zonelist = node_zonelist(preferred_nid, gfp_mask);
ac->nodemask = nodemask;
/*3.gfp_migratetype()根据分配掩码gfp_mask获取页面迁移类型*/
ac->migratetype = gfp_migratetype(gfp_mask);
might_alloc(gfp_mask);
if (should_fail_alloc_page(gfp_mask, order))
return false;
/*4.根据 gfp_mask 和 alloc_flags 的初始值生成最终的分配标志*/
*alloc_flags = gfp_to_alloc_flags_cma(gfp_mask, *alloc_flags);
/*5.根据gfp_mask设置脏页扩散标志,允许在分配过程中扩散脏页*/
ac->spread_dirty_pages = (gfp_mask & __GFP_WRITE);
/*6.获取 zonelist 中第一个符合分配条件的 zone,并将其设置为首选区域。*/
ac->preferred_zoneref = first_zones_zonelist(ac->zonelist,
ac->highest_zoneidx, ac->nodemask);
return true;
}