概述

当系统内存短缺的情况下仍去申请内存,可能会触发系统对内存的回收,那什么时候应该进行回收,回收到什么标准又可以停止回收,参考依据是什么?即本文将介绍的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 :

  1. bool __zone_watermark_ok(struct zone *z, unsigned int order, unsigned long mark,
  2. int highest_zoneidx, unsigned int alloc_flags,
  3. long free_pages)
  4. {
  5. long min = mark;
  6. /*将当前zone中的free pages刨去不可用于分配的pages*/
  7. free_pages -= __zone_watermark_unusable_free(z, order, alloc_flags);
  8. /*上文提到的,若是紧急分配,watermark的标准将被放宽*/
  9. if (alloc_flags & ALLOC_HIGH)
  10. min -= min / 2;
  11. if (unlikely(alloc_harder)) {
  12. if (alloc_flags & ALLOC_OOM)
  13. min -= min / 2;
  14. else
  15. min -= min / 4;
  16. }
  17. /*1.引入lowmem_reserve的概念;2.这里的min实际上可能是min low high
  18. 注意:对水线的检查实际还会在watermark的基础上加上lowmem_reserve的
  19. 值,若当前zone是preferred zone,那么lowmem_reserve[x]=0。
  20. */
  21. if (free_pages <= min + z->lowmem_reserve[highest_zoneidx])
  22. return false;
  23. /* 如果请求是order-0,那么一定可以得到order-0的page,返回true */
  24. if (!order)
  25. return true;
  26. /*若不是order-0的请求,则检查free_area[]中是否有存在大于等于目标order的page*/
  27. for (o = order; o < MAX_ORDER; o++) {
  28. struct free_area *area = &z->free_area[o];
  29. int mt;
  30. if (!area->nr_free)
  31. continue;
  32. for (mt = 0; mt < MIGRATE_PCPTYPES; mt++) {
  33. if (!free_area_empty(area, mt))
  34. return true;
  35. }
  36. }
  37. return false;
  38. }

结论: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_DMACONFIG_ZONE_DMA32、则MAX_NR_ZONES值为4,他们是:

  1. enum zone_type {
  2. ZONE_DMA, /* 0 */
  3. ZONE_DMA32,
  4. ZONE_NORMAL,
  5. ZONE_MOVABLE,
  6. __MAX_NR_ZONES /* 3 */
  7. };

【2】那各个lowmem_reserve[MAX_NR_ZONES]是如何计算的呢?

