Kernel Exploring
  • 前言
  • 支持
  • 老司机带你探索内核编译系统
    • 编译出你的第一个内核
    • 内核编译中的小目标
    • 可能是kbuild中最直接的小目标 – help
    • 使用了一个kbuild函数的目标 – cscope
    • 内核中单个.o文件的编译过程
    • 根目录vmlinux的编译过程
    • 启动镜像bzImage的前世今生
    • setup.bin的诞生记
    • 真假vmlinux–由vmlinux.bin揭开的秘密
    • bzImage的全貌
    • kbuild系统浅析
  • 启动时的小秘密
    • INIT_CALLS的秘密
    • 内核参数
  • 内核加载全流程
    • bootloader如何加载bzImage
    • 内核压缩与解压
    • 内核加载的几个阶段
    • 保护模式内核代码赏析
  • 内存管理
    • 内核页表成长记
      • 未解压时的内核页表
      • 内核早期的页表
      • cleanup_highmap之后的页表
      • 映射完整物理地址
      • 启用init_level4_pgt
    • 自底而上话内存
      • e820从硬件获取内存分布
      • 原始内存分配器--memblock
      • 页分配器
        • 寻找页结构体的位置
        • 眼花的页结构体
        • Node-Zone-Page
        • 传说的伙伴系统
        • Compound Page
        • GFP的功效
        • 页分配器的用户们
      • slub分配器
        • slub的理念
        • 图解slub
      • 内存管理的不同粒度
      • 挑战和进化
        • 扩展性的设计和实现
        • 减少竞争 per_cpu_pageset
        • 海量内存
        • 延迟初始化
        • 内存热插拔
        • 连续内存分配器
    • 虚拟内存空间
      • 页表和缺页中断
      • 虚拟地址空间的管家--vma
      • 匿名反向映射的前世今生
      • 图解匿名反向映射
      • THP和mapcount之间的恩恩怨怨
      • 透明大页的玄机
      • NUMA策略
      • numa balance
      • 老版vma
    • 内存的回收再利用
      • 水线
      • Big Picture
      • 手动触发回收
      • Page Fram Reclaim Algorithm
      • swapfile原理使用和演进
    • 内存隔离
      • memcg初始化
      • 限制memcg大小
      • 对memcg记账
    • 通用
      • 常用全局变量
      • 常用转换
    • 测试
      • 功能测试
      • 性能测试
  • 中断和异常
    • 从IDT开始
    • 中断?异常?有什么区别
    • 系统调用的实现
    • 异常向量表的设置
    • 中断向量和中断函数
    • APIC
    • 时钟中断
    • 软中断
    • 中断、软中断、抢占和多处理器
  • 设备模型
    • 总线
    • 驱动
    • 设备
    • 绑定
  • nvdimm初探
    • 使用手册
    • 上帝视角
    • nvdimm_bus
    • nvdimm
    • nd_region
    • nd_namespace_X
    • nd_dax
      • dev_dax
  • KVM
    • 内存虚拟化
      • Qemu内存模型
      • KVM内存管理
  • cgroup
    • 使用cgroup控制进程cpu和内存
    • cgroup文件系统
    • cgroup层次结构
    • cgroup和进程的关联
    • cgroup数据统计
  • 同步机制
    • 内存屏障
    • RCU
  • Trace/Profie/Debug
    • ftrace的使用
    • 探秘ftrace
    • 内核热补丁的黑科技
    • eBPF初探
    • TraceEvent
    • Drgn
  • 内核中的数据结构
    • 双链表
    • 优先级队列
    • 哈希表
    • xarray
    • B树
    • Maple Tree
    • Interval Tree
  • Tools
  • Good To Read
    • 内核自带文档
    • 内存相关
    • 下载社区邮件
Powered by GitBook
On this page
  • 水线的含义
  • 水线的计算
  • min_free_kbytes
  • 水位查看
  • 水线的调整
  • lowmem_reserve
  1. 内存管理
  2. 内存的回收再利用

水线

Previous内存的回收再利用NextBig Picture

Last updated 10 months ago

既然谈到了内存的回收再利用,那么就一定有这么一个标准:

什么时候开始回收,什么时候结束回收

在内核中这个标准就是 水线。

水线的含义

水线定义在每个zone上,如下简易示意图所示。

    struct zone
    +------------------------------+
    |watermark[NR_WMARK]           |
    |   (unsigned long)            |
    +------------------------------+

一共有三条水线:min, low, high。那这三条水线是什么含义呢?用一张图来解释应该比较清楚:

从这张图上我们可以看出:

  • 当内存容量小于低位水线,则开始启动kswapd回收内存

  • 当内存容量小于最小水线,则开启直接回收

  • 当内存容量高于高位水线,则kswapd可以休息

概念上还是很清楚的。

水线的计算

水线的计算在函数setup_per_zone_wmarks()中完成。不仅启动时会计算,还会在某些参数调整时再次计算。

