深度剖析 Linux 伙伴系统的设计与实现

news2024/12/30 2:35:11

目录

伙伴系统的核心数据结构

总结:

 到底什么是伙伴

伙伴系统的内存分配原理 

伙伴系统的内存回收原理

 伙伴系统的实现

从 CPU 高速缓存列表中获取内存页


伙伴系统的核心数据结构

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

 

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

数组 free_area[MAX_ORDER] 中的索引表示的就是分配阶 order,用于指定对应双向链表组织管理的内存块包含多少个 page。

struct free_area {
 struct list_head free_list[MIGRATE_TYPES];
 unsigned long  nr_free;
};

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 的基础上近一步根据页面的迁移类型将这些相同尺寸的内存块划分到不同的双向链表中管理

enum migratetype {
 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 // 不代表任何区域,只是单纯表示一共有多少个迁移类型
};

总结:

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

struct free_area {
    struct list_head    free_list[MIGRATE_TYPES];
    unsigned long       nr_free;
};

首先伙伴系统会将物理内存区域 zone 中的空闲内存页按照分配阶 order 将相同尺寸的内存块组织在 free_area[MAX_ORDER] 数组中:随后在 struct free_area 结构中伙伴系统近一步根据这些相同尺寸内存块的页面迁移类型 MIGRATE_TYPES,将相同迁移类型的物理页面组织在 free_list[MIGRATE_TYPES] 数组中,最终形成了完整的伙伴系统结构。

enum migratetype {
 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 // 不代表任何区域,只是单纯表示一共有多少个迁移类型
};

 到底什么是伙伴

内核中的伙伴指的是大小相同并且在物理内存上是连续的两个或者多个 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],这里存放的就是符合内存分配要求的所有内存块。通过遍历这个双向链表就可以轻松获得要分配的内存。

举个例子

 

我们假设当前伙伴系统中只有 order = 3 的空闲链表 free_area[3],其余剩下的分配阶 order 对应的空闲链表中均是空的。 free_area[3] 中仅有一个空闲的内存块,其中包含了连续的 8 个 page。

现在我们向伙伴系统申请一个 page 大小的内存(对应的分配阶 order = 0),那么内核会在伙伴系统中首先查看 order = 0 对应的空闲链表 free_area[0] 中是否有空闲内存块可供分配。

随后内核会根据前边介绍的内存分配逻辑,继续升级到 free_area[1] ,  free_area[2] 链表中寻找空闲内存块,直到查找到  free_area[3] 发现有一个可供分配的内存块。这个内存块中包含了 8 个 连续的空闲 page,但是我们只要一个 page 就够了,那该怎么办呢?

于是内核先将 free_area[3] 中的这个空闲内存块从链表中摘下,然后减半分裂成两个内存块,分裂出来的这两个内存块分别包含 4 个 page(分配阶 order = 2)。

现在我们加上了内存 MIGRATE_TYPES 的组织结构,其实分配流程还是和核心流程一样的,只不过上面提到的那些高阶 order 的减半分裂情形都发生在各个 free_area[order] 中固定的 free_list[MIGRATE_TYPE] 里罢了。

比如我们要求分配的内存迁移属性要求是 MIGRATE_MOVABLE 类型,那么减半分裂流程分别发生在 free_area[2] ,free_area[1] ,free_area[0] 对应的 free_list[MIGRATE_MOVABLE] 中,多了一个 free_list 的维度,仅此而已。

不过笔者这里想重点着墨的地方是内存分配的一种异常情形,比如我们想要分配特定迁移类型的内存,但是当前伙伴系统所有 free_area[order] 里对应的 free_list[MIGRATE_TYPE] 均无法满足内存分配的需求(没有足够特定迁移类型的空闲内存块)。那么这种场景下内核会怎么处理呢?

当时笔者介绍内存 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;

 每个 NUMA 节点的 struct pglist_data 结构中都会包含一个 node_zonelists,其中包含了当前NUMA 节点以及备用 NUMA 节点的所有内存区域以及对应的伙伴系统,当前 NUMA 节点内存不足时,内核会从 node_zonelists 中的备用 NUMA 节点中分配内存。

正常的分配流程先是从低阶到高阶依次查找空闲内存块,然后将高阶中的内存块依次减半分裂到低阶 free_list 链表中。

内存分配 fallback 流程(从其他节点分配内存)则刚好是相反的,它是先从备用 fallback 类型的迁移列表中的最高阶开始查找,找到一块空闲内存块之后,先迁移到最初指定的 free_list[MIGRATE_TYPE]  链表中,然后在指定的 free_list[MIGRATE_TYPE]  链表执行减半分裂。

 