我们来看看这个数组的初始化流程,它是由setup_per_zone_lowmem_reserve(void)函数来完成的。

  1. static void setup_per_zone_lowmem_reserve(void)
  2. {
  3. struct pglist_data *pgdat;
  4. enum zone_type j, idx;
  5. /* 遍历各个节点,我们考虑的UMA场景只有一个节点 */
  6. for_each_online_pgdat(pgdat) {
  7. /* 从DMA zone开始遍历,假设这里有ZONE_DMA,ZONE_DMA32,ZONE_NORMAL,ZONE_MOVABLE四个zone */
  8. for (j = 0; j < MAX_NR_ZONES; j++) {
  9. struct zone *zone = pgdat->node_zones + j;
  10. unsigned long managed_pages = zone->managed_pages;
  11. /*
  12. * 当j=0时:zone[DMA].lowmem_reserve[DMA] = 0 (不进入while循环)
  13. * 当j=1时:zone[DMA32].lowmem_reserve[DMA32] = 0
  14. */
  15. zone->lowmem_reserve[j] = 0;
  16. idx = j;
  17. while (idx) {
  18. struct zone *lower_zone;
  19. idx--;
  20. lower_zone = pgdat->node_zones + idx;
  21. if (sysctl_lowmem_reserve_ratio[idx] < 1) {
  22. sysctl_lowmem_reserve_ratio[idx] = 0;
  23. lower_zone->lowmem_reserve[j] = 0;
  24. } else {
  25. /*
  26. * 当j=1时实际计算为:
  27. * zone[DMA].lowmem_reserve[DMA32] = zone[DMA32]->managed_pages / sysctl_lowmem_reserve_ratio[DMA]
  28. * 当j=2时实际计算为:
  29. * zone[DMA32].lowmem_reserve[NORMAL] = zone[NORMAL]->managed_pages / sysctl_lowmem_reserve_ratio[DMA32] ( idx=1)
  30. * zone[DMA].lowmem_reserve[NORMAL] = (zone[NORMAL]->managed_pages + zone[DMA32]->managed_pages) / sysctl_lowmem_reserve_ratio[DMA] (idx=2)
  31. * 当j=3时实际计算为:
  32. * zone[NORMAL].lowmem_reserve[MOVE] = zone[MOVE]->managed_pages/sysctl_lowmem_reserve_ratio[NORMAL]
  33. * zone[DMA32].lowmem_reserve[MOVE] = (zone[MOVE]->managed_pages + zone[NORMAL]->managed_pages) /
  34. sysctl_lowmem_reserve_ratio[DMA32]
  35. * zone[DMA].lowmem_reserve[MOVE] = (zone[MOVE]->managed_pages + zone[NORMAL]->managed_pages + zone[DMA32]->managed_pages)/
  36. sysctl_lowmem_reserve_ratio[DMA]
  37. *
  38. */
  39. lower_zone->lowmem_reserve[j] =
  40. managed_pages / sysctl_lowmem_reserve_ratio[idx];
  41. }
  42. /*
  43. * 当j=2时,managed_pages = zone[NORMAL]->managed_pages + zone[DMA32]->managed_pages
  44. */
  45. managed_pages += lower_zone->managed_pages;
  46. }
  47. }
  48. }
  49. /* update totalreserve_pages */
  50. calculate_totalreserve_pages();
  51. }

上面的代码涉及的变量和参数较多,比较容易晕。我们对其中一个迭代进行实例化后就容易理解了。有了上面的理论,我们来看看真实情况和我们的理解是否一致。

实例验证

内核使用proc文件系统为用户提供了查看内存区信息的接口,我们一一观察。

首先看一下我们这个案例系统中的zone分布情况:

  1. / # cat /proc/buddyinfo
  2. Node 0, zone DMA 29 35 20 19 13 3 3 1 1 1 0
  3. Node 0, zone DMA32 482 533 479 352 244 71 35 21 16 13 131

可以看到系统里面只有两个zone,即DMA和DMA32。

接下来我们看看系统里面各个zone的的各个位段的保留内存lowmem_reserve情况:

  1. sh-4.3# cat /proc/zoneinfo | grep protection
  2. protection: (0, 1979, 1979, 1979)
  3. protection: (0, 0, 0, 0)

上面的两行分别是:zone[DMA]lowmem_reserve[]zone[DMA32]lowmem_reserve[],其元素对应为如下:

  1. zone[DMA].lowmem_reserve[DMA]=0 zone[DMA].lowmem_reserve[DMA32]=1979 zone[DMA].lowmem_reserve[NORMAL]=1979 zone[DMA].lowmem_reserve[MOVE]=1979
  2. 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接口来分别查看一下这两个参数:

  1. sh-4.3# cat /proc/zoneinfo | grep managed
  2. managed 3976
  3. managed 506662
  4. sh-4.3# cat /proc/sys/vm/lowmem_reserve_ratio
  5. 256 256 32

首先,系统总共有DMA和DMA32两个zone,所以上面的managed也只有两行,分别为zone[DMA].managed_pages=3976zone[DMA32].managed_pages=506662,单位是page;

其次,lowmem_reserve_ratio是用于控制保留内存数量的系数,这个是可手工修改的参数,其定义如下:

  1. int sysctl_lowmem_reserve_ratio[MAX_NR_ZONES-1] = {
  2. #ifdef CONFIG_ZONE_DMA
  3. 256,
  4. #endif
  5. #ifdef CONFIG_ZONE_DMA32
  6. 256,
  7. #endif
  8. #ifdef CONFIG_HIGHMEM
  9. 32,
  10. #endif
  11. 32,
  12. };

