一篇图解Linux内存碎片整理

news2024/11/26 7:59:34

我们知道物理内存是以页为单位进行管理的,每个内存页大小默认是4K(大页除外)。申请物理内存时,一般都是按顺序分配的,但释放内存的行为是随机的。随着系统运行时间变长后,将会出现以下情况:

要解决这个问题也比较简单,只需要把空闲的内存块移动到一起即可。如下图所示:

网络上有句很有名的话:理想很美好,现实很骨感

内存整理也是这样,看起来很简单,但实现起来就不那么简单了。因为在内存整理后,需要修正进程的虚拟内存与物理内存之间的映射关系。如下图所示:

但由于 Linux 内核有个名为 内存页反向映射 的功能,所以内存整理就变得简单起来。

接下来,我们将会分析内存碎片整理的原理与实现。

 资料直通车:Linux内核源码技术学习路线+视频教程内核源码

学习直通车:Linux内核源码内存调优文件系统进程管理设备驱动/网络协议栈

内存碎片整理原理

内存碎片整理的原理比较简单:在内存碎片整理开始前,会在内存区的头和尾各设置一个指针,头指针从头向尾扫描可移动的页,而尾指针从尾向头扫描空闲的页,当他们相遇时终止整理。下面说说内存随便整理的过程(原理参考了内核文档):

  1. 初始时内存状态:

在上图中,白色块表示空闲的内存页,而红色块表示已分配出去的内存页。在初始状态时,内存中存在多个碎片。如果此时要申请 3 个地址连续的内存页,那么将会申请失败。

  1. 内存碎片整理扫描开始:

头部指针从头扫描可移动页,而尾部指针从从尾扫描空闲页。在整理时,将可移动页的内容复制到空闲页中。复制完成后,将可移动内存页释放即可。

  1. 最后结果:

经过内存碎片整理后,如果现在要申请 3 个地址连续的内存页,就能申请成功了。

内存碎片整理实现

接下来,我们将会分析内存碎片整理的实现过程。

注:本文使用的是 Linux-2.6.36 版本的内存

1. 内存碎片整理时机

当要申请多个地址联系的内存页时,如果申请失败,将会进行内存碎片整理。其调用链如下:

alloc_pages_node()
└→ __alloc_pages()
   └→ __alloc_pages_nodemask()
      └→ __alloc_pages_slowpath()
         └→ __alloc_pages_direct_compact()

当调用 alloc_pages_node() 函数申请多个地址连续的内存页失败时,将会触发调用 __alloc_pages_direct_compact() 函数来进行内存碎片整理。我们来看看 __alloc_pages_direct_compact() 函数的实现:

static struct page *
__alloc_pages_direct_compact(gfp_t gfp_mask, 
                             unsigned int order, 
                             struct zonelist *zonelist, 
                             enum zone_type high_zoneidx, 
                             nodemask_t *nodemask, 
                             int alloc_flags,
                             struct zone *preferred_zone,
                             int migratetype, 
                             unsigned long *did_some_progress)
{
    struct page *page;

    // 1. 如果申请一个内存页,那么就没有整理碎片的必要(这说明是内存不足,而不是内存碎片导致)
    if (!order || compaction_deferred(preferred_zone))
        return NULL;

    // 2. 开始进行内存碎片整理
    *did_some_progress = try_to_compact_pages(zonelist, order, gfp_mask, nodemask);

    if (*did_some_progress != COMPACT_SKIPPED) {
        ...
        // 3. 整理完内存碎片后,继续尝试申请内存块
        page = get_page_from_freelist(gfp_mask, nodemask, order, zonelist, 
                                      high_zoneidx, alloc_flags, preferred_zone, 
                                      migratetype);
        if (page) {
            ...
            return page;
        }
        ...
    }

    return NULL;
}

__alloc_pages_direct_compact() 函数是内存碎片整理的入口,其主要完成 3 个步骤:

  • 先判断申请的内存块是否只有一个内存页,如果是,那么就没有整理碎片的必要(这说明是内存不足,而不是内存碎片导致)。
  • 如果需要进行内存碎片整理,那么调用 try_to_compact_pages() 函数进行内存碎片整理。
  • 整理完内存碎片后,调用 get_page_from_freelist() 函数继续尝试申请内存块。