整个计算过程其实不难,让我用伪代码解释一下:

  zone->_watermark[min] = (current zone lowmem * pages_min) / total lowmem
                        = (current zone lowmem / total lowmem) * pages_min
                        = (proportionate to the zone's size) * pages_min
  then we can get
  =>  sum(zone->_watermark[WMARK_MIN]) = pages_min

首先系统启动时会计算出整个系统内存保留的最小值: pages_min。这个计算在init_per_zone_wmark_min()完成。 最低水线的计算依赖于这个值。每个zone的最低水线 = (当前zone的lowmem * pages_min) / total lowmem。

这意味着系统上所有zone的最小水线之和 = pages_min 也就是整个系统想要最少预留的内存。

PS: 这个pages_min的当前值可以从/proc/sys/vm/min_free_kbytes查看,不过需要转换一下。

接着就是计算低位和高位水线了:

   tmp = (zone_managed_pages(z) * watermark_scale_factor) / 10000

   zone->_watermark[WMARK_LOW]  = min_wmark_pages(zone) + tmp;
   zone->_watermark[WMARK_HIGH] = min_wmark_pages(zone) + tmp * 2;

可以看到这三个水线之间的距离是相同的,而这个距离则是当前zone内存容量的一个比例。默认这个比例是0.1%。

PS: 这里的watermark_scale_factor也可以从/proc/sys/vm/watermark_scale_factor查看。

好了,是不是挺简单的了。

min_free_kbytes

在水线计算过程中,我们可以看到依赖了一个值pages_min。而这个值是从min_free_kbytes转换来的。

min_free_kbytes是在calculate_min_free_kbytes()中计算得到。实际公式很简单,在注释中已经给出了。

 	min_free_kbytes = sqrt(lowmem_kbytes * 16)

而这个lowmem_kbytes实际是从nr_free_buffer_pages()得到的,可以认为就是所有buddy管理的内存。

水位查看

了解了计算过程,我们也可以来看看当前系统中真实设置的值。这个信息可以在/proc/zoneinfo中查到。

Node 0, zone   Normal
  pages free     2761
        boost    1459
        min      1946
        low      2189
        high     2432
        spanned  262144
        present  262144
        managed  243597

其中managed的页有243597,那么默认情况0.1%就是243个page。从上面的数据看,正好符合这个公式。

low  = min + 243
high = low + 243

然后我们看一下这个水位对一个zone究竟是什么比例。

min      1946      = 7.6M
low      2189      = 8.5M
high     2432      = 9.5M
managed  243597    = 951M

可以看到,对一个1G的zone来讲,高水位也就10M,而每个水位之间间隔1M。 这么看,要唤醒kswapd,那是内存已经非常紧张的时候了。

水线的调整

水线是可以调整的,这样可以让系统保有更多的内存可以使用。方法就是通过上面提到的两个文件

  • /proc/sys/vm/min_free_kbytes

  • /proc/sys/vm/watermark_scale_factor

前者抬高低水位,后者加大水位之间的间隔。

具体的效果可以从/proc/zoneinfo文件里查看,这里就不做具体展示了。

lowmem_reserve

除了水线,还有一个概念控制着什么时候进行内存回收 -- lowmem_reserve。准确的来说应该是这是水线的组成部分。

在判断水线的函数__zone_watermark_ok()中,我们可以看到这么一个判断:

    /*
     * Check watermarks for an order-0 allocation request. If these
     * are not met, then a high-order request also cannot go ahead
     * even if a suitable page happened to be free.
     */
    if (free_pages <= min + z->lowmem_reserve[classzone_idx])
      return false;

也就是空闲页需要大于 水线(min) + lowmem_reserve[]才能算是水线达标。那么我们就来看看这个lowmem_reserve是怎么计算的吧。

setup_per_zone_lowmem_reserve()是设置lowmem_reserve[]的函数,让我们用一张图来展示一下这个概念:

        +-------------+-------------+------------+------------+
  lr[3] |mp[3] + mp[2]|mp[3] + mp[2]|mp[3]       |     0      |
        |+ mp[1] /    |   /         |   /        |            |
        |ra[0]        |ra[1]        |ra[2]       |            |
        +-------------+-------------+------------+------------+
  lr[2] |mp[2] + mp[1]|mp[2]        |     0      |   N/A      |
        |   /         |   /         |            |            |
        |ra[0]        |ra[1]        |            |            |
        +-------------+-------------+------------+------------+
  lr[1] |mp[1]        |     0       |   N/A      |   N/A      |
        |   /         |             |            |            |
        |ra[0]        |             |            |            |
        +-------------+-------------+------------+------------+
  lr[0] |     0       |   N/A       |   N/A      |   N/A      |
        |             |             |            |            |
        |             |             |            |            |
        +-------------+-------------+------------+------------+
        Zone[0]       Zone[1]      Zone[2]      Zone[3]

其中采用的缩写标示为:

  • mp[i]: managed pages of zone[i]

  • ra[i]: sysctl_lowmem_reserve_ratio[i]

  • lr[i]: lowmem_reserve[i]

这么看来 zone.lowmem_reserve[i]的含义是如果要在zone上分配 zone[i]的内存,而此时zone上的内存小于zone.lowmem_reserve[i] + watermark,那么这个分配将出发回收动作。.

隔了一段时间来看,自己都看不懂了。让我来展开讲一下,希望下次自己能看懂。

上面这个图竖着看,每一列代表了一个zone上的lowmem_reserve[]数组。比如我们拿第一列Zone[0]来看。

        +-------------+
  lr[3] |mp[3] + mp[2]|
        |+ mp[1] /    |
        |ra[0]        |
        +-------------+
  lr[2] |mp[2] + mp[1]|
        |   /         |
        |ra[0]        |
        +-------------+
  lr[1] |mp[1]        |
        |   /         |
        |ra[0]        |
        +-------------+
  lr[0] |     0       |
        |             |
        |             |
        +-------------+
        Zone[0]

正常我们期望的是用户想要分配Zone[0]的内存时,才会真的到Zone[0]上分配内存。所以此时的lr[0]等于0,表示当我们在Zone[0]上分配Zone[0]内存时,不需要再额外保留内存了。

但是有时候(估计这种情况不多),用户想要从Zone[3]上分配内存,但是因为各种原因没法满足,page allocator就会遍历zonelists往低zone去尝试分配。假如此时走到了Zone[0],那么就会在低位水线上是否还有额外的lr[3]的空闲内存。如果没有了,则会触发回收。

启动时计算流程参见

初始化流程
watermark