内核这里的 fallback 策略是:如果无法避免分配迁移类型不同的内存块,那么就分配一个尽可能大的内存块(从最高阶开始查找),避免向其他链表引入内存碎片。

当我们向伙伴系统申请 MIGRATE_UNMOVABLE 迁移类型的内存时,假设内核在伙伴系统中的 free_area[0] 到 free_area[10] 中的所有 free_list[MIGRATE_UNMOVABLE] 链表中均无法找到一个空闲的内存块。

那么就会 fallback 到 MIGRATE_RECLAIMABLE 类型,从最高阶 free_area[10] 中的 free_list[MIGRATE_RECLAIMABLE] 链表开始查找,如果找到一个空闲的内存块,则首先会迁移到对应的 order 的 free_list[MIGRATE_UNMOVABLE] 链表,然后流程继续回到核心流程,在各个  free_area[order]  对应的 free_list[MIGRATE_UNMOVABLE] 链表中执行减半分裂。

伙伴系统的内存回收原理

伙伴系统中的内存回收刚好和内存分配的过程相反,核心则是从低阶 free_list 中寻找释放内存块的伙伴,如果没有伙伴则将要释放的内存块插入到对应分配阶 order 的 free_list中。如果存在伙伴,则将释放内存块与它的伙伴合并,作为一个新的内存块继续到更高阶的 free_list 中循环重复上述过程,直到不能合并为止

内存分配是从高阶先查找到空闲内存块,然后依次减半分裂,将分裂后的内存块插入到低阶的 free_list 中,将最后分裂出来的内存块分配给进程。

内存释放是先从低阶开始查找释放内存块的伙伴,如果找到,则两两合并成一个新的内存块,随后继续到高阶中去查找新内存块的伙伴,直到没有伙伴可以合并。

举个例子:

最初的伙伴系统视角:

 

物理页视角:

有了这些基本概念之后,我回过头来在看 page10 释放回伙伴系统的整个过程: 

由于我们要释放的内存块只包含了一个物理内存页 page10,所以它的分配阶 order = 0,首先内核需要在伙伴系统 free_area[0] 中查找与 page10 大小相等并且连续的内存块(伙伴)。

从物理内存的真实视图中我们可以看到 page11 是 page10 的伙伴,于是将 page11 从 free_area[0] 上摘下并与 page10 合并组成一个新的内存块(分配阶 order = 1)。随后内核会在 free_area[1] 中查找新内存块的伙伴:

 

我们继续对比物理内存页的真实视图,发现在 free_area[1] 中 page8 和 page9 组成的内存块与 page10 和 page11 组成的内存块是伙伴,于是继续将这两个内存块(分配阶 order = 1)继续合并成一个新的内存块(分配阶 order = 2)。随后内核会在 free_area[2] 中查找新内存块的伙伴: 

继续对比物理内存页的真实视图,发现在 free_area[2] 中 page12,page13,page14,page15 组成的内存块与 page8,page9,page10,page11 组成的新内存块是伙伴,于是将它们从 free_area[2] 上摘下继续合并成一个新的内存块(分配阶 order = 3),随后内核会在 free_area[3] 中查找新内存块的伙伴: 

最后:

内存分配是从高阶先查找到空闲内存块,然后依次减半分裂,将分裂后的内存块插入到低阶的 free_list 中,将最后分裂出来的内存块分配给进程。

内存释放是先从低阶开始查找释放内存块的伙伴,如果找到,则两两合并成一个新的内存块,随后继续到高阶中去查找新内存块的伙伴,直到没有伙伴可以合并。

 伙伴系统的实现

 现在内核通过前边介绍的 get_page_from_freelist 函数,循环遍历 zonelist 终于找到了符合内存分配条件的物理内存区域 zone。接下来就会通过 rmqueue 函数进入到该物理内存区域 zone 对应的伙伴系统中实际分配物理内存。

从 CPU 高速缓存列表中获取内存页

内核对只分配一页物理内存的情况做了特殊处理,当只请求一页内存时,内核会借助 CPU 高速缓存冷热页列表 pcplist 加速内存分配的处理,此时分配的内存页将来自于 pcplist 而不是伙伴系统。pcp 是 per_cpu_pageset 的缩写,内核会为每个 CPU 分配一个高速缓存列表。

在 NUMA 内存架构下,每个物理内存区域都归属于一个特定的 NUMA 节点,NUMA 节点中包含了一个或者多个 CPU,NUMA 节点中的每个内存区域会关联到一个特定的 CPU 上.

而每个 CPU 都有自己独立的高速缓存,所以每个 CPU 对应一个 per_cpu_pageset 结构,用于管理这个 CPU 高速缓存中的冷热页。

