目录
2.3 预应性内存规整(proactive compaction)
2.3.2.1 should_proactive_compact_node
2.3.2.2 fragmentation_score_wmark
2.3.2.3 fragmentation_score_node
3.2.2 suitable_migration_source函数解析
3.2.3 pageblock内存隔离(isolate_migratepages_block)
3.3.1 空闲页扫描器快速扫描(fast_isolate_freepages)
3.3.2 空闲页隔离(isolate_freepages_block)
3.3.2.1 伙伴系统处理(__isolate_free_page)
3.4 内存规整退出判断(compact_finished)
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和目标阶,因此存在水线判断。
(4)direct_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前进行内存规整的条件判断,其意图如下:
(1)kswapd内存回收失败多次;
(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线程。