linux内存管理3

伙伴系统的核心数据结构


如上图所示,内核会为 NUMA 节点中的每个物理内存区域 zone 分配一个伙伴系统用于管理该物理内存区域 zone 里的空闲内存页。而伙伴系统的核心数据结构就封装在 struct zone 里.
在本小节中,我们聚焦于伙伴系统相关的数据结构介绍~~

struct zone {
    // 被伙伴系统所管理的物理内存页个数
    atomic_long_t       managed_pages;
    // 伙伴系统的核心数据结构
    struct free_area    free_area[MAX_ORDER];
}

struct zone 结构中的 managed_pages 用于表示该内存区域内被伙伴系统所管理的物理内存页数量。
伙伴系统的真正核心数据结构就是这个 struct free_area 类型的数组 free_area[MAX_ORDER] 。
伙伴系统所分配的物理内存页全部都是物理上连续的,并且只能分配 2 的整数幂个页,这里的整数幂在内核中称之为分配阶 order。

在我们调用物理内存分配接口时,均需要指定这个分配阶 order,意思是从伙伴系统申请多少个物理内存页,假设我们指定分配阶为 order,那么就会从伙伴系统中申请 2 的 order 次幂个物理内存页。

伙伴系统会将物理内存区域中的空闲内存根据分配阶 order 划分出不同尺寸的内存块,并将这些不同尺寸的内存块分别用一个双向链表组织起来。

比如:分配阶 order 为 0 时,对应的内存块就是一个 page。分配阶 order 为 1 时,对应的内存块就是 2 个 pages。依次类推,当分配阶 order 为 n 时,对应的内存块就是 2 的 order 次幂个 pages。

MAX_ORDER - 1 就是内核中规定的分配阶 order 的最大值,定义在 /include/linux/mmzone.h文件中,最大分配阶 MAX_ORDER - 1 = 10,也就是说一次,最多只能从伙伴系统中申请 1024 个内存页,对应 4M 大小的连续物理内存。

/* Free memory management - zoned buddy allocator.  */
#ifndef CONFIG_FORCE_MAX_ZONEORDER
#define MAX_ORDER 11

数组 free_area[MAX_ORDER] 中的索引表示的就是分配阶 order,用于指定对应双向链表组织管理的内存块包含多少个 page。
我们可以通过 cat /proc/buddyinfo 命令来查看 NUMA 节点中不同内存区域 zone 的伙伴系统当前状态:

上图展示了不同内存区域伙伴系统的 free_area[MAX_ORDER] 数组中,不同分配阶对应的内存块个数,从左到右依次是 0 阶,1 阶, … ,10 阶对应的双向链表中包含的内存块个数。

以上内容展示的只是伙伴系统的一个基本骨架,有了这个基本骨架之后,下面笔者继续按照一步一图的方式,来为大家揭开伙伴系统的完整样貌。

我们先从 free_area[MAX_ORDER] 数组的类型 struct free_area 结构开始谈起~~~

struct free_area {
	struct list_head	free_list[MIGRATE_TYPES];
	unsigned long		nr_free;
};
struct list_head {
    // 双向链表
    struct list_head *next, *prev;
};

根据前边的内容我们知道 free_area[MAX_ORDER] 数组描述的只是伙伴系统的一个基本骨架,数组中的每一个元素统一组织存储了相同尺寸的内存块。内存块的尺寸分为 0 阶,1 阶 ,… ,10 阶,一共 MAX_ORDER 个尺寸。

struct free_area 主要描述的就是相同尺寸的内存块在伙伴系统中的组织结构, nr_free 则表示的是该尺寸的内存块在当前伙伴系统中的个数,这个值会随着内存的分配而减少,随着内存的回收而增加。

注意:nr_free 表示的可不是空闲内存页 page 的个数,而是空闲内存块的个数,对于 0 阶的内存块来说 nr_free 确实表示的是单个内存页 page 的个数,因为 0 阶内存块是由一个 page 组成的,但是对于 1 阶内存块来说,nr_free 则表示的是 2 个 page 集合的个数,以此类推对于 n 阶内存块来说,nr_free 表示的是 2 的 n 次方 page 集合的个数

这些相同尺寸的内存块在 struct free_area 结构中是通过 struct list_head 结构类型的双向链表统一组织起来的。

按理来说,内核只需要将这些相同尺寸的内存块在 struct free_area 中用一个双向链表串联起来就行了。

但是我们从源码中却看到内核是用多个双向链表来组织这些相同尺寸的内存块的,这些双向链表组成一个数组 free_list[MIGRATE_TYPES],该数组中双向链表的个数为 MIGRATE_TYPES。

我们从 MIGRATE_TYPES 的字面意思上可以看出,内核会根据物理内存页的迁移类型将这些相同尺寸的内存块近一步通过不同的双向链表重新组织起来。

free_area 是将相同尺寸的内存块组织起来,free_list 是在 free_area 的基础上近一步根据页面的迁移类型将这些相同尺寸 的内存块划分到不同的双向链表中管理

