【转载】内存基本概念-watermark&lowmem_reserve
概述
当系统内存短缺的情况下仍去申请内存,可能会触发系统对内存的回收,那什么时候应该进行回收,回收到什么标准又可以停止回收,参考依据是什么?即本文将介绍的watermark(内存水位线),当检查watermark时又不单单是判断watermark,还会牵扯到lowmem_reserve[]
,关于lowmem_reserve
本文会一并介绍。
说明:内核版本 5.9-rc2
watermark的概念
系统中每个NUMA node的每个struct zone中都定义着一个_watermark[NRWMARK]
数组,其中存放着该zone的min、low和high三种内存水位线。简单来说,它们是衡量当前系统剩余内存是否充足的一个标尺。当zone中的剩余内存高于high时说明剩余内存充足,低于low但高于min时说明内存短缺但是仍可分配内存,若低于min则说明剩余内存极度短缺将停止分配(GFP_ATOMIC类型的分配例外)并全力回收。
下图展示了min、low、high和内存回收的关系:
看图说话:
开始随着横轴time的增长,纵轴剩余内存total size由于内存分配急剧下降,当total size低于low的时候,会唤醒kswapd内核线程进行异步回收(所谓异步回收,就是此时仍可以分配内存,内存回收通过kswapd内核线程在后台同时进行);若回收的速度小于分配的速度,total size会降至min水位线以下,此时会触发同步回收,即在__alloc_pages_slowpath
函数中阻塞分配内存并尝试直接内存回收(reclaim)、内存压缩(compact)、更甚者OOM Killer强制回收;直到tatol size大于high水位线,回收才会停止。整体呈现为一个"V"字形状。
简单总结:
high:高于high时,kswapd会睡眠。
low:低于low时,kswapd会被唤醒。
min:低于min时,说明在low和min之间kswapd的回收的速度低于分配,因此阻塞分配,并通过各种形式回收内存。
补充:细心的话会发现,当内存低于min时还会下降一小段,说明仍然可以分配内存。但仅仅限于内核中的一些紧急的分配或是带有GFP_ATOMIC
标志的分配请求,会放宽对watermark
的检查,放宽多少,具体参看后面__zone_watermark_ok
()函数的实现。
大小关系:
high:low:min == 6:5:4
(这是系统启动后默认的比例,用户可以通过procfs下的watermark_scale_factor
进行调节)
追根溯源watermark:
procfs下面有个min_free_kbytes
的接口,它对应kernel中的一个同名全局变量min_free_kbytes
,表示上文提到的内存水位线min的大小。用户可以通过procfs下的/proc/sys/vm/min_free_kbytes
节点去调节它。但是它的值的范围不得大于256MB,不得小于128KB。若用户不去调节它,在系统启动时会为它计算一个初值,并根据这个值扩大1.25倍和1.5倍分别作为low和high的值。
这些工作都是在init_per_zone_wmark_min()
函数中实现,不贴代码了,直接看函数内容的提炼,如图:
(图中说明了全局变量min_free_kbytes
的计算方法,以及如何根据min_free_kbytes
为各个zone设置它们的min、low和high。)
检查watermark
kernel中检查watermark的函数有两个:zone_watermark_fast
和__zone_watermark_ok
,它们会在每次rmqueue分配内存前被调用用来检查watermark。
其中zone_watermark_fast()
是_zone_watermark_ok()
的扩展版本,增加了对order 0分配的快速检查。我们详细看下基础版本__zone_watermark_ok
:
bool __zone_watermark_ok(struct zone *z, unsigned int order, unsigned long mark, int highest_zoneidx, unsigned int alloc_flags, long free_pages) { long min = mark; /*将当前zone中的free pages刨去不可用于分配的pages*/ free_pages -= __zone_watermark_unusable_free(z, order, alloc_flags); /*上文提到的,若是紧急分配,watermark的标准将被放宽*/ if (alloc_flags & ALLOC_HIGH) min -= min / 2; if (unlikely(alloc_harder)) { if (alloc_flags & ALLOC_OOM) min -= min / 2; else min -= min / 4; } /*1.引入lowmem_reserve的概念;2.这里的min实际上可能是min low high 注意:对水线的检查实际还会在watermark的基础上加上lowmem_reserve的 值,若当前zone是preferred zone,那么lowmem_reserve[x]=0。 */ if (free_pages <= min + z->lowmem_reserve[highest_zoneidx]) return false; /* 如果请求是order-0,那么一定可以得到order-0的page,返回true */ if (!order) return true; /*若不是order-0的请求,则检查free_area[]中是否有存在大于等于目标order的page*/ for (o = order; o < MAX_ORDER; o++) { struct free_area *area = &z->free_area[o]; int mt; if (!area->nr_free) continue; for (mt = 0; mt < MIGRATE_PCPTYPES; mt++) { if (!free_area_empty(area, mt)) return true; } } return false; }
结论:watermark的检查不仅仅是对watermark的检查,还要在watermark的基础上加上lowmemreserve的值(下文将会介绍原因),后续再判断free_area中是否存在可以满足目标order分配需求的pages,全都都符合才算watermark ok~
lowmem_reserve的概念:
好了,是时候了解一下什么是lowmem_reserve了。
kernel在分配内存时,可能会涉及到多个zone,分配会尝试从zonelist第一个zone分配,如果失败就会尝试下一个低级的zone(这里的低级仅仅指zone内存的位置,实际上低地址zone是更稀缺的资源)。
考虑这样一种场景应用进程通过内存映射申请Highmem 并且加mlock分配,如果此时HIGH zone无法满足分配,则会尝试从Normal进行分配。问题来了,应用进程在从HIHG“降”到Normal区的分配请求有可能会耗尽Normal区的内存,而且由于mlock又无法回收,最终的结果就是Normal区无内存-在i386这样的架构上内核能够正常访问的线性区正是Normal区,这就导致kernel可能无法正常工作,然而HIGH zone却可能有足量的可回收内存。
针对这个情形,当Normal zone在碰到来自HIGH的分配请求时,可以通过lowmem_reserve声明:可以使用我的内存,但是必须要保留lowmem_reserve[NORMAL]给我自己使用。
同样当从Normal失败后,会尝试从zonelist中的DMA申请分配,通过lowmem_reserve[DMA],限制来自HIGHMEM和Normal的分配请求。
保留内存的初始化
有了上面的铺垫我们看一下各个区的lowmem_reseve[]是如何配置的,各个区究竟保留了多少内存。
【1】内核定义了一个long lowmem_reserve[MAX_NR_ZONES]
数组来表示各个区的保留内存。这个数组的大小MAX_NR_ZONES
与内核配置有关系,例如内核使能了CONFIG_ZONE_DMA
、CONFIG_ZONE_DMA32
、则MAX_NR_ZONES
值为4,他们是:
enum zone_type { ZONE_DMA, /* 0 */ ZONE_DMA32, ZONE_NORMAL, ZONE_MOVABLE, __MAX_NR_ZONES /* 3 */ };
【2】那各个lowmem_reserve[MAX_NR_ZONES]
是如何计算的呢?
我们来看看这个数组的初始化流程,它是由setup_per_zone_lowmem_reserve(void)
函数来完成的。
static void setup_per_zone_lowmem_reserve(void) { struct pglist_data *pgdat; enum zone_type j, idx; /* 遍历各个节点,我们考虑的UMA场景只有一个节点 */ for_each_online_pgdat(pgdat) { /* 从DMA zone开始遍历,假设这里有ZONE_DMA,ZONE_DMA32,ZONE_NORMAL,ZONE_MOVABLE四个zone */ for (j = 0; j < MAX_NR_ZONES; j++) { struct zone *zone = pgdat->node_zones + j; unsigned long managed_pages = zone->managed_pages; /* * 当j=0时:zone[DMA].lowmem_reserve[DMA] = 0 (不进入while循环) * 当j=1时:zone[DMA32].lowmem_reserve[DMA32] = 0 */ zone->lowmem_reserve[j] = 0; idx = j; while (idx) { struct zone *lower_zone; idx--; lower_zone = pgdat->node_zones + idx; if (sysctl_lowmem_reserve_ratio[idx] < 1) { sysctl_lowmem_reserve_ratio[idx] = 0; lower_zone->lowmem_reserve[j] = 0; } else { /* * 当j=1时实际计算为: * zone[DMA].lowmem_reserve[DMA32] = zone[DMA32]->managed_pages / sysctl_lowmem_reserve_ratio[DMA] * 当j=2时实际计算为: * zone[DMA32].lowmem_reserve[NORMAL] = zone[NORMAL]->managed_pages / sysctl_lowmem_reserve_ratio[DMA32] ( idx=1) * zone[DMA].lowmem_reserve[NORMAL] = (zone[NORMAL]->managed_pages + zone[DMA32]->managed_pages) / sysctl_lowmem_reserve_ratio[DMA] (idx=2) * 当j=3时实际计算为: * zone[NORMAL].lowmem_reserve[MOVE] = zone[MOVE]->managed_pages/sysctl_lowmem_reserve_ratio[NORMAL] * zone[DMA32].lowmem_reserve[MOVE] = (zone[MOVE]->managed_pages + zone[NORMAL]->managed_pages) / sysctl_lowmem_reserve_ratio[DMA32] * zone[DMA].lowmem_reserve[MOVE] = (zone[MOVE]->managed_pages + zone[NORMAL]->managed_pages + zone[DMA32]->managed_pages)/ sysctl_lowmem_reserve_ratio[DMA] * */ lower_zone->lowmem_reserve[j] = managed_pages / sysctl_lowmem_reserve_ratio[idx]; } /* * 当j=2时,managed_pages = zone[NORMAL]->managed_pages + zone[DMA32]->managed_pages */ managed_pages += lower_zone->managed_pages; } } } /* update totalreserve_pages */ calculate_totalreserve_pages(); }
上面的代码涉及的变量和参数较多,比较容易晕。我们对其中一个迭代进行实例化后就容易理解了。有了上面的理论,我们来看看真实情况和我们的理解是否一致。
实例验证
内核使用proc文件系统为用户提供了查看内存区信息的接口,我们一一观察。
首先看一下我们这个案例系统中的zone分布情况:
/ # cat /proc/buddyinfo Node 0, zone DMA 29 35 20 19 13 3 3 1 1 1 0 Node 0, zone DMA32 482 533 479 352 244 71 35 21 16 13 131
可以看到系统里面只有两个zone,即DMA和DMA32。
接下来我们看看系统里面各个zone的的各个位段的保留内存lowmem_reserve情况:
sh-4.3# cat /proc/zoneinfo | grep protection protection: (0, 1979, 1979, 1979) protection: (0, 0, 0, 0)
上面的两行分别是:zone[DMA]lowmem_reserve[]
、zone[DMA32]lowmem_reserve[]
,其元素对应为如下:
zone[DMA].lowmem_reserve[DMA]=0, zone[DMA].lowmem_reserve[DMA32]=1979 , zone[DMA].lowmem_reserve[NORMAL]=1979, zone[DMA].lowmem_reserve[MOVE]=1979 zone[DMA32].lowmem_reserve[DMA]=0, zone[DMA32].lowmem_reserve[DMA32]=0, zone[DMA32].lowmem_reserve[NORMAL]=0, zone[DMA32].lowmem_reserve[MOVE]=0
另外,我们从之前源代码分析了解到lowmem_reserve[]
的计算还依赖两个其他参数sysctl_lowmem_reserve_ratio[]
和 zone->managed_pages
。我们通过proc接口来分别查看一下这两个参数:
sh-4.3# cat /proc/zoneinfo | grep managed managed 3976 managed 506662 sh-4.3# cat /proc/sys/vm/lowmem_reserve_ratio 256 256 32
首先,系统总共有DMA和DMA32两个zone,所以上面的managed也只有两行,分别为zone[DMA].managed_pages=3976
, zone[DMA32].managed_pages=506662
,单位是page;
其次,lowmem_reserve_ratio
是用于控制保留内存数量的系数,这个是可手工修改的参数,其定义如下:
int sysctl_lowmem_reserve_ratio[MAX_NR_ZONES-1] = { #ifdef CONFIG_ZONE_DMA 256, #endif #ifdef CONFIG_ZONE_DMA32 256, #endif #ifdef CONFIG_HIGHMEM 32, #endif 32, };
所以在咱们的用例中(内核只使能了CONFIG_ZONE_DMA
,CONFIG_ZONE_DMA32
,没有使能CONFIG_HIGHMEM
)。所以lowmem_reserve_ratio[]
系数的值为:
sysctl_lowmem_reserve_ratio[DMA]=256, sysctl_lowmem_reserve_ratio[DMA32]=256, sysctl_lowmem_reserve_ratio[NORMAL]=32。
所有的参数和算法已经准备完毕,下面我们就来手动实际验算一下。
先看看zone[DMA]保留内存的情况:
zone[DMA].lowmem_reserve[DMA] = 0 zone[DMA].lowmem_reserve[DMA32]= zone[DMA32].managed_pages / sysctl_lowmem_reserve_ratio[DMA] = 506662 / 256 = 1979 zone[DMA].lowmem_reserve[NORMAL] = (zone[NORMAL]->managed_pages + zone[DMA32]->managed_pages) / sysctl_lowmem_reserve_ratio[DMA] = (0 + 506662)/256= 1979 zone[DMA].lowmem_reserve[MOVE] = (zone[MOVE]->managed_pages + zone[NORMAL]->managed_pages + zone[DMA32]->managed_pages)/ sysctl_lowmem_reserve_ratio[DMA] = (0 + 0 + 506662)/256 = 1979
上面的计算结果 (0,1979,1979,1979)与我们从proc接口看到的protection字段看到的一致。
再看看zone[DMA32]的保留内存情况
zone[DMA32].lowmem_reserve[DMA32] = 0 zone[DMA32].lowmem_reserve[NORMAL] = zone[NORMAL]->managed_pages / sysctl_lowmem_reserve_ratio[DMA32] = 0/256 = 0 zone[DMA32].lowmem_reserve[MOVE] = (zone[MOVE]->managed_pages + zone[NORMAL]->managed_pages) / sysctl_lowmem_reserve_ratio[DMA32] = (0 + 0)/256 = 0
从上面的接口可以看到这些代码中的算法计算出来的实际值与proc接口读出来的值相符。
总结
这篇文章还是从实际遇到的问题引出的。之前在某个环境出现过Normal区内存量远高于水线时出现OOM的情况,最后查明原因就是因为保留内存太多导致。最终通过调整sysctl_lowmem_reserve_ratio参数进行规避。
【推荐】国内首个AI IDE,深度理解中文开发场景,立即下载体验Trae
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· 10年+ .NET Coder 心语 ── 封装的思维:从隐藏、稳定开始理解其本质意义
· 地球OL攻略 —— 某应届生求职总结
· 提示词工程——AI应用必不可少的技术
· Open-Sora 2.0 重磅开源!
· 周边上新:园子的第一款马克杯温暖上架