OOM看 之 低端内存保护机制lowmem_reserve

发布时间 2023-06-17 17:56:57作者: 温暖的电波

一 什么是lowmem_reserve

为了防止高端内存申请者”偷用”太多的低端内存,内核的内存页分配器提供了一种叫做”lowmem_reserve”的机制防止来防止高端内存的申请者占用太多低端内存,这个机制是通过”lowmem_reserve_ratio”这个调节接口来决定低端内存被高端内存占用的程度。

lowmem_reserve_ratio在内核中是一个整形数组,可通过如下proc接口文件访问(linux-5.10版本)

cat /proc/sys/vm/lowmem_reserve_ratio
256    256    32    0    0

这个数组的默认值在内核定义如下:

int sysctl_lowmem_reserve_ratio[MAX_NR_ZONES] = {
#ifdef CONFIG_ZONE_DMA
        [ZONE_DMA] = 256,
#endif
#ifdef CONFIG_ZONE_DMA32
        [ZONE_DMA32] = 256,
#endif
        [ZONE_NORMAL] = 32,
#ifdef CONFIG_HIGHMEM
        [ZONE_HIGHMEM] = 0,
#endif
        [ZONE_MOVABLE] = 0,
};

需要注意的是,lowmem_reserve_ratio只是一个系数,并不是直接可用的数值,内核函数setup_per_zone_lowmem_reserve()通过lowmem_reserve_ratio这个系数来计算出各个zone的保留内存。各个zone的保留内存值在/proc/zoneinfo获取到,如下所示:

Node 0, zone      DMA
……
  pages free     2655
        min      70
        low      87
        high     104
        spanned  4095
        present  3998
        managed  3840
        cma      0
        protection: (0, 2740, 3567, 3567, 3567)

这里的protections可作为一个判断这个zone是否有足够分配内存的一个因素。

在上例中,如果一个normal内存页(index=2)要申请DMA的内存且使用到watermark[WMARK_HIGH]水线,内核会做如下判断:

free_pages = 2655 
watermark(WMARK_HIGH)+protection[2] = 104+3567 = 3671

因此free_pages < watermark(WMARK_HIGH)+protection[2] ,so,这次分配会失败。这里使用protection[2]是因为normal内存的index是2;如果请求的是DMA内存,则index=0,则使用protection[0]。

二 如何计算各个zone的lowmem_reserve

其实就是关于如何计算各个zone的protection[]。

系统在初始化时会调用根据setup_per_zone_lowmem_reserve计算各个zone的lowmem_reserve;此外,还可以在系统启动后通过/proc/sys/vm/lowmem_reserve_ratio接口调整各个zone的lowmem_reserve,具体计算公式参考如下伪代码:

  (i < j):
    zone[i]->protection[j]
    = (total sums of managed_pages from zone[i+1] to zone[j] on the node)
      / lowmem_reserve_ratio[i];
  (i = j):
     (should not be protected. = 0;
  (i > j):
     (not necessary, but looks 0)

有了伪代码,我们就更好理解下面的内核的计算代码,如下所示:

/*
 * setup_per_zone_lowmem_reserve - called whenever
 *      sysctl_lowmem_reserve_ratio changes.  Ensures that each zone
 *      has a correct pages reserved value, so an adequate number of
 *      pages are left in the zone after a successful __alloc_pages().
 */
static void setup_per_zone_lowmem_reserve(void)
{
        struct pglist_data *pgdat;
        enum zone_type i, j;

        for_each_online_pgdat(pgdat) {
                for (i = 0; i < MAX_NR_ZONES - 1; i++) {
                        struct zone *zone = &pgdat->node_zones[i];
                        int ratio = sysctl_lowmem_reserve_ratio[i];
                        bool clear = !ratio || !zone_managed_pages(zone);
                        unsigned long managed_pages = 0;
            /**
                i=0: zone[DMA].lowmem_reserve[DMA32] = managed_pages(zone[DMA32]) / ratio[DMA]
                          zone[DMA].lowmem_reserve[NORMAL] = (managed_pages(zone[NORMAL]) + managed_pages(zone[DMA32])) / ratio[DMA]
                i=1:   zone[DMA32].lowmem_reserve[NORMAL] = managed_pages(zone[NORMAL]) / ratio[DMA32]
            **/
                        for (j = i + 1; j < MAX_NR_ZONES; j++) {
                                struct zone *upper_zone = &pgdat->node_zones[j];

                                managed_pages += zone_managed_pages(upper_zone);

                                if (clear)
                                        zone->lowmem_reserve[j] = 0;
                                else
                                        zone->lowmem_reserve[j] = managed_pages / ratio;
                        }
                }
        }

        /* update totalreserve_pages */
        calculate_totalreserve_pages();
}

 

三 lowmem_reserve对于内存页分配的影响

内核在分配内存页时会调用__zone_watermark_ok()函数去检查zone中是否有足够的内存,其中的一个判定条件就是:

if (free_pages <= min + z->lowmem_reserve[highest_zoneidx])

其中,free_pages就是对应zone中的剩余可用内存;而min就是经过计算后的水线;z->lowmem_reserve[highest_zoneidx]就是该zone对于highest_zoneidx的预留内存。如果上述条件不满足,本次扫描会跳过这个zone,表示这个zone内存不足。

从这个代码逻辑来看,lowmem_reserve对于内存页的分配确实起着至关重要的作用,决定着内存分配成败。

在实际实践过程中如果发生OOM时看到free内存并未触及到水线时,可以看看是否是由于z->lowmem_reserve影响~