而物理内存页面的迁移类型 MIGRATE_TYPES 定义在 /include/linux/mmzone.h 文件中:

	MIGRATE_UNMOVABLE, // 不可移动
	MIGRATE_MOVABLE,   // 可移动
	MIGRATE_RECLAIMABLE, // 可回收
	MIGRATE_PCPTYPES,	// 属于 CPU 高速缓存中的类型,PCP 是 per_cpu_pageset 的缩写
	MIGRATE_HIGHATOMIC = MIGRATE_PCPTYPES, // 紧急内存
#ifdef CONFIG_CMA
	MIGRATE_CMA, // 预留的连续内存 CMA
#endif
#ifdef CONFIG_MEMORY_ISOLATION
	MIGRATE_ISOLATE,	/* can't allocate from here */
#endif
	MIGRATE_TYPES // 不代表任何区域,只是单纯表示一共有多少个迁移类型
};

MIGRATE_UNMOVABLE 表示不可移动的页面类型,这种类型的物理内存页面是固定的不能随意移动,内核所需要的核心内存大多数是从 MIGRATE_UNMOVABLE 类型的页面中进行分配,这部分内存一般位于内核虚拟地址空间中的直接映射区。

在内核虚拟地址空间的直接映射区中,虚拟内存地址与物理内存地址都是直接映射的,虚拟内存地址通过减去一个固定的偏移量就可以直接得到物理内存地址,由于这种直接映射的关系,所以这部分内存是不能移动的,因为一旦移动虚拟内存地址就会发生变化,这样一来虚拟内存地址减去固定的偏移得到的物理内存地址就不一样了。

MIGRATE_MOVABLE 表示可以移动的内存页类型,这种页面类型一般用于在进程用户空间中分配,因为在用户空间中虚拟内存与物理内存都是通过页表来动态映射的,物理页移动之后,只需要改变页表中的映射关系即可,而虚拟内存地址并不需要改变。一切对进程来说都是透明的。

MIGRATE_RECLAIMABLE 表示不能移动,但是可以直接回收的页面类型,比如前面提到的文件缓存页,它们就可以直接被回收掉,当再次需要的时候可以从磁盘中继续读取生成。或者一些生命周期比较短的内存页,比如 DMA 缓存区中的内存页也是可以被直接回收掉。

完整的伙伴系统结构:

到底什么是伙伴

伙伴在我们日常生活中含义就是形影不离的好朋友,在内核中也是如此,内核中的伙伴指的是大小相同并且在物理内存上是连续的两个或者多个 page。

比如在上图中,free_area[1] 中组织的是分配阶 order = 1 的内存块,内存块中包含了两个连续的空闲 page。这两个空闲 page 就是伙伴。

free_area[10] 中组织的是分配阶 order = 10 的内存块,内存块中包含了 1024 个连续的空闲 page。这 1024 个空闲 page 就是伙伴。

再比如上图中的 page0 和 page 1 是伙伴,page2 到 page 5 是伙伴,page6 和 page7 又是伙伴。但是 page0 和 page2 就不能成为伙伴,因为它们的物理内存是不连续的。同时 (page0 到 page3) 和 (page4 到 page7) 所组成的两个内存块又能构成一个伙伴。伙伴必须是大小相同并且在物理内存上是连续的两个或者多个 page。

伙伴系统的内存分配原理

介绍了如下四个内存分配的接口,内核可以通过这些接口向伙伴系统申请内存:

struct page *alloc_pages(gfp_t gfp, unsigned int order)
unsigned long __get_free_pages(gfp_t gfp_mask, unsigned int order)
unsigned long get_zeroed_page(gfp_t gfp_mask)
unsigned long __get_dma_pages(gfp_t gfp_mask, unsigned int order)

首先我们可以根据内存分配接口函数中的 gfp_t gfp_mask ,找到内存分配指定的 NUMA 节点和物理内存区域 zone ,然后找到物理内存区域 zone 对应的伙伴系统。

随后内核通过接口中指定的分配阶 order,可以定位到伙伴系统的 free_area[order] 数组,其中存放的就是分配阶为 order 的全部内存块。

最后内核进一步通过 gfp_t gfp_mask 掩码中指定的页面迁移类型 MIGRATE_TYPE,定位到 free_list[MIGRATE_TYPE],这里存放的就是符合内存分配要求的所有内存块。通过遍历这个双向链表就可以轻松获得要分配的内存。

比如我们向内核申请 ( 2 ^ (order - 1),2 ^ order ] 之间大小的内存,并且这块内存我们指定的迁移类型为 MIGRATE_MOVABLE 时,内核会按照 2 ^ order 个内存页进行申请。

随后内核会根据 order 找到伙伴系统中的 free_area[order] 对应的 free_area 结构,并进一步根据页面迁移类型定位到对应的 free_list[MIGRATE_MOVABLE],如果该迁移类型的 free_list 中没有空闲的内存块时,内核会进一步到上一级链表也就是 free_area[order + 1] 中寻找。

如果 free_area[order + 1] 中对应的 free_list[MIGRATE_MOVABLE] 链表中还是没有,则继续循环到更高一级 free_area[order + 2] 寻找,直到在 free_area[order + n] 中的 free_list[MIGRATE_MOVABLE] 链表中找到空闲的内存块。