所谓的热页就是已经加载进 CPU 高速缓存中的物理内存页,所谓的冷页就是还未加载进 CPU 高速缓存中的物理内存页,冷页是热页的后备选项

 在 Linux 内核中,系统会经常请求和释放单个页面。如果针对每个 CPU,都为其预先分配一个用于缓存单个内存页面的高速缓存页列表,用于满足本地 CPU 发出的单页内存请求,就能提升系统的性能

当内核尝试从 pcplist 中获取一个物理内存页时,会首先获取运行当前进程的 CPU 对应的高速缓存列表 pcplist。然后根据指定的具体页面迁移类型 migratetype 获取对应迁移类型的 pcplist。

pcplist 中缓存的内存页面其实全部来自于伙伴系统,当 pcplist 中的页面数量 count 为 0 (表示此时 pcplist 里没有缓存的页面)时,内核会调用 rmqueue_bulk 从伙伴系统中获取 batch 个物理页面添加到 pcplist,从伙伴系统中获取页面的过程参照本文 "3. 伙伴系统的内存分配原理" 小节中的内容。

随后内核会将 pcplist 中的第一个物理内存页从链表中摘下返回,count 计数减一。

从这里我们看到伙伴系统回收内存的流程和伙伴系统分配内存的流程是一样的,在最开始首先都会检查本次释放或者分配的是否只是一个物理内存页(order = 0),如果是则直接释放到 CPU 高速缓存列表 pcplist 中。如果不是则将内存释放回伙伴系统中。

这里笔者需要强调的是,内核只会将 UNMOVABLE,MOVABLE,RECLAIMABLE 这三种页面迁移类型放入 CPU 高速缓存列表 pcplist 中,其余的迁移类型均需释放回伙伴系统。

如果当前 pcplist 中的页面数量 count 超过了规定的水位线 high 的值,说明现在 pcplist 中的页面太多了,需要从 pcplist 中释放 batch 个物理页面到伙伴系统中。这个过程称之为惰性合并。 

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/710921.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

《计算机网络--自顶向下方法》第四章--网络层:数据平面

4.1网络层概述 每台路由器的数据平面的主要作用是从其输入链路向其输出链路转发数据报;控制平面的主要作用是协调这些本地路由器转发动作,使得数据报沿着源和目的地主机之间的路由器路径进行端到端传送 路由器具有截断的协议栈,即没有网络层…

[RISC-V]Milk-V开发板 i2c测试oled及波形输出

I2C3 引脚图 修改i2c3复用功能 build\boards\cv180x\cv1800b_sophpi_duo_sd\u-boot\cvi_board_init.c //I2C3 pin6 7 PINMUX_CONFIG(SD1_CMD, IIC3_SCL); PINMUX_CONFIG(SD1_CLK, IIC3_SDA);扫描I2C3上的设备 [rootcvitek]~# i2cdetect -y -r 3 0 1 2 3 4 5 6 7 8 9 a b c …

大数据Doris(五十四):BACKUP数据备份原理和语法

文章目录 BACKUP数据备份原理和语法 一、BACKUP数据备份原理 1、快照及快照上传 2、元数据准备及上传 二、BACKUP数据备份语法 BACKUP数据备份原理和语法 通过Doris数据导出的各种方式我们可以将Doris中的数据进行备份,除了export方式之外,Doris 还…

高压线路零序电流方向保护程序逻辑原理(二)

二、零序电流方向保护的采样中断服务程序 零序电流方向保护与其他微机保护的采样中断服务程序相同,均有电压求和自检和电流求和自检及相电流差突变量起动元件DI1。零序电流方向保护的采样中断服务程序中最突出的问题是通过3U。突变量元件来实现闭锁保护&#xff0c…

使用Dependency Walker和Process Explorer排查程序缺少ucrtbase.dll等运行时库以及报0xC000007B错误问题总结

目录 1、问题描述 2、分析软件问题的常用分析工具 3、使用Dependency Walker排查启动程序时报找不到ucrtbase.dll、vcruntime140.dll等运行时库的问题 3.1、使用Dependency Walker查看exe程序的库依赖关系,排查找不到ucrtbase.dll、vcruntime140.dll库问题 3.2…

华为OD机试真题 Python 实现【相对开音节】【2022Q4 100分】,附详细解题思路

一、题目描述 相对开音节构成的结构为辅音元音(aeiou)辅音(r除外)e,常见的单词有life,time,woke,coke,joke,note,nose,communicate,use,gate,same,late等。 给定一个字符串,以空格…

关于 Camera 产品的功能分析

1、问题背景 通过最近做的一些项目,发现 Tuning 一款 Camera 产品前,要考虑到的事情有很多,不是简单的点亮,按要求调完效果就结束了。 从目前的经验来看,准备工作做的越充分,后期遇到的问题也就越少。本文…