2. 内存碎片整理过程

由于内存碎片整理的具体实现在 try_to_compact_pages() 函数中进行,所以我们继续来看看 try_to_compact_pages() 函数的实现:

unsigned long
try_to_compact_pages(struct zonelist *zonelist, int order, gfp_t gfp_mask,
                     nodemask_t *nodemask)
{
    ...
    // 1. 遍历所有内存区(由于内核会把物理内存分成多个内存区进行管理)
    for_each_zone_zonelist_nodemask(zone, z, zonelist, high_zoneidx, nodemask) {
        ...
        // 2. 对内存区进行内存碎片整理
        status = compact_zone_order(zone, order, gfp_mask);
        ...
    }

    return rc;
}

可以看出,try_to_compact_pages() 函数最终会调用 compact_zone_order() 函数来进行内存碎片整理。我们只能进行来分析 compact_zone_order() 函数:

static unsigned long
compact_zone_order(struct zone *zone, int order, gfp_t gfp_mask)
{
    struct compact_control cc = {
        .nr_freepages = 0,
        .nr_migratepages = 0,
        .order = order,
        .migratetype = allocflags_to_migratetype(gfp_mask),
        .zone = zone,
    };
    INIT_LIST_HEAD(&cc.freepages);
    INIT_LIST_HEAD(&cc.migratepages);

    return compact_zone(zone, &cc);
}

到这里,我们还没有看到内存碎片整理的具体实现(调用链可真深啊 ^_^!),compact_zone_order() 函数也是构造了一些参数,然后继续调用 compact_zone() 来进行内存碎片整理:

static int compact_zone(struct zone *zone, struct compact_control *cc)
{
    ...
    while ((ret = compact_finished(zone, cc)) == COMPACT_CONTINUE) {
        ...
        // 1. 收集可移动的内存页列表
        if (!isolate_migratepages(zone, cc))
            continue;
        ...
        // 2. 将可移动的内存页列表迁移到空闲列表中
        migrate_pages(&cc->migratepages, compaction_alloc, (unsigned long)cc, 0);
        ...
    }
    ...
    return ret;
}

在 compact_zone() 函数里,我们终于看到内存碎片整理的逻辑了。compact_zone() 函数主要完成 2 个步骤:

  • 调用 isolate_migratepages() 函数收集可移动的内存页列表。
  • 调用 migrate_pages() 函数将可移动的内存页列表迁移到空闲列表中。

这两个函数非常重要,我们分别来分析它们是怎么实现的。

isolate_migratepages() 函数

isolate_migratepages() 函数用于收集可移动的内存页列表,我们来看看其实现:

static unsigned long
isolate_migratepages(struct zone *zone, struct compact_control *cc)
{
    unsigned long low_pfn, end_pfn;
    struct list_head *migratelist = &cc->migratepages;
    ...

    // 1. 扫描内存区所有的内存页
    for (; low_pfn < end_pfn; low_pfn++) {
        struct page *page;
        ...

        // 2. 通过内存页的编号获取内存页对象
        page = pfn_to_page(low_pfn);
       ...

        // 3. 判断内存页是否可移动内存页,如果不是可移动内存页,那么就跳过
        if (__isolate_lru_page(page, ISOLATE_BOTH, 0) != 0)
            continue;

        // 4. 将内存页从 LRU 队列中删除
        del_page_from_lru_list(zone, page, page_lru(page));

        // 5. 添加到可移动内存页列表中
        list_add(&page->lru, migratelist); 
        ...
        cc->nr_migratepages++;
        ...
    }
    ...
    return cc->nr_migratepages;
}