但是此时我们在 free_area[order + n] 链表中找到的空闲内存块的尺寸是 2 ^ (order + n) 大小,而我们需要的是 2 ^ order 尺寸的内存块,于是内核会将这 2 ^ (order + n) 大小的内存块逐级减半分裂,将每一次分裂后的内存块插入到相应的 free_area 数组里对应的 free_list[MIGRATE_MOVABLE] 链表中,并将最后分裂出的 2 ^ order 尺寸的内存块分配给进程使用。

__alloc_pages 内存分配流程


无论是快速路径还是慢速路径下的内存分配都需要最终调用 get_page_from_freelist 函数进行最终的内存分配。只不过,不同路径下 get_page_from_freelist 函数的内存分配策略以及需要考虑的内存水位线会有所不同,其中慢速路径下的内存分配策略会更加激进一些,这一点我们在上篇文章的相关章节内容介绍中体会很深。

在每次调用 get_page_from_freelist 函数之前,内核都会根据新的内存分配策略来重新初始化 struct alloc_context 结构,alloc_context 结构体中包含了内存分配所需要的所有核心参数。

struct alloc_context {
    // 运行进程 CPU 所在 NUMA 节点以及其所有备用 NUMA 节点中允许内存分配的内存区域
    struct zonelist *zonelist;
    // NUMA 节点状态掩码
    nodemask_t *nodemask;
    // 内存分配优先级最高的内存区域 zone
    struct zoneref *preferred_zoneref;
    // 物理内存页的迁移类型分为:不可迁移,可回收,可迁移类型,防止内存碎片
    int migratetype;

    // 内存分配最高优先级的内存区域 zone
    enum zone_type highest_zoneidx;
    // 是否允许当前 NUMA 节点中的脏页均衡扩散迁移至其他 NUMA 节点
    bool spread_dirty_pages;
};

这里最核心的两个参数就是 zonelist 和 preferred_zoneref。preferred_zoneref 表示当前本地 NUMA 节点(优先级最高),其中 zonelist 我们在 《深入理解 Linux 物理内存管理》的 “ 4.3 NUMA 节点物理内存区域的划分 ” 小节中详细介绍过,zonelist 里面包含了当前 NUMA 节点在内的所有备用 NUMA 节点的所有物理内存区域,用于当前 NUMA 节点没有足够空闲内存的情况下进行跨 NUMA 节点分配。

typedef struct pglist_data {
    // NUMA 节点中的物理内存区域个数
    int nr_zones; 
    // NUMA 节点中的物理内存区域
    struct zone node_zones[MAX_NR_ZONES];
    // NUMA 节点的备用列表
    struct zonelist node_zonelists[MAX_ZONELISTS];
} pg_data_t;

struct pglist_data 里的 node_zonelists 是一个全集,而 struct alloc_context 里的 zonelist 是在内存分配过程中,根据指定的内存分配策略从全集 node_zonelists 过滤出来的一个子集(允许进行本次内存分配的所有 NUMA 节点及其内存区域)。

get_page_from_freelist 的核心逻辑其实很简单,就是遍历 struct alloc_context 里的 zonelist,挨个检查各个 NUMA 节点中的物理内存区域是否有足够的空闲内存可以满足本次的内存分配要求,如果可以满足则进入该物理内存区域的伙伴系统中完整真正的内存分配动作。

下面我们先来看一下 get_page_from_freelist 函数的完整逻辑:

参考链接:https://www.cnblogs.com/binlovetech/p/17090846.html

  • 24
    点赞
  • 23
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
Linux 内存管理主要包括内存节点、分区、页框和虚拟内存等概念。 1. 内存节点 Linux 根据 CPU 访问代价的不同将内存划分为不同的分区,即内存节点。内核以 struct zone 来描述内存分区。通常一个节点分为 DMA、Normal 和 High Memory 内存区。其中,DMA 内存区为直接内存访问分区,通常为物理内存的起始16M,供外设使用,外设和内存直接访问数据而无需 CPU 参与;Normal 内存区为从 16M 到 896M 的内存区;HighMemory 内存区为 896M 以后的内存区。 2. 分区 内存节点中的分区是内存管理的基本单位,每个分区都有自己的页框列表和空闲页框列表。页框是内存管理的最小单位,通常为 4KB。内核通过页框来管理内存,将内存分为多个页框,每个页框都有自己的状态,包括已分配、未分配、已使用等。 3. 页框 页框是内存管理的最小单位,通常为 4KB。内核通过页框来管理内存,将内存分为多个页框,每个页框都有自己的状态,包括已分配、未分配、已使用等。内核通过页表来映射虚拟地址和物理地址,将虚拟地址转换为物理地址。 4. 虚拟内存 虚拟内存是一种将硬盘中划出一段 swap 分区当作虚拟的内存,用来存放内存中暂时用不到的内存页,等到需要的时候再从 swap 分区中将对应的内存页调入到内存中的技术。硬盘此时相当于一个虚拟的内存Linux 通过虚拟内存技术来扩展内存,使得进程可以使用比物理内存更大的内存空间。

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值