MQTT(三)MQTTX工具使用

MQTTX工具使用 MQTTX是一种开源的MQTT客户端工具,用于帮助开发人员测试和调试MQTT(Message Queuing Telemetry Transport)协议。MQTT是一种轻量级的消息传输协议,通常用于物联网设备和应用程序之间的通信。 MQTTX提供了一个用户…

MySQL中常用查看锁和事务的SQL语句

MySQL中常用查看锁和事务的SQL语句 当我们在使用MySQL数据库时,了解如何查看锁和事务的状态是非常重要的。这些信息可以帮助我们调试和优化数据库性能,以及解决并发访问的问题。在本博客中,我将介绍一些常用的MySQL查询语句,用于查…

文心一言 VS 讯飞星火 VS chatgpt (53)-- 算法导论6.2 5题

五、MAX-HEAPIFY的代码效率较高,但第 10 行中的递归调用可能例外,它可能使某些编译器产生低效的代码。请用循环控制结构取代递归,重写 MAX-HEAPIFY代码。 文心一言: 以下是使用循环控制结构取代递归调用的 MAX-HEAPIFY 代码&…

搜索引擎——倒排索引

搜索引擎——倒排索引 什么是倒排索引 倒排索引(Inverted Index)是一种用于快速查找文档的数据结构,常用于搜索引擎中。与正向索引(Forward Index)相反,倒排索引是基于单词或术语来组织文档的索引。 倒排…

【Andv】Andv图片上传组件:

文章目录 一、效果图:二、实现思路:三、实现代码:【1】components/AndvImageUpload/index.vue【2】main.js【3】使用: 一、效果图: 二、实现思路: 把andv的上传组件封装起来,并全局注册,这样别的页面不用引入就可以直接使用 三、实现代码: 【…

【复习《剑指Offer》13-17题】【每天40分钟,我们一起用50天刷完 (剑指Offer)】第十三天 13/50

专注 效率 记忆 预习 笔记 复习 做题 欢迎观看我的博客,如有问题交流,欢迎评论区留言,一定尽快回复!(大家可以去看我的专栏,是所有文章的目录)   文章字体风格: 红色文字表示&#…

k8s中 分布式存储器longhorn的安装

条件: [rootk8s-master longhorn]# kubectl get nodes -o wide #K8S集群一个 NAME STATUS ROLES AGE VERSION INTERNAL-IP EXTERNAL-IP OS-IMAGE KERNEL-VERSION CONTAINER-RUNTIME k8s-master …

ABP recall:ABP modularization

为什么recall,因为之前有个task涉及到项目的配置问题,完全不知道配置文件到底在干什么,重新结合 ABP的模块化理解一下。 之前对模块化的理解:结合ABP VNext来理解DDD_abp.vnext和abp哪个生产ddd_董厂长的博客-CSDN博客 再深入一…

Jmeter之Bean shell使用详解

目录 一、什么是Bean Shell 二、Jmeter有哪些Bean Shell 三、BeanShell的用法 四、Bean Shell常用内置变量 总结: 一、什么是Bean Shell BeanShell是一种完全符合Java语法规范的脚本语言,并且又拥有自己的一些语法和方法;BeanShell是一种松散类型的脚本语言(这…

react环境

目录 一、React环境安装 1. vite集成 2. 官方脚手架 二、React特点 三、基础语法 1. JSX语法 2. 组件的写法——类组件/方法 3. 循环渲染 4. 条件渲染 5. css样式 6. 响应式状态——useState 一、React环境安装 1. vite集成 npm init vitelatest> 创建项目名>…

数分面试题-AB测试

目录标题 1、ABtest实验目的2、A/Btest是什么?意义/目的/作用3、A/Btest工作原理4、A/B test流程(面试喜欢问)5、一个实际的ABtest案例6、AB实验的注意事项6.1 网络效应:一个用户影响另一个用户6.2 学习效应:新奇效应6…

前端开发:JS中堆和栈的区别

前言 在前端实际开发中,有关JS原生的堆和栈也是很重要的点,关于底层和原理的掌握使用,尤其是在性能优化方面甚为重要。众所周知,JS的变量都是存放在内存中的,而且内存给变量开辟了两块区域,即堆区域和栈区域…

头条_signature

文章目录 0x0目标url0x1接口分析0x2定位0x3调试分析0x4扣代码,补环境0x5运行测试0x6相关代码 0x0目标url aHR0cHM6Ly93d3cudG91dGlhby5jb20 0x1接口分析 打开开发者工具,在xhr下可以找到相关接口 _signature由js算法生成 0x2定位 这里通过全局搜索的…