isolate_migratepages() 函数主要完成 5 个步骤,分别是:

  • 扫描内存区所有的内存页(与内存碎片整理原理一致)。
  • 通过内存页的编号获取内存页对象。
  • 判断内存页是否可移动内存页,如果不是可移动内存页,那么就跳过。
  • 将内存页从 LRU 队列中删除,这样可避免被其他进程回收这个内存页。
  • 添加到可移动内存页列表中。

当完成这 5 个步骤后,内核就收集到可移动的内存页列表。

migrate_pages() 函数

migrate_pages() 函数负责将可移动的内存页列表迁移到空闲列表中,我们来分析一下其实现过程:

int migrate_pages(struct list_head *from, new_page_t get_new_page,
                  unsigned long private, int offlining)
{
    ...

    for (pass = 0; pass < 10 && retry; pass++) {
        retry = 0;

        // 1. 遍历可移动内存页列表
        list_for_each_entry_safe(page, page2, from, lru) {
            ...
            // 2. 将可移动内存页迁移到空闲内存页中
            rc = unmap_and_move(get_new_page, private, page, pass > 2, offlining);
            switch(rc) {
            case -ENOMEM:
                goto out;
            case -EAGAIN:
                retry++;
                break;
            case 0:
                break;
            default:
                nr_failed++;
                break;
            }
        }
    }
    ...
    return nr_failed + retry;
}

migrate_pages() 函数的逻辑很简单,主要完成 2 个步骤:

  • 遍历可移动内存页列表,这个列表就是通过 isolate_migratepages() 函数收集的可移动内存页列表。
  • 调用 unmap_and_move() 函数将可移动内存页迁移到空闲内存页中。

可以看出,具体的内存迁移过程在 unmap_and_move() 函数中实现。我们来看看 unmap_and_move() 函数的实现:

static int
unmap_and_move(new_page_t get_new_page, unsigned long private,
               struct page *page, int force, int offlining)
{
    ...
    // 1. 从内存区中找到一个空闲的内存页
    struct page *newpage = get_new_page(page, private, &result);
    ...

    // 2. 解开所有使用了当前可移动内存页的进程的虚拟内存映射(涉及到内存页反向映射)
    try_to_unmap(page, TTU_MIGRATION|TTU_IGNORE_MLOCK|TTU_IGNORE_ACCESS);

skip_unmap:
    // 3. 将可移动内存页的数据复制到空闲内存页中
    if (!page_mapped(page))
        rc = move_to_new_page(newpage, page, remap_swapcache);
    ...
    return rc;
}

由于 unmap_and_move() 函数的实现比较复杂,所以我们对其进行了简化。可以看出,unmap_and_move() 函数主要完成 3 个工作:

  • 从内存区中找到一个空闲的内存页。根据内存碎片整理算法,会从内存区最后开始扫描,找到合适的空闲内存页。
  • 由于将可移动内存页迁移到空闲内存页后,进程的虚拟内存映射将会发生变化。所以,这里要调用 try_to_unmap() 函数来解开所有使用了当前可移动内存页的映射。
  • 调用 move_to_new_page() 函数将可移动内存页的数据复制到空闲内存页中。在 move_to_new_page() 函数中,还会重新建立进程的虚拟内存映射,这样使用了当前可移动内存页的进程就能够正常运行。

至此,内存碎片整理的过程已经分析完毕。

不过细心的读者可能发现,在文中并没有分析重新构建虚拟内存映射的过程。是的,因为重新构建虚拟内存映射要涉及到 内存页反向映射 的知识点,后续的文章会介绍这个知识点,所以这里就不作详细分析了。

总结

从上面的分析可知,内存碎片整理 是为了解决:在申请多个地址连续的内存页时,空闲内存页数量充足,但还是分配失败的情况。

但由于内存碎片整理需要消耗大量的 CPU 时间,所以我们在申请内存时,可以通过指定 __GFP_WAIT 标志位(不等待)来避免内存碎片整理过程。

 

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

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

相关文章

树莓派板载蓝牙使用

1 设置树莓派板载蓝牙 1.1 相关环境安装、配置 sudo apt-get update sudo apt-get install pi-bluetooth bluez bluez-firmware blueman1.2 树莓派蓝牙操作 参考&#xff1a; https://blog.csdn.net/guzhong10/article/details/78574577 有时候会失败&#xff0c; 可以尝试…