所以在咱们的用例中(内核只使能了CONFIG_ZONE_DMACONFIG_ZONE_DMA32,没有使能CONFIG_HIGHMEM)。所以lowmem_reserve_ratio[]系数的值为:

  1. sysctl_lowmem_reserve_ratio[DMA]=256, sysctl_lowmem_reserve_ratio[DMA32]=256, sysctl_lowmem_reserve_ratio[NORMAL]=32

所有的参数和算法已经准备完毕,下面我们就来手动实际验算一下。

先看看zone[DMA]保留内存的情况:

  1. zone[DMA].lowmem_reserve[DMA] = 0
  2. zone[DMA].lowmem_reserve[DMA32]= zone[DMA32].managed_pages / sysctl_lowmem_reserve_ratio[DMA]
  3. = 506662 / 256 = 1979
  4. zone[DMA].lowmem_reserve[NORMAL] = (zone[NORMAL]->managed_pages + zone[DMA32]->managed_pages) / sysctl_lowmem_reserve_ratio[DMA]
  5. = (0 + 506662)/256= 1979
  6. zone[DMA].lowmem_reserve[MOVE] = (zone[MOVE]->managed_pages + zone[NORMAL]->managed_pages + zone[DMA32]->managed_pages)/
  7. sysctl_lowmem_reserve_ratio[DMA]
  8. = (0 + 0 + 506662)/256 = 1979

上面的计算结果 (0,1979,1979,1979)与我们从proc接口看到的protection字段看到的一致。

再看看zone[DMA32]的保留内存情况

  1. zone[DMA32].lowmem_reserve[DMA32] = 0
  2. zone[DMA32].lowmem_reserve[NORMAL] = zone[NORMAL]->managed_pages / sysctl_lowmem_reserve_ratio[DMA32]
  3. = 0/256 = 0
  4. zone[DMA32].lowmem_reserve[MOVE] = (zone[MOVE]->managed_pages + zone[NORMAL]->managed_pages) /
  5. sysctl_lowmem_reserve_ratio[DMA32]
  6. = (0 + 0)/256 = 0

从上面的接口可以看到这些代码中的算法计算出来的实际值与proc接口读出来的值相符。

总结

这篇文章还是从实际遇到的问题引出的。之前在某个环境出现过Normal区内存量远高于水线时出现OOM的情况,最后查明原因就是因为保留内存太多导致。最终通过调整sysctl_lowmem_reserve_ratio参数进行规避。

【转载】内存基本概念-watermark&lowmem_reserve的更多相关文章

  1. linux中高端内存和低端内存的概念【转】

    转自:http://blog.csdn.net/hdujinhuihui/article/details/8686817 高端内存是Linux中一个重要的概念,初涉Linux时曾经对这个概念非常迷惑. ...

  2. JVM内存模型、指令重排、内存屏障概念解析(转载)

    在高并发模型中,无是面对物理机SMP系统模型,还是面对像JVM的虚拟机多线程并发内存模型,指令重排(编译器.运行时)和内存屏障都是非常重要的概念,因此,搞清楚这些概念和原理很重要.否则,你很难搞清楚哪 ...

  3. JVM内存模型、指令重排、内存屏障概念解析

    在高并发模型中,无是面对物理机SMP系统模型,还是面对像JVM的虚拟机多线程并发内存模型,指令重排(编译器.运行时)和内存屏障都是非常重要的概念,因此,搞清楚这些概念和原理很重要.否则,你很难搞清楚哪 ...

  4. <转载>内存管理内幕-动态分配的选择、折衷和实现 对malloc内存分配有个简单的描述,对内存管理有个大致的说明

    这篇文章看后感觉不错,和我在glibc下的hurdmalloc.c文件里关于malloc的实现基本意思相同,同时,这篇文章还介绍了一些内存管理方面的知识,值得推荐. 原文链接地址为:http://ww ...

  5. 作为php了解一下共享内存的概念及优缺点

    共享内存是一种在相同机器中两个正在运行的进程之间共享和传递数据的有效方式,不同进程之间共享的内存通常安排为同一段物理内存:顾名思义,共享内存就是允许两个不相关的进程访问同一个逻辑内存.一个进程可创建一 ...

  6. 关于掌握C#的内存堆栈概念

    很多时候,我们使用C#语言书写业务逻辑时,并不会太多地主动考虑到内存的占用和分配问题,但编的程序多了,就总会遇到一些性能问题.提到"性能"二字,就不得不考虑CPU和内存,而提到内存 ...

  7. [转载]oracle游标概念讲解

    原文URL:http://www.2cto.com/database/201203/122387.html ORACLE游标概念讲解 什么是游标?  ①从表中检索出结果集,从中每次指向一条记录进行交互 ...

  8. [转载]内存的一些magic number和debug crt

    原文:http://www.360doc.com/content/13/0105/17/6295074_258392439.shtml 调试过debug版本的vc程序的人一定对0xCCCCCCCC和0 ...

  9. CPU、内存通俗概念

    在计算机的组成结构中,有一个很重要的部分,就是存储器.存储器是用来存储程序和数据的部件,对于计算机来说,有了存储器,才有记忆功能,才能保证正常工作.存储器的种类很多,按其用途可分为主存储器和辅助存储器 ...

  10. Java内存模型概念简单介绍,想深入自行百度

