Linux内存管理:(一)物理页面分配流程 及 Linux6.5源码分析(上)

《Linux6.5源码分析:内存管理系列文章》

本系列文章将对内存管理相关知识进行梳理与源码分析,重点放在linux源码分析上,并结合eBPF程序对内核中内存管理机制进行数据实时拿取与分析。

在进行正式介绍之前,有必要对文章引用进行提前说明。本系列文章参考了大量的博客、文章以及书籍:

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_maskorder 等)初始化 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;
}
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值