[附源码]SSM计算机毕业设计学校缴费系统JAVA

项目运行 环境配置&#xff1a; Jdk1.8 Tomcat7.0 Mysql HBuilderX&#xff08;Webstorm也行&#xff09; Eclispe&#xff08;IntelliJ IDEA,Eclispe,MyEclispe,Sts都支持&#xff09;。 项目技术&#xff1a; SSM mybatis Maven Vue 等等组成&#xff0c;B/S模式 M…

文件包含总结

概念 是指将已有的代码以文件形式包含到某个指定的代码中&#xff0c;从而使用其中的代码或者数据&#xff0c;一般是为了方便直接调用所需文件&#xff0c;文件包含的存在使得开发变得更加灵活和方便。 文件包含常见函数 include() // 执行到include时才包含文件&#xff…

区间信息维护与查询【线段树 】 - 原理2 线段树中的“懒操作”

区间信息维护与查询【线段树 】 - 原理2 线段树中的“懒操作” 之前我们已经说了对线段树的点更新和区间查询&#xff0c;若要求对区间中的所有点都进行更新&#xff0c;该怎么办&#xff1f; 若对区间的每个点都进行更新&#xff0c;则时间复杂度较高&#xff0c;可以引入懒…

Cocos2d-x 3D渲染技术 (三)

包围盒算法 说白了就是给物体装进一个盒子里&#xff0c;该盒子可以装下物体。目的是为了进行碰撞检测。 种类&#xff1a; 球状碰撞体立方体碰撞体胶囊碰撞体Mesh碰撞体 实现原理是OBB包围盒。 经常使用的两种碰撞算法是OBB包围盒和AABB包围盒算法。 OBB包围盒算法 方向…

JavaScript -- 01. 基础语法介绍

文章目录基础语法1 Hello World2 JS的编写位置3 基本语法3.1 多行注释3.2 单行注释3.3 区分大小写3.4 空格和换行会被忽略3.5 以分号结尾3.6 字面量3.7 变量3.8 变量的内存结构3.9 常量3.10 标识符基础语法 JS的基本语法 1 Hello World js的三种输出方式 <!DOCTYPE html&g…

精彩回顾 | 云原生系统软件的产业应用

11月18日&#xff0c;2022年第五届中国金融科技产业大会暨第四届中新&#xff08;苏州&#xff09;数字金融应用博览会“基础软件与云原生系统软件”分论坛成功举办。该论坛由由中国计算机学会CTO CLUB&#xff08;苏州&#xff09;承办&#xff0c;江苏省金融科技云原生融合创…

如何用 Python 做一个简单的翻译工具?

前言 平时经常在网上翻译一些单词&#xff0c;突发奇想&#xff0c;可不可以直接调某些免费翻译网站的接口呢&#xff1f;然后做一个图形界面的翻译小工具&#xff1f;下面开始实践 &#xff08;文末送读者福利&#xff09; 1.先找一下有哪些免费翻译的接口 百度了一下关键字…

神经架构搜索的综合调查:挑战和解决方案(二)

4 PERFORMANCE COMPARISON NAS 是一项很有前途的研究。在本节中&#xff0c;我们根据主流搜索方法 [27, 28] 对现有 NAS 的性能进行分类和比较&#xff0c;同时还根据第 3 节报告了它们使用的优化策略。这些搜索方法主要包括以下内容&#xff1a;强化学习&#xff08;RL&#…

操作系统学习笔记(Ⅲ):内存

目录 1 内存管理 1.1 内存基础知识 1.内存 2.进程运行 1.2 内存管理的概念 1.3 覆盖与交换 1.覆盖 2.交换 3.区别 1.4 连续分配管理方式 1.单一连续分配 2.固定分区分配 3.动态分区分配 1.5 动态分区分配算法 1.首次适应算法 2.最佳适应算法 3.最坏适应算法 …