随机推荐

  1. 掌握这些,轻松管理BusyBox:如何交叉编译和集成BusyBox

    在嵌入式系统中,由于设备的资源限制,需要开发人员寻找一种轻量.小型且使用广泛的工具集.而 BusyBox 就是这样一个在嵌入式系统中非常实用的工具集.本文将介绍如何在 Ubuntu 22.04 平台上 ...

  2. [ABC246E] Bishop

    Problem Statement We have an $N \times N$ chessboard. Let $(i, j)$ denote the square at the $i$-th r ...

  3. MyBatisPlus简介

    MyBatisPlus特性 国内的一个网站 网站地址简介 | MyBatis-Plus (baomidou.com)

  4. SpringSecurity入门(SSM版)

    1. 简介 Spring Security是一个能够为基于Spring的企业应用系统提供声明式的安全访问控制解决方案的安全框架.它提供了一组可以在Spring应用上下文中配置的Bean,充分利用了Sp ...

  5. 图片Base64相互转换

    一.简介 Base64编码是一种广泛应用于网络传输和数据存储的编码方式.在实际应用中,我们将图片转换为Base64编码,可以大大减少数据量,便于传输和存储.本文将详细介绍图片Base64编码的相互转换 ...

  6. 10 个免费的 AI 图片生成工具分享

    原文: https://openaigptguide.com/ai-picture-generator/ 在人工智能(AI)图像生成技术的推动下,各类AI图片生成网站如雨后春笋般涌现,为我们的日常生活 ...

  7. leetcode:354 俄罗斯套娃信封问题(LIS)

    解题思路: 根据题意,不难发现组合的元素,他们的长宽都是单调递增的,因此可以转化为最长上升子序列问题. 首先按照长度从小到大对信封进行排序,长度相同,按照宽度从大到小进行排序.因为当长度相同,因为可能 ...

  8. flask统一异常捕获(作用:统一捕获指定的异常并返回)

    flask中可以使用装饰器errorhandler来对指定的异常.状态码等统一捕获并处理. 对指定的状态码进行统一捕获 @app.errorhandler(404) # 参数e是异常的详细信息的对象, ...

  9. 3D组合地图在数据可视化大屏中的应用

    前言 当下数据可视化大屏展示的花样层出不穷,可视化大屏的C位越来越卷,地图的样式已经不再止步于普通的平面地图,在虚拟环境中探索和交互,今天我们要介绍的这一款3D组合地图可以将复杂的数据以直观的方式呈现 ...

  10. ElasticSearch之Clear cache API

    本方法用于清理缓存. 命令样例如下: curl -X POST "https://localhost:9200/testindex_001/_cache/clear?pretty" ...