山东临沂网站推广,汕头网上推广找谁,自己开发微信小程序教程,集团网站建设活动方案1.前言 伙伴系统作为内核最基础的物理页内存分配器#xff0c;具有高效、实现逻辑简介等优点#xff0c;其原理页也尽可能降低内存外部碎片产生#xff0c;但依然无法杜绝碎片问题。外部碎片带来的最大影响就是内存足够#xff0c;但是却无法满足内存分配需求#xff0c;如… 1.前言 伙伴系统作为内核最基础的物理页内存分配器具有高效、实现逻辑简介等优点其原理页也尽可能降低内存外部碎片产生但依然无法杜绝碎片问题。外部碎片带来的最大影响就是内存足够但是却无法满足内存分配需求如下图所示 内存外部碎片导致实际占用物理页不多但是已无法申请4个页连续内存理想当中我们希望内存没有外部碎片如下图所示 内核并未为此目标设计新的内存分配算法伙伴系统足够简单和高效其选择在伙伴系统基础上根据内存使用需求进行内存分配将不可移动内存和可移动内存归类在内存碎片问题出现时尝试进行内存规整compact移动可移动的页面腾出更多连续内存如下图简述 上图中将一个页移动到另一个页的过程叫页迁移这并不是一件轻松的事情数据的拷贝、进程映射信息更改等等都很耗时并且也是个复杂逻辑这注定内存规整的过程是一个重负载的过程。事实上页迁移是内存管理的独立逻辑内核对此单独封装接口migrate_pages内存规整只是其中一个应用场景类似场景还有NUMA Balance、Memory hotplug及CMA内存等等。本文聚焦内存规整不描述内存迁移逻辑。 站在开发者角度有了内存迁移基础能力那么就有实现内存规整基础但依然有值得思考的问题比如内存规整的范围何时进行内存规整等等。 对于内存规整范围问题内核通常选择以zone为单位进行规整实际范围受到参数影响可能为zone一部分并为此封装compact_zone接口作为内存规整核心接口alloc_contig_range例外。 对于何时触发问题属于触发策略和场景问题内核当前引入直接内存规整、被动内存规整、预应性内存规整及主动内存规整四种策略场景这些场景最终都会通过compact_zone进行内存规整但是他们触发的时机不同、目标不同、规整范围不同、规整退出条件不同规整强度不同等等。基础能力和策略分离设计是内核的基础设计理念。 如上图所示内存规整是基于内存迁移实现的功能内核根据策略在不同实际触发内存规整用于缓解内存外部碎片问题可以分层分析看待内存规整。 2.内存规整场景 前言中已说明内核当前触发内存规整的策略有四种为便于查看和直观理解优先罗列四种场景特点见下表 上述表格中各规整策略详见2.1~2.4节描述compact_control各种含义详见3.1节描述。 FAQ 1直接内存规整较为特殊内存分配过程中如果触发直接内存规整依然无法分配内存那么有可能循环调用并且提高内存规整的级别因此出现首次和重试之分。 2规整页类型中不包含不可回收页除非通过sysctl_compact_unevictable_allowed进行设置。 3内存规整中有一个特例就是alloc_contig_range函数该函数用于分配指定地址区域内存若这部分内存被占用会尝试对这段内存进行规整迁移其并非针对zone的规整而是针对指定内存区域的规整它的规整类型与主动内存规整类似其实现核心是内存规整机制本文不对此逻辑进行说明。 2.1 直接内存规整direct compaction 2.1.1 直接内存规整触发条件 伙伴系统分配内存时会先以low水线为基准调用get_page_from_freelist函数尝试进行内存分配如果失败则会进入慢速内存分配流程即__alloc_pages_slowpath函数我们对此函数逻辑稍作删减内容如下 慢速内存分配会尝试唤醒kswapd进行内存回收但并不会等待内存回收的结果而是直接先调用get_page_from_freelist函数尝试内存分配但这次不同的是使用min水线进行尝试如果依然失败那么将会根据gfp标识确认当前分配是否支持直接内存回收若支持将会调用__alloc_pages_direct_compact尝试第一次直接内存规整以及内存分配。如果依然失败则进入唤醒kswapd、get_page_from_freelist、__alloc_pages_direct_reclaim及__alloc_pages_direct_compact循环调用流程里面来当然这之中存在众多条件判断随时可能返回页分配失败、页分配成功、重试甚至是触发OOM。值得注意的是在慢速内存分配逻辑中首次调用直接内存规整时其优先级设置为INIT_COMPACT_PRIORITY这将影响内存规整触发页迁移的类型比如INIT_COMPACT_PRIORITY对应的就是MIGRATE_ASYNC即异步迁移类型代表页迁移时不会阻塞当然这样带来的效果就是规整或迁移的能力较弱。慢速内存分配逻辑中后续直接内存规整调用其规整优先级可能会逐步降低越低对应规整强度越高从而提升内存规整效用但是内存规整可能变为阻塞规整这是相互对应逻辑。 通过上述描述可以初步了解直接内存规整起到的作用也可以感受到内核内存分配进入到慢速分配逻辑后性能的代价。 另一方面直接内存规整实际是由于伙伴系统无法分配内存时触发因此直接内存规整目标并也并非消除整个zone的外部碎片而只是通过内存规整迁移出目标阶连续内存。 2.1.2 直接内存规整逻辑说明 __alloc_pages_direct_compact函数是直接内存规整运行入口该函数核心内容如下 try_to_compact_pages函数将会进一步调用compact相关流程进行规整规整完成后调用get_page_from_freelist进行内存分配。try_to_compact_pages核心代码逻辑如下 try_to_compact_pages函数核心遍历规定范围内zone针对每个zone调用compaction_deferred确认其是否合适进行规整若合适进一步调用compact_zone_order函数进行规整规整成功则直接内存规整将会直接返回。在try_to_compact_pages函数中我们重点说明一下zone延迟判断逻辑这部分逻辑同样适用于后续kcompactd对于zone的判断。 2.1.2.1 延迟规整 compaction_deferred函数用于判断当前zone是否需要进行延迟处理延迟的目的是避免频繁或无效的内存规整其引入两个机制用于延迟一个是内存规整失败阶判断另一个是内存规整延迟次数判断这更像一种计时器。 A) 规整失败阶的判断compact_order_failed 如果当前规整阶大于等于zone的最大规整失败阶那么代表当前再去规整失败的可能性很高建议延迟对当前zone规整。 B) 内存规整延迟次数超阈值判断 如果失败阶判断满足那么会对延迟次数进行判断compact_considered记录了当前zone延迟次数compaction_deferred每次调用时compact_considered都会累加如果其小于阈值那么建议zone不进行规整标识近期可能已经进行过规整。 可以想象到延迟判断的这些参数会动态变化实际如上图所示。 A) 当内存规整成功时调用compaction_defer_reset函数清空compact_considered延迟计数清空compact_defer_shift延迟计数阈值defer_limit compact_defer_shift 1同时如果当前order大于等于compact_defer_shift 则更新compact_order_failed最大规整失败阶。 总结当规整成功时会降低此zone延迟标准让后续对zone规整判断变得更为容易。 B) 当内存规整失败时依然会将compact_considered清零若order大于更新更新compact_order_failed最大规整失败阶增大compact_defer_shift延迟计数阈值。 总结当规整失败时会增大此zone延迟标准让后续对zone规整将会延迟更多次。 通过上述延迟方案确保对于某个zone不做重复规整、不做成功率低的规整当一次对zone规整失败时内核将会尽量给与zone足够时间然后再进行尝试。zone延迟判断机制适用于直接内存规整以及kcompactd内存规整机制这两种机制对于耗时较为敏感其它场景内存规整通常不需要此机制。 2.1.2.2 capture_control说明 再次回到try_to_compact_pages函数一个zone在通过延迟判断后将会调用compact_zone_order函数该函数核心是定义compact_control并调用compact_zone完成规整。但是这里引入了一个很有意思的机制capture_control因此需要额外进行说明这笔修改可见如下内容 通常的逻辑通过内存规整迁移出目标阶内存块再进行内存分配而为了提效capture_control的思路则是在内存规整的过程中就将内存分配出来只不过这个分配更像是截胡在直接内存规整的过程中若发生内存释放则在伙伴系统内存释放逻辑中截胡合适的内存下面详细说明这个过程。 在compact_zone_order函数会填充capture_control变量并将其赋值给当前进程上下文标志着当前进程进入到直接规整逻辑里面。可以想象在内存规整过程中涉及内存释放此时capture开始行动代码如下 内存释放流程中通过compaction_capture尝试捕获已释放的内存compaction_capture函数代码实现如下 释放内存阶必须与直接内存规整阶相等才有可能捕获同时需要强调如果当前释放的是MIGRATE_MOVABLE类型页尽量不去捕获避免污染可移动页面因为触发直接规整的有可能是不可移动的内存请求。 2.1.3 直接内存规整特点 1指定了规整目标阶降低规整范围和难度 2迁移页扫描器和空闲页扫描器使用快速扫描能力 3其指定highest_zoneidx和目标阶因此存在水线判断。 4direct_compaction设置直接规整标识 直接内存规整优先级COMPACT_PRIO_ASYNC逐步升高内存规整强度将会增强内容如下 5随着规整失败规整模式MIGRATE_ASYNC变为MIGRATE_SYNC_LIGHT即直接内存规整可能是不阻塞也可能是阻塞模式 6随着规整失败规整范围从根据上次规整结果制定范围变为完整zone地址范围 7随着规整失败pageblock将会被重新扫描不会根据标记skip逐步加强规整强度 8随着规整失败空闲页扫描器将变得严格空闲页必须来自于MIGRATE_MOVABLE和MIGRATE_CMA可移动的页面 上述compact_control结构体参数含义见3.1节 2.2 被动内存规整kcompactd 内核在启动过程中会调kcompactd_init函数为每个node启动一个kcompactd内核线程并且kcompactd线程会运行在与node相对应的CPU核上在合适的时机kcompactd将会被唤醒进行内存规整这就是被动内存规整逻辑。一个特殊场景是若开启proactive compaction功能那么kcompactd会被周期性唤醒。 本节主要从三个方面说明分别是kcompactd唤醒条件、kcompactd运行条件、以及kcompactd内存规整特点kcompactd被唤醒不一定会进行内存规整。 2.2.1 kcompactd唤醒条件 内存规整模块向内核提供wakeup_kcompactd口用于唤醒node对应的kcompactd线程内核中kcompactd唤醒与kswapd强相关总结如下场景会被被动唤醒 FAQ这里指的触发内存规整指的是调用wakeup_kcompactd函数未必真的进行内存规整wakeup_kcompactd还存在诸多判断 2.2.1.1 kswapd运行前触发内存规整 当内存分配失败时经各种判断后会进⼊内存慢速分配过程此时伙伴系统将尝试唤醒内存回收在这个过程中有如下关键代码 上述代码为未唤醒kswapd前进行内存规整的条件判断其意图如下 1kswapd内存回收失败多次 2根据pgdat_balanced函数判断当前水位安全即存在⾜够可⽤内存并且未出现”偷“内存情况本质在于当前内存无法分配的原因并非低内存此时内存回收可能已经无法解决此问题时wakeup_kswapd函数将会提前进行内存规整。这里还需要说明的是内存分配指定不支持直接内存回收时上述逻辑才能生效这是因为若支持直接内存规整则可以借助直接内存回收来进行改善并且通常直接内存规整有更好的性能表现。 2.2.1.2 kswapd运行中触发内存规整 watermark_boost_factor导致的内存规整归类为kswapd运行中触发内存规整稍有牵强不过其确实是在kswapd内存规整核心逻辑中触发。这里简单介绍⼀下watermark_boost_factor特性当分配内存时如果在对应migrate type上没有分配到内存那么系统将会从fall_back的migrate type进行内存分配有时将其叫做”偷“由于分配了不匹配迁移类型的内存内核会认为这可能存在外部碎片的风险所以当出现这种”偷“时内核会提前进行内存回收及规整从而降低后续”偷“行为的发生避免内存碎片问题提升内存分配的效率这就是watermark_boost_factor特性。 steal_suitable_fallback函数是从其它迁移类型上分配内存的核心逻辑此函数中会设置⼀个ZONE_BOOSTED_WATERMARK标志位这个标志位只能被kswapd清除伙伴系统在内存分配成功后如果发现ZONE_BOOSTED_WATERMARK被置位将会唤醒kswapd线程。 kswapd函数通过调用balance_pgdat进行内存回收而balance_pgdat函数会在整体内存回收结束后尝试唤醒kcompactd线程当然前提是本次内存回收是boosted属性的内存回收。 boost特性引起内存规整其规整阶为pageblock对应阶在后续compact_zone函数介绍中将会说明其影响。上述仅仅简单描述watermark_boost_factor特性其还涉及到内存回收时boost导致水线提升等特性由于其与内存回收关系较大本文不深入分析大家可通过如下合入记录进⼀步了解 2.2.1.3 kswapd运行后触发内存规整 kswapd内核线程每次在内存回收完毕后将会调⽤kswapd_try_to_sleep尝试休眠此函数会在休眠前调用reset_isolation_suitable函数清空迁移扫描器和空闲页扫描器扫描过程中产生的缓存数据比如某个pageblock是否需要跳过信息等等随后调用wakeup_kcompactd函数尝试进行内存规整。 简单说每次kswapd运行完成后都会尝试唤醒内存规整线程但是内存规整线程是否真的需要运行其有复杂的判断逻辑。 2.2.2 kcompactd运行条件 当kcompactd线程运行时若当前是非预应性规整详见2.3节那么将会调用 kcompactd_do_work函数进行被动内存规整。kcompactd_do_work函数会遍历当前node的所有zone进行合法性判断若符合条件则调用compact_zone针对zone进⾏内存规整其对zone的判断逻辑如下 根据上述代码可以总结如下判断逻辑 A. zone是否包含有效物理页若不包含不需要规整 B. 当前规整目标阶是否大于等于之前规整失败的最小阶若大于不需要规整 C. 是否需要延迟规整若延迟次数超过阈值则须规整否则不规整避免无效规整 D. 当前内存水线是否满足内存申请若满足则不规整 E. 若当前order大于3同时不满足内存分配则评估其内存碎片程度若小于阈值则不规整其中populated_zone函数仅仅判断当前zone是否存在有效物理页其它逻辑在compaction_deferred和compaction_suitable函数中实现。 2.2.2.1 延迟规整 在直接内存规整中已经介绍了延迟判断的逻辑对于kcompactd同样使用下述代码可以看到其对延迟参数的更新逻辑。 如果comapct_zone返回值是由于扫描器相遇导致一轮扫描结束而非规整目标达成导致代表规整并未达成目标同样按照失败处理。 2.2.2.2 水线判断 compaction_suitable函数一方面通过__compaction_suitable判断水线是否满足当前order阶内存分配要求若满足则当前zone不需要规整另一方面通过fragmentation_index函数获取当前zone对于order阶内存块碎片程度评估如果认为碎片程度不高则不进行规整。先来分析__compaction_suitable函数此函数用于评估当前水线是否满足内存分配若不满足则评估水线是否满足内存规整过程中内存占用需求关键实现如下 此函数中调用两次__zone_watermark_ok进行水线判断__zone_watermark_ok函数功能是判断当前zone在分配order阶内存块后水线是否达标。第一次水线检查成功代表当前zone可以满足内存分配诉求因此当前zone不用规整。若第一次调用失败则尝试第二次判断第二次水线判断的目的是确认内存规整过程中是否可能存在水线问题因为内存规整过程中需要order阶空闲页用于内存迁移。因此第二次水线的判断仅对0阶内存判断因为迁移过程中申请空闲页都是单页另一方面watermark增加order阶内存块代表迁移内存占用但是需要注意的是理论上迁移只需要order阶大小的内存但是实际watermark增加了2倍order阶大小的内存compact_gap函数这是由于实际内存规整过程中由于迁移页扫描器可能扫描出大于order阶待迁移内存因此空闲页扫描器也会占用大于order阶空闲页为了确保评估的安全性改为2倍order阶内存这部分说明在compact_gap函数内部注释中有所描述。 2.2.2.3 各阶碎片评分判断 fragmentation_index函数用于获取目标阶碎片程度评价从而评估当前内存无法分配的原因是由于低内存还是外部碎片导致。 其中fill_contig_page_info会遍历zone内存统计当前空闲页以及对应order阶空闲区域数量此函数下文将会详细介绍最后通过__fragmentation_index函数和上述遍历空闲页获得的相关信息进行计算评估。代码中计算公式如下 先明确info-free_blocks_total代表当前zone中各个order阶内存区域数量info-free_pages代表当前zone中空闲页的数量requested代表order阶对应页数量。我们命名info-free_pages/requested为target_order_blocks在当前空闲页状态下若不存在内存碎片问题这种理想状态下拥有多少个order阶内存块现在重新简化公式即可获得如下描述 极限状态info-free_blocks_total非常大时意味着严重的内存碎片问题上述值趋近于1反之0代表内存不足问题。__fragmentation_index函数引入1000这个数值参与运算是为了避免小数导致返回值不易判断现在引入此值后使函数返回值落入-10000~1000范围中其中-1000场景较为特殊其代表当前zone满足内存分配需求但是在此之前却通过了上文中__compaction_suitable函数的判断当前-1000返回值实际在代码中似乎并未被使用。重点还是回到0~1000返回值那么数值越大代表对于当前order阶内存块而言碎片程度越高难以分配。 再回到compaction_suitable函数有两个关注点一个是对于内存分配阶大于3的申请才会利用fragmentation_index进行额外判断对于较小内存需求评估其内存碎片程度意义降低例如单页内存分配伙伴系统基本分配单元就不涉及内存外部碎片问题另一个是fragmentation_index返回值需要和sysctl_extfrag_threshold阈值进行比较如果小于阈值则不进行规整此值通过/proc/sys/vm/extfrag_threshold进行设置。 FAQ基于上述逻辑内核中可以通过/sys/kernel/debug/extfrag/extfrag_index文件节点查看各个阶外部碎片评估数据其数值对1000取余为小数越趋近于1代表当前order阶内存碎片程度高相关代码如下 2.2.3 kcompactd规整特点 内存规整的特点还是需要从其配置compact_control来进行说明下述代码为kcompactd规整CC设置。 1指定了规整目标阶降低规整范围和难度 2迁移页扫描器和空闲页扫描器使用快速扫描能力 3其指定highest_zoneidx和目标阶因此存在水线判断。 4规整模式为MIGRATE_SYNC_LIGHT轻度同步模式会阻塞 5规整范围从根据上次规整结果继续规整 6pageblock将会根据标记选择跳过避免频繁扫描 上述compact_control结构体参数含义见3.1节 2.3 预应性内存规整proactive compaction 这属于新增内存规整特性其最初目的是降低大页分配延迟通过大页内存块碎片程度决策当前是否启动内存规整提前减少内存碎片提升大页分配性能。以下链接对此特性做了原理性说明 https://lwn.net/Articles/817905/ 实际最终代码与上文的最初设想已不相同下文将依据代码说明代码合⼊记录如下 https://lore.kernel.org/all/20200616204527.19185-1-niguptanvidia.com/T/#u 2.3.1 触发预应行规整 预应性规整并非独立存在其融⼊kcompactd内核线程但又与kcompactd原有功能互斥。关键代码如下 上述代码总结如下 1若设置预应性规整kcompactd将会每500msHPAGE_FRAG_CHECK_INTERVAL_MSEC宏设置唤醒判断当前是否需要进行规整 2若设置预应性规整则会跳过kcompactd原有逻辑调用预应性规整判断及规整逻辑 3预应性规整触发的依据是根据整个node的碎片化程度决策 4预应性规整会在规整前后统计碎片得分若得分增加达标碎片问题未解决那么会增加kcompactd唤醒周期避免频繁无效的预应性规整 5每轮循环后均会关闭预应性规整这是考虑到在某些嵌入式场景下并不需要频繁的唤醒并判断将启动预应性规整策略交给用户层如下合入增加了这个功能 https://lore.kernel.org/all/1627653207-12317-1-git-send-emailcharante codeaurora.org/T/#u 预应性规整在内核中引入vm.compaction_proactiveness文件节点可以向其写入0~100数值其用于指定预应性规整的积极性数值越大积极性越高见2.3节如果设置为0相当于关闭设置此数值时pgdat-proactive_compact_trigger将会被设置为true预应性内存规整功能打开此节点实现不过多描述。 2.3.2 碎片程度评估 预应性内存规整的碎片化评价实际是对大页碎片程度的评价其本身也是为了解决大页分配延迟而产生与上文各order阶内存碎片评估方式不同目前也并不针对其它order阶内存通过如下代码进⾏定义 即便未开启大页功能COMPACTION_HPAGE_ORDER通常也会被设置为9代表要预应性内存规整主要是针对2MB内存碎片程度进行评估下面从最顶层代码进行说明。 2.3.2.1 should_proactive_compact_node should_proactive_compact_node 函数计算当前node碎片程度当然是针对 COMPACTION_HPAGE_ORDER阶内存块其将会对每个zone进行评估得分并将所有zone所得分数累加获得最后得分。此得分如果高于预应性规整水线线代表碎片化程度较高需要进行预应性规整。之前通过vm.compaction_proactiveness节点设置积极性将会影响预应性规水线其值越高预应性水线值越低将越容易触发规整。预应性水线判断逻辑如下 注意预应性规整是⼀种预操作应尽可能降低对系统性能影响因此当kswapd运行时预应性规整不会启动。 2.3.2.2 fragmentation_score_wmark fragmentation_score_wmark 函数将会获取当前水线值当low入参设置为true时获取的是低水线否则获取高水线。 预应性水线的计算不复杂compaction_proactiveness设置的越低低水线值越高高水线通常比低水线高10但是高低水线都在100数值以内。 2.3.2.3 fragmentation_score_node fragmentation_score_node函数实现清晰即针对每⼀个zone计算一个得分并进行累加即为node的最后得分。 由于每个zone大小不同因此得分应有对应比重fragmentation_score_zone_weighted函数完成这个计算实现方式较为直接。 zone碎片评分乘以zone的有效内存页数量再除以整个node有效内存数量即为zone碎片程度得分的比重值。(zone有效页数量 / node有效页数量) * zone当前得分简单来说就是按照内存大小重进⾏计算。 fragmentation_score_zone函数用于计算每个zone碎片得分其实现原理是统计当前zone中⼤于等于COMPACTION_HPAGE_ORDER阶空闲区域的个数并计算除此之外空闲内存内存与当前zone空闲内存百分比这个占比可以说明碎片程度此值越高说明符合COMPACTION_HPAGE_ORDER阶内存块越少。 fill_contig_page_info函数用于获取当前zone空闲页、多少个COMPACTION_HPAGE_ORDER阶内存块等等此函数将会遍历当前zone上所有order的空闲链表进行累加计算对于order等于COMPACTION_HPAGE_ORDER的阶内存块个数累加对于order大于COMPACTION_HPAGE_ORDER阶内存块会拆分累加因为包含多个COMPACTION_HPAGE_ORDER阶空闲内存区此函数实现并不复杂不展开描述。 2.3.3 预应性规整特点 经过上述的判断终于可以开始预应性内存规整proactive_compact_node函数实现此功能代码如下 从compact_control结构体的设置可以看出预应性规整与下文主动规整类似其指定参数有如下特点 1不指定目标阶规整持续进行待扫描结束 2迁移页扫描器和空闲页扫描器不使用快速扫描能力 3规整模式为MIGRATE_SYNC_LIGHT轻度同步模式会阻塞 4规整范围为完整zone地址空间 5pageblock将会不会根据标记选择跳过 上述compact_control结构体参数含义见3.1节 2.4 主动内存规整 主动内存规整主要是指用户通过设备节点触发完整内存规整或针对node内存规整。内核提供两个设备节点用于触发内存规整。 通过compact_memory节点可以触发当前所有node以及下属所有zone的内存规整这个操作的成本非常高。另一个compact节点只有在NUMA系统上存在可以仅触发某一个node进行规整。无论如何主动内存规整触发逻辑是简单明了。 2.4.1 主动内存规整特点 主动内存规整compact_control设置较为简单解释如下 1不指定目标阶规整持续进行待扫描结束 2迁移页扫描器和空闲页扫描器不使用快速扫描能力 3规整模式为MIGRATE_SYNC同步模式会阻塞 4规整范围为完整zone地址空间 5pageblock将会不会根据标记选择跳过 上述compact_control结构体参数含义见3.1节 主动内存规整是内存规整最全面的方法当然其带来的性能影响也会最大默认情况下内核并不会触发这类内存规整。 3.内存规整 前言中已经简述规整的大致思路将一些页迁移聚拢腾出更多连续空间。那么在真正实现时实际需要解决的问题是内存规整范围是什么如何找到需要迁移的页什么页适合迁移规整何时结束等问题。上述的问题最终都在compact_zone函数中被解决。 compact_zone函数针对单个zone内存区进行内存规整这是内存规整的最小单元。其通过迁移页扫描器从低地址到高地址寻找迁移页通过空闲页扫描器从高地址到低地址寻找空闲页最终将扫描出的迁移页迁移至扫描出的空闲页完成内存规整如下图所示 更细节一些来说内存规整开始后先通过迁移页扫描器扫描并且扫描的单位为一个pageblock将当前pageblock中可迁移的页隔离后放入到待迁移的链表。随后调用空闲页扫描器扫描空闲页扫描器依然以pageblock为步长但不再限制扫描一个pageblock其扫描的目标是找到大于等于当前迁移页数量的空闲页上述中绿色和黄色箭头长度不同即想表达这个逻辑。上述扫描过程将会产生迁移页和空闲页用于后续内存迁移这样就完成了内存规整的一轮操作可能与大家理解不同内存规整并非一次性扫描zone然后再迁移而是以这种一步一步的方式进行迁移这能平摊内存规整对性能带来的风险并且每轮处理后都有机会判断当前内存规整是否可以退出。上文描述已经非常清晰勾画了compact_zone函数的核心逻辑但是实际上实现可能更加复杂比如什么条件下规整可以提前结束、迁移页扫描器和空闲页扫描器是否可以加速等等这些都属于更高层面优化的话题下文将会简述。 再次回到compact_zone函数将复杂的代码剥离可以很容易得到如下核心代码逻辑 1compact_finished函数用于判断当前规整是否结束 2isolate_migratepages是迁移页扫描器实现用于查找需要移动的页 3isolate_freepages是空闲页扫描器实现用于查找用于页迁移的空闲页 4migrate_pages是页迁移函数将上述两个扫描器扫描结果进行页迁移处理完成规整 至此compact_zone大体逻辑已经完成说明下文将会对1~3函数进行细致描述。 3.1 内存规整参数说明 compact_control控制了compact_zone的诸多行为不同场景触发内存规整诉求不同因此参数也不同这里初步罗列常用参数含义有助于理解不同场景下内存规整的差异。 3.2 迁移页扫描器migrate scanner isolate_migratepages函数实际就是迁移页扫描器的代码实现其通常会从低地址到高地址完整或部分扫描zone区域以pageblock为步长选择一个合适pageblock调用isolate_migratepages_block函数进行内存隔离需要注意的是isolate_migratepages函数在处理完一个pageblock后就会退出换句话说此函数一次调用只处理一个合适的pageblock。核心代码如下所示 总结一下isolate_migratepages函数主要通过如下三个函数调用完成整个isolate工作 A快速寻找合适pageblock或返回而迁移扫描起始位置fast_find_migrateblock B判断pageblock是否合适进行隔离suitable_migration_source C实际对一个pageblock进行隔离页搜索操作isolate_migratepages_block FAQ注意若未找到合适pageblock那么会持续进行线性遍历查找直到地址超过cc-free_pfn最开始时此值应为zone地址区域结尾处。 下面将会对上述三个函数进行解析完整描述isolate_migratepages函数功能细节。 isolate_migratepages函数会返回三个返回值内容如下 这些返回值将会影响compact_zone函数的返回值。 3.2.1 迁移页扫描器快速扫描 fast_find_migrateblock fast_find_migrateblock函数会尝试快速寻找一个pageblock来进行规整如果无法找到则返回cc-migrate_pfn注此值初始应为zone的起始地址后续应记录上一次迁移扫描器扫描结束位置避免后续重复扫描作为起始地址开始遍历通常考虑不就是从zone内存区域的起始地址寻找一个么但这样可能并不高效在之前版本中迁移页扫描器每次循环确实是基于zone的某个地址开始进行线性遍历这是一种线性搜索的过程但是随后引入了如下补丁改变了这一现状 https://patchwork.kernel.org/project/linux-mm/patch/20181214230531.GC29005techsingularity.net/ 其目的是尽量选择一个充斥着可移动空闲页pageblock块这样通过较少页迁移就可以满足高阶order内存申请需求。通过查找freelist空闲内存块反向查找对应pageblock这样效率非常高另一方面由于pageblock选择不是简单的顺序查找为了避免后续扫描重复pageblock还需要将其进行单独标识通过set_pageblock_skip函数完成设置确保再次进行扫描时会跳过这个pageblock内存块。以上就是主要思路但是具体在实现上有很多细节比如 a如果规整目标order太小那么完全没必要去寻找依然使用cc-migrate_pfn作为起始地址 b由于对于cc-order有要求因此仅适用于直接内存回收和异步内存回收仅这两种场景会指定cc-order其它场景cc-order为-1 c寻找空闲页所在pageblock必须是在内存搜索范围的前1/2或1/8这部分是最有可能被迁移页扫描到得区域避免影响到空闲页扫描 d空闲页扫描会改变free_list布局尽量保证下次扫描free_list不重复扫描空闲内存块 e若这只cc-ignore_skip_hint则迁移页扫描器不采用迁移页的fast机制 f仅搜索可移动空闲页并且搜索范围从order - 1阶开始 fast_find_migrateblock函数若无法找到合适pageblock那么将会返回cc-migrate_pages退化为正常线性扫描请注意的是fast_find_migrateblock函数每次也只找到一个pageblock并设置其为skip通过上述方式确实在一定程度上能够加速针对目标阶内存规整能够更快整理出目标阶内存需求但是对于完整内存规整并无实质效果因此fast加速查找仅适用于直接内存规整和kcompactd被动内存规整因为这些场景下通常会指定规整目标阶。核心代码如下 无论如何通过fast_find_migrateblock函数我们可以找到一个待迁移pageblock或者返回一个迁移页扫描起始地址cc-migrate_pfn用于后续针对pageblock内存迁移页扫描使用。 3.2.2 suitable_migration_source函数解析 suitable_migration_source函数用于判断当前pageblock是否可以进行隔离及迁移这部分逻辑相对简单。 这里需要关注的是如果非直接内存规整或非迁移类型非ASYNC模式则不需要判断pageblock迁移类型与compact_control迁移类型是否匹配尽可能进行内存规整。 3.2.3 pageblock内存隔离isolate_migratepages_block isolate_migratepages_block函数会在单个pageblock内进行遍历尝试将符合规整要求的页放入对应compact_control所指向的migratepages链表进行隔离用于后续页迁移操作。函数整体结构大致如下 起始会通过too_many_isolated函数检查当前内存节点上是否存在过多isolated页如果数量过大isolated (inactive active) / 2那么根据MIGRATE模式选择处理方法比如对于异步模式就是直接退出对于同步模式函数将会在这里进行等待一段时间再循环检查是否合适继续向下执行。随后就是通过for循环开始遍历这个pageblock里面所有页并对每一个页进行判断决定其处理方法这是一个复杂的过程下文拆分代码进行说明。 3.2.3.1 大页处理 对于大页处理一般情况下内存规整是会选择略过不进行整理。处理代码如下 从代码看当页为复合页并且cc-alloc_contig为false时此页将不会被规整。无论是hugetlbfs和THP大页都属于复合页那么问题的关键来到cc-alloc_contig是什么 从代码进一步推进可以看到通过alloc_contig_range函数进行内存分配时此函数会指定申请内存地址范围并尽力实现如果指定范围已经被占用会尝试触发直接规整进行页迁移如下代码所示 可以看到这种情况下会将alloc_contig参数设置为true在此逻辑中当调用到isolate_migratepages_block函数是会尝试规整大页其实际的做法是通过isolate_or_dissolve_huge_page函数实现大页溶解这部分涉及大页逻辑不再发散。 总结只有当页是hugetlbfs大页并且通过alloc_contig_range函数调用下来触发内存规整时才会进行处理其它场景下大页处理策略都是略过。 3.2.3.2 空闲物理页处理 空闲页的处理为直接跳过这无可厚非唯一需要注意的时空闲页跳过时并非单页跳过而是根据页的order阶进行跳过代码如下 3.2.3.3 non-LRU物理页 这个很有意思上文谈到大部分可移动页应该都是用户态的匿名页这里怎么还会有不再LRU上的物理页呢实际这涉及到页迁移特性的一种功能有兴趣的朋友可以阅读一下Documentation/vm/page_migration.rst文章中Non-LRU page migration这一小节。内核中申请的内存通常都是non-LRU上并且不可移动但是内核提供了定制能力开发者可以在内核驱动中将自己申请的内存标记为可移动为此内核为page添加了两个新的flag即PG_movable和PG_isolated用于标识这种non-LRU并且可迁移的页。开发者通常使用__SetPageMovable接口主动设置这些内存页PG_movable标记而PG_isolated标识此页已经被隔离开发者不需要主动设置此标记。 现在我们应该可以理解上述代码中对于__PageMovable(page)判断如果一个non-LRU页被设置了PG_movable并且PG_isolated还未被设置那么代表这个页也是可以进行迁移随后将会调用isolate_movable_page函数进行隔离操作。 问题还没有结束实际想要让这些在内核中直接申请的页变为可迁移光设置标记还不行开发人员需要自定义这些页如何隔离以及如何迁移因此内核要求开发者需要在address_space_operations结构体里面实现isolate_page、migratepage及putback_page函数。现在回到isolate_movable_page函数此函数将会调用开发人员注册的isolate_page函数完成这些页隔离操作代码如下 3.2.3.4 pinned匿名页 如果匿名页已经被mlock等接口pin住那么将会略过。 一方面通过page_mapping判断当前页是否为文件页另一方面通过page_count(page) page_mapcount(page)判断是否被pin住匿名页被pin住时会增加_refcount数值。 3.2.3.5 GFP_NOFS配置下仅处理匿名页 __GFP_FS表示内存分配过程中可以触发文件操作如果compact_control中gfp_mask不带__GFP_FS则结果依赖page_mapping返回值对于匿名页而言page_mapping返回NULL因此上述代码判断实际的含义是当分配上下文为无__GFP_FS并且是文件页时将会略过另一方面也可以解释为GFP_NOFS时仅处理匿名页。 那么什么时候compact_control中gfp_mask不带__GFP_FS前文说明了触发内存回收的场景在内存分配失败时有可能导致直接触发内存规整此时内存分配GFP标记将会被赋值到compact_control中用于配置内存规整行为。 可以想象这么做的原因实际在内存分配的过程中直接触发内存规整其系统并不希望耗费过多时间做有限度规整更为合适。 其它触发内存规整的场景通常compact_control中gfp_mask为GFP_KERNEL这是包含__GFP_FS因此规整涉及的内存范围通常更广。 3.2.3.6 不同isolate模式会影响页的处理策略 __isolate_lru_page_prepare完成此任务关键代码如下 此处逻辑是根据隔离类型筛选可迁移的物理页隔离类型来源于cc-mode也就是迁移类型代码如下 通常仅当MIGRATE_ASYNC和MIGRATE_SYNC_LIGHT模式时其隔离模式为ISOLATE_ASYNC_MIGRATE异步模式在这种模式下其会尽可能避免隔离可能会阻塞页比如代码中正在回写的页或者是脏页这里注意如果页为脏页但是其并非文件页swap匿名页或拥有自己migratepage函数那么页被认为迁移过程不会被阻塞否则都无法隔离。如果isolate_mode为ISOLATE_UNEVICTABLE代表本次隔离可以处理不可回收页这个主要是针对那些被lock住的unevictable页这些页不能够被回收但是支持迁移。 3.2.3.7 修改LRU即真正意义isolate隔离 在完成1~6的判断后剩下页将能够被隔离隔离的含义就是将其从LRU链表去除这个LRU有可能是来自于pglist_data也可能来自于页对应memcg随后将这些页添加至cc-migratepages用于后续页迁移。 3.2.3.8 总结 isolate_migratepages_block函数是内存规整过程中页隔离的重要函数其确定哪些页应该被隔离哪些页应该被略过其基本策略如下 1大页不应被隔离但是alloc_contig_range场景下有可能触发hugetlbfs大页溶解但这已不属于内存规整场景 2空闲物理页不会被隔离 3non-LRU物理页作为在内核分配内存如果开发者为其实现isolate_page、migratepage及putback_page函数则可以被隔离或迁移 4被Pin住的匿名页不会被隔离 5GFP_NOFS分配上下文仅隔离匿名页 6不同isolate模式会影响页的处理策略比如ISOLATE_ASYNC_MIGRATE不会隔离正在回写的页或脏页 3.3 空闲页扫描器free scanner isolate_freepages函数是空闲页扫描器的核心逻辑但是compact_zone中对于空闲页扫描器调用并不直接而是通过migrate_pages间接调用。 migrate_pages是内存迁移的基础接口其核心功能是将from链表中的页迁移至空闲页空闲页如何获取则在get_new_page中实现这是一个函数指针在compact_zone函数中此接口的实际调用形态如下 cc-migratepages就是之前通过isolate_migratepages隔离出来页compaction_alloc则实现如何获取空闲页可以想象isolate_freepages函数会在此调用也是本节分析的重点。compaction_alloc函数并不复杂其用于为内存规整页迁移时申请迁移的目的内存代码如下 cc-migratepages保存了需要进行规整迁移的页也就是迁移扫描器扫描的结果。 cc-freepages保存了页迁移的目的空闲页也就是空闲页扫描器扫描的结果。 当cc-freepages为空时尝试调用空闲页扫描器isolate_freepages函数尝试扫描隔离更多空闲页用于页迁移为什么要隔离呢隔离的本质是将其从伙伴分配系统中取出不再参与系统内存分配仅用于内存规整迁移使用。 isolate_freepages与上文isolate_migratepages函数相对应用于隔离空闲页用于页迁移。此函数也是free scanner空闲页扫描器的核心逻辑。其实现逻辑也与isolate_migratepages函数相似核心逻辑大致如下 isolate_freepages函数会根据cc-free_pfn开始反向以pageblock为单位进行遍历如果遇到合适pageblock则会进一步对pageblock中的页进行遍历将其中合适的空闲页进行隔离放入cc-freepages链表中用于后续页迁移使用当收集的空闲页足够迁移时将会退出。上图仅描述核心逻辑与实际实现有一些出入比如fast_isolate_freepages机制引入就会导致上述反向线性遍历的过程改变但是上图已经比较简要说明了空闲页扫描器的工作原理。 上文从函数调用角度描述isolate_freepages函数逻辑下文对部分重要函数调用做详细分析。 3.3.1 空闲页扫描器快速扫描fast_isolate_freepages 常规情况下系统通过从cc-free_pfn开始反向遍历寻找一个合适pageblock随后针对这个pageblock隔离其空闲页。但是这有可能低效比如第一个pageblock里面并没有多少空闲页那么针对这个pageblock进行大部分操作都是无效fast_isolate_freepages就是为改善这个问题其并不从cc-free_pfn开始进行线性查找而是借助伙伴系统中free_list快速找到一块合适空闲区域进行隔离从某种角度看这已经不是基于pageblock的处理了。这与fast_find_migrateblock函数目标类似均为提升扫描器效率。详细代码功能描述如下 1首先选取合适order即cc-search_order通常开始此值为cc-order - 1 FAQ此功能也是应用在直接内存回收和kcompactd场景下因为其快速搜索的前提是cc-order和cc-search_order。其它场景下触发的内存规整cc-order为-1其直接返回cc-free_pfn也就蜕变为线性搜索的模式 2在order的free_list中进行空闲页的遍历查找 3查找到合适空闲页后如果空闲页落入图中下图中绿色区域那么此空闲区域就会被优先隔离这里注意并非以pageblock为单位进行了min_pfn为1/2处low_pfn为3/4处 这个逻辑并不复杂内存规整期望内存向后方迁移如果空闲页太靠前极端点如果空闲页落入红色区域内存规整扫描器容易快速相遇导致无法解决内存碎片的问题 那么如果空闲页落入min_pfn和low_pfn之间那么系统会降低在当前阶freelist遍历机会倾向于降阶在新search_order阶的freelist中寻找绿色区域的空闲页除此以外这种场景下系统还会记录当前search_order对应freelist搜索记录这会改变freelist内存块顺序后续可避免额外搜索代码如下 4对于3已经找到search_order次的空闲区域将直接调用__isolate_free_page接口分析详见3.3.2.1节函数完成隔离随后将这些页放入cc-freepages链表完成整个操作 5到这里已经成功隔离了search_order阶空闲页这并不针对pageblock并且对于是否已经满足迁移需求数量也并没有约束所以在函数末尾调用了fast_isolate_around函数此函数本质是根据当前需求确认是否需要针对当前空闲区域所在pageblock再额外进行隔离代码如下 简单总结代码逻辑如下 如果当前已隔离的绿色区域已经满足诉求那么此函数将会直接退出如果不满足将会尝试将这个空闲区域对应pageblock左侧和右侧区域通过isolate_freepages_block函数进行空闲页隔离。 无论如何fast_isolate_freepages接口都尝试以更快速的方式获取空闲页有可能这个空闲页并不是紧贴着cc-free_pfn但是它一定在后1/4范围内并且它会改变freelist的结构避免重复判断相同空闲页这是一个优化功能。对于内存规整若想简单理解可以忽略此处细节直接理解为从zone末尾开始线性查找。 3.3.2 空闲页隔离isolate_freepages_block isolate_freepages_block函数即在指定内存范围内正向遍历将合适的空闲页进行隔离加入到cc-freepages链表用于后续页迁移这是isolate_freepages函数得核心函数调用通常isolate_freepages每次调用会传递一个pageblock范围进行空闲页隔离。关键代码如下 函数关键逻辑说明 1函数在pageblock内遍历并不一定按照页为单位参数stride作为步长存在 2复合页将会略过 3非空闲页将会略过 4符合上述要求空闲页通过__isolate_free_page进行隔离操作随后将这些加入到cc-freepages链表 5如果当前收集空闲页已经大于当前已经收集迁移页则退出循环 上述即isolate_freepages_block函数的逻辑这里面需要关注一个strict参数如果该参数为true那么isolate_freepages_block函数将会以页为遍历单位进行遍历及隔离并且不会再根据上述5条件提前退出而是完整隔离整个pageblock中合适的空闲页。 3.3.2.1 伙伴系统处理__isolate_free_page 空闲页隔离与迁移页的隔离不太相同由于空闲页还属于伙伴系统管辖范围内伙伴体统提供专用隔离接口即__isolate_free_page函数。 此结构逻辑清晰不过多赘述。 3.4 内存规整退出判断compact_finished compact_finished用于判断当前规整是否结束有多种不同条件导致规整结束并且返回值不同由于compact_finished函数较长并且对于理解内存规整较为重要因此代码拆分说明。 3.4.1 扫描器相遇 上文说明migrate scanner从正向扫描free scanner反向扫描当两者相遇代表扫描和迁移操作结束因此规整结束这是最为正常的一种退出方式代码如下 扫描器相遇场景退出上述代码注释完整标记其逻辑对于如何判断扫描器相遇实际根据cc-free_pfn和cc-migrate_pfn的大小容易判断不进行函数代码说明。 3.4.2 预应性规整退出条件 这里预应性规整除了扫描器相遇退出条件外拥有额外退出条件。 fragmentation_score_zone和fragmentation_score_wmark均为预应性规整碎片评估函数简单说当前如果对于大页阶碎片评估分数低于预应性碎片水线时则停止规整返回成功。关于预应性规整碎片评估逻辑详见2.3.2节。 3.4.3 direct规整模式额外退出条件 直接内存规整在识别是否成功时如果判断当前申请需求已满足并且分配迁移类型也满足一定要求即可退出直接规整逻辑。为何在申请可以满足的情况下还要满足一定要求才能退出呢主要考虑是即便满足分配但也不能引入潜在扩大内存碎片化的情况否则将会频繁进入直接内存规整。 3.4.4 返回值总结 1.COMPACT_CONTINUE代表内存规整未结束继续规整 2.COMPACT_COMPLETE在cc-whole_zone为true场景下完成全区域扫描和规整将返回此值 3.COMPACT_PARTIAL_SKIPPED多种场景下均会返回此值例如 1cc-whole_zone为false场景下扫描和规整完成将会范围此值 2在proactive_compaction模式下如果此时kswapd运行规整也将会停止返回此值 4.COMPACT_SUCCESS代表规整成功此值也是多种场景下均会返回 1proactive规整模式下碎片化得分达标主动退出规整即返回成功 2direct规整模式下需求order阶及迁移类型链表上已有足量内存即返回成功 3direct规整模式下需求order阶上如果有足量CMA内存前提是本身需求也是可迁移页否则CMA内存申请时无法迁移即返回成功 4direct规整模式下可以从其它迁移类型偷到内存在满足一定条件下也会范围成功 5.COMPACT_CONTENDED若当前进程被强制退出或依然持有zone lock则规整逻辑返回此值属于一种异常退出状态 4.内存规整总结 代码分析基本完成再对内存规整统计信息及可调文件节点进行简要说明。 4.1 内存规整统计信息 在上文的代码描述中忽略内存规整相关信息统计逻辑统计信息可以通过/proc/vmstat文件节点进查询相关信息含义说明如下 4.2 内存规整文件节点 4.3 总结 内存规整是一个较重内存碎片优化措施在使用时内核较为谨慎当前有直接内存规整、kcompactd内存规整、预应性内存规整及主动内存规整四种场景这些场景涵盖在内存分配、内存回收等上下文由于规整的诉求和紧迫程度不同其通过compact_control结构体参数控制compact_zone内存规整行为包括但不限于内存扫描范围、页迁移的能力、迁移页是否适合规整及是否可以阻塞等等。 另一方面内存规整的核心逻辑在于迁移页扫描器migrate scanner和空闲页扫描器free scanner运作原理包括哪些页可以作为迁移页或空闲页何时内存规整结束等等这些直接影响对内存规整理解。 由于个人能力有限文中如有疏漏或错误请见谅指正。 参考链接 【1】https://elixir.bootlin.com/linux/v5.15/source/mm/compaction.c 【2】https://lore.kernel.org/all/20190118175136.31341-23-mgormantechsingularity.net/T/#u 【3】https://lore.kernel.org/all/20181123114528.28802-5-mgormantechsingularity.net/T/#u 【4】https://lwn.net/Articles/817905/ 【5】https://lore.kernel.org/all/20200616204527.19185-1-niguptanvidia.com/T/#u 【6】https://lore.kernel.org/all/1627653207-12317-1-git-send-emailcharantecodeaurora.org/T/#u 【7】https://patchwork.kernel.org/project/linux-mm/patch/20181214230531.GC29005techsingularity.net/ 往 期 推 荐 手机投屏之WFD简介 Android logd日志简介及典型案例分析 OPPO在CLK大会上公布可编程内核技术引领安卓流畅体验升级 长按关注内核工匠微信 Linux内核黑科技| 技术文章| 精选教程