网络安全与IP安全

网络安全 是指网络系统的硬件&#xff0c;软件以及系统中的数据收到的保护。 保护的基本属性为&#xff1a;机密性&#xff0c;身份认证&#xff0c;完整性和可用性&#xff1b; 基本特征&#xff1a;相对性&#xff0c;时效性&#xff0c;相关性&#xff0c;不确定性&#xf…

React项目实战之租房app项目(六)渲染房源列表axios优化封装顶部搜索栏列表找房模块之条件筛选

前言 目录前言一、地图找房模块-获取并渲染房源列表1.1 房源列表示例图1.2 实现步骤1.3 代码示例二、axios优化2.1 问题概述2.2 配置生产环境和开发环境2.3 axios优化三、封装顶部搜索导航栏四、列表找房模块-导入顶部导航栏组件五、列表找房模块-条件筛选&#xff08;上&#…

Python将Excel文件插入Mysql数据库(脚本)

目录前言最近接到一个需求&#xff0c;就是将多个Eccel文件&#xff08;表头相同&#xff1b;每个都非常大&#xff0c;约60多万行&#xff0c;每个都是&#xff01;&#xff01;&#xff09;先合并在一起&#xff0c;再做一些处理&#xff0c;但是Excel表格一个文件根本存不下…

Python矩阵乘法 二重循环实现 + 列表推式

这是python 矩阵乘法的简单例子 col 2 row 2 a [[1, 2], [3, 4]] b [[5, 6], [7, 8]] c [[0, 0], [0, 0]] “”" a b c 二维矩阵初始化 c [[0 for col in range(col)] for row in range(row)] a [[0 for col in range(col)] for row in range(row)] b [[0 for c…

Android热修复,精简学习

接入热修复 接入热修复流程如下&#xff1a; 配置开发环境在控制台创建应用在客户端创建新工程签名配置加密信息编写代码发布带有热修复功能的客户端版本 配置开发环境 在控制台创建应用 在控制台创建 mPaaS 应用。此时&#xff0c;本地还没有带签名的 APK&#xff0c;因此…

面试:插件化相关---broadcastReceiver

实现原理 1 采用的模型 Android中的广播使用了设计模式中的观察者模式&#xff1a;基于消息的发布 / 订阅事件模型因此&#xff0c;Android将广播的发送者 和 接收者 解耦&#xff0c;使得系统方便集成&#xff0c;更易扩展 2 模型讲解 模型中有3个角色&#xff1a; 消息订阅…

小程序项目结构

pages 用来存放所有小程序的页面utils 用来存放工具性质的模块(例如:格式化时间的自定义模块)app.js 小程序项目的入口文件app.json 小程序项目的全局配置文件app.wxss 小程序项目的全局样式文件project.config.json 项目的配置文件sitemap.json 用来配置小程序及其页面是否允许…

【数据结构-查找】散列表

文章目录1 线性探测法1.1 查找成功时的 ASL1.2 查找失败时的 ASL1.3 散列表的装填因子 α2 拉链法1 线性探测法 1.1 查找成功时的 ASL 查找元素 47&#xff08;散列函数&#xff1a;3&#xff09;次数&#xff1a;1查找元素 7&#xff08;散列函数&#xff1a;11&#xff09;次…

云上办公便捷、安全,就用华为云桌面

云上办公便捷、安全&#xff0c;就用华为云桌面&#xff01; 根据IDC提出的“未来工作空间”的概念&#xff0c;未来工作空间意味着将打破时空与地域的限制&#xff0c;让员工随时随地工作。未来工作空间也将成为企业整体数字化转型战略中的必要组成部分。 恰逢其时&#xff0…

kubernetes Pod详解

文章目录Pod生命周期创建和终止pod的创建过程pod的终止过程初始化容器钩子函数容器探测重启策略Pod调度定向调度NodeNameNodeSelector亲和性调度NodeAffinityPodAffinityPodAntiAffinity污点和容忍污点&#xff08;Taints&#xff09;容忍&#xff08;Toleration&#xff09;Po…