[[linux内存管理] 第023篇 watermark详解

0. 前言

简单来说,在使用zoned page frame allocator分配页面时,会将可用的free pageszonewatermark进行比较,以便确定是否分配内存。
同时watermark也用来决定kswapd内核线程的睡眠与唤醒,以便对内存进行检索和压缩处理。

回忆一下之前提到过的struct zone结构体:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
struct zone {
/* Read-mostly fields */

/* zone watermarks, access with *_wmark_pages(zone) macros */
unsigned long watermark[NR_WMARK];

unsigned long nr_reserved_highatomic;

....
}

enum zone_watermarks {
WMARK_MIN,
WMARK_LOW,
WMARK_HIGH,
NR_WMARK
};

#define min_wmark_pages(z) (z->_watermark[WMARK_MIN] + z->watermark_boost)
#define low_wmark_pages(z) (z->_watermark[WMARK_LOW] + z->watermark_boost)
#define high_wmark_pages(z) (z->_watermark[WMARK_HIGH] + z->watermark_boost)
#define wmark_pages(z, i) (z->_watermark[i] + z->watermark_boost)

可以看出,总共有三种水印,并且只能通过特定的宏来访问。

  • WMARK_MIN
    内存不足的最低点,如果计算出的可用页面低于该值,则无法进行页面计数;当系统剩余的空闲内存(free memory)降到min水位以下时,表明内存非常紧张。在这种情况下,内存分配请求会被阻塞,直到内存回收完成,以防止发生OOM(Out of Memory)错误。min水位是一个临界值,低于这个值时,内存分配器会同步等待内存回收,即触发direct reclaim。
  • WMARK_LOW
    默认情况下,该值为WMARK_MIN的125%,此时kswapd将被唤醒,可以通过修改watermark_scale_factor来改变比例值;当系统剩余的空闲内存降到low水位以下但仍在min水位以上时,表明内存面临一定的压力。在这种情况下,内核会唤醒kswapd线程进行异步内存回收,以逐步恢复空闲内存。low水位是一个警戒值,低于这个值时,kswapd会被唤醒,但内存分配请求不会被阻塞,也就是是异步回收。
  • WMARK_HIGH
    默认情况下,该值为WMARK_MAX的150%,此时kswapd将睡眠,可以通过修改watermark_scale_factor来改变比例值;当系统剩余的空闲内存恢复到high水位以上时,表明内存压力已经缓解。kswapd线程会停止内存回收操作,系统恢复正常运行。high水位是一个安全值,内存回收的目标是将空闲内存恢复到这个水平。

那在什么情况下我们需要调整内存水位呢?

  1. 避免性能下降:

通过合理设置内存水位,可以避免因内存不足而导致的性能下降。
例如,在内存紧张时,提前唤醒 kswapd 进行内存回收,可以减少直接内存回收(direct reclaim)的频率,从而降低进程的内存分配延迟。

  1. 提高系统稳定性:

内存水位机制确保系统在内存不足时能够及时采取措施,避免OOM错误的发生。通过调整水位值,可以平衡内存使用和系统性能,提高系统的整体稳定性。

  1. 适应不同业务场景:

对于不同的业务场景,可以通过调整内存水位来优化内存管理。例如,对于需要大量缓存的业务,可以适当提高low水位,以更早地进行内存回收,保持更多的空闲内存。

1
2
3
4
5
6
7
8
9
10
11
~ # cat /proc/zoneinfo | grep -E "Node|min|low|high|managed"
Node 0, zone DMA
min 1555
low 2318
high 3081
managed 763429
Node 0, zone Normal
min 492
low 733
high 974
managed 241968

1. 水位的初始化

内核在初始化阶段会调用 init_per_zone_wmark_min() 来进行每个zone的内存水位线初始化,同时也会设置zone 的 lowmem_reserve。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
int __meminit init_per_zone_wmark_min(void)
{
unsigned long lowmem_kbytes;
int new_min_free_kbytes;
/* 计算此ZONE_DMA和ZONE_NORMAL中超出high水线的页面数之和,由于此时high水位线还没初始化,因此等于各zone的页面数之和 */
lowmem_kbytes = nr_free_buffer_pages() * (PAGE_SIZE >> 10);
/* 只是对数值开平方,不对单位KB开平方,结果还取KB为单位 */
new_min_free_kbytes = int_sqrt(lowmem_kbytes * 16);
/* user_min_free_kbytes 默认是-1,但是通过sysctl更新 min_free_kbytes 时也会更新它 */
if (new_min_free_kbytes > user_min_free_kbytes) {
/* 系统让 min_free_kbytes 的初始化值介于128k~64M之间,但是之后通过sysctl接口设置就没这个限制 */
min_free_kbytes = new_min_free_kbytes;
if (min_free_kbytes < 128)
min_free_kbytes = 128;
if (min_free_kbytes > 262144)
min_free_kbytes = 262144;
} else {
pr_warn("min_free_kbytes is not updated to %d because user defined value %d is preferred\n",
new_min_free_kbytes, user_min_free_kbytes);
}
/* 计算每个zone的min、low、high水位值,其中min是按比例分配给各个zone的,low和high是在min的基础上加delta值得到。*/
setup_per_zone_wmarks();
/* 根据处理器数量和每个zone的内存量来计算阈值 */
refresh_zone_stat_thresholds();
/* 初始化 lowmem_reserve */
setup_per_zone_lowmem_reserve();

#ifdef CONFIG_NUMA
setup_min_unmapped_ratio();
setup_min_slab_ratio();
#endif

khugepaged_min_free_kbytes_update();

return 0;
}

1.1 nr_free_buffer_pages

在初始化的时候,会根据系统内存的free pages,进行一次计算:

new_min_free_kbytes = sqrt(free pages * 4 * 16) = 4 * sqrt(free pages * 4);

这里 free pages 指的是高于high 水位的pages 数,乘以4 是转换为KB (假设PAGE_SIZE 为4KB)。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
/**
* nr_free_buffer_pages - count number of pages beyond high watermark
*
* nr_free_buffer_pages() counts the number of pages which are beyond the high
* watermark within ZONE_DMA and ZONE_NORMAL.
*
* Return: number of pages beyond high watermark within ZONE_DMA and
* ZONE_NORMAL.
*/
unsigned long nr_free_buffer_pages(void)
{
return nr_free_zone_pages(gfp_zone(GFP_USER));
}

static unsigned long nr_free_zone_pages(int offset)
{
struct zoneref *z;
struct zone *zone;

/* Just pick one node, since fallback list is circular */
unsigned long sum = 0;

struct zonelist *zonelist = node_zonelist(numa_node_id(), GFP_KERNEL);

for_each_zone_zonelist(zone, z, zonelist, offset) {
unsigned long size = zone_managed_pages(zone);
unsigned long high = high_wmark_pages(zone);
if (size > high)
sum += size - high;
}

return sum;
}

对于 UMA来说,内存就一个 node ,并通过全局变量 contig_page_data 来管理。所以这里的zonlist 指的是 contig_page_data 管理下的所有zone,本函数的目的是为了计算超过 high 水位的有效内存。在系统初始化前期,high 水位没有设定,所以,在初始化时候统计的就是每个 zone 中 managed_pages 之和。

1.2 setup_per_zone_wmarks

该函数用以配置每个 zone 的水位,在初始化或者设置节点 /proc/sys/vm/min_free_kbytes/proc/sys/vm/watermark_scale_factor 时,都会通过此函数对zone 水位进行更新。

一共三处调用此函数:

  1. init_per_zone_wmark_min
1
2
3
4
5
6
int __meminit init_per_zone_wmark_min(void)
{
//...
setup_per_zone_wmarks();
//...
}
  1. /proc/sys/vm/min_free_kbytes节点
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
int min_free_kbytes_sysctl_handler(struct ctl_table *table, int write,
void *buffer, size_t *length, loff_t *ppos)
{
int rc;

rc = proc_dointvec_minmax(table, write, buffer, length, ppos);
if (rc)
return rc;

if (write) {
user_min_free_kbytes = min_free_kbytes;
setup_per_zone_wmarks();
}
return 0;
}
  1. /proc/sys/vm/watermark_scale_factor节点
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
int watermark_scale_factor_sysctl_handler(struct ctl_table *table, int write,
void *buffer, size_t *length, loff_t *ppos)
{
int rc;

rc = proc_dointvec_minmax(table, write, buffer, length, ppos);
if (rc)
return rc;

if (write)
setup_per_zone_wmarks();

return 0;
}

关于这两个节点的详细介绍,后续单独出文章讲述。

下面我们专心于这个函数

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
void setup_per_zone_wmarks(void)
{
struct zone *zone;
static DEFINE_SPINLOCK(lock);

spin_lock(&lock);
__setup_per_zone_wmarks();
spin_unlock(&lock);

/*
* The watermark size have changed so update the pcpu batch
* and high limits or the limits may be inappropriate.
*/
for_each_zone(zone)
zone_pcp_update(zone, 0);
}

1.2.1 __setup_per_zone_wmarks

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
static void __setup_per_zone_wmarks(void)
{
//pages_min是一个内存页面的阈值,单位为页(pages)。min_free_kbytes是系统中保留的最小空闲内存,PAGE_SHIFT是页面大小的位移,用于计算最小的空闲页面数。
unsigned long pages_min = min_free_kbytes >> (PAGE_SHIFT - 10);
// lowmem_pages用于记录系统中所有低内存区的页面总数
unsigned long lowmem_pages = 0;
struct zone *zone;
unsigned long flags;

/* Calculate total number of !ZONE_HIGHMEM pages */
// 遍历所有内存区,统计非高内存区(!is_highmem(zone))的页面数。zone_managed_pages(zone)返回该内存区管理的页面数。
for_each_zone(zone) {
if (!is_highmem(zone))
lowmem_pages += zone_managed_pages(zone);
}

for_each_zone(zone) {
u64 tmp;

spin_lock_irqsave(&zone->lock, flags);
// 再次遍历每个内存区,计算该内存区的水印值。
// tmp是通过计算每个内存区相对低内存页面数的比例来决定每个区域的WMARK_MIN值
tmp = (u64)pages_min * zone_managed_pages(zone);
do_div(tmp, lowmem_pages);
if (is_highmem(zone)) {
/*
* __GFP_HIGH and PF_MEMALLOC allocations usually don't
* need highmem pages, so cap pages_min to a small
* value here.
*
* The WMARK_HIGH-WMARK_LOW and (WMARK_LOW-WMARK_MIN)
* deltas control async page reclaim, and so should
* not be capped for highmem.
*/
unsigned long min_pages;
// 对于高内存区,WMARK_MIN值不完全根据比例计算,而是设置为一个较小的值,以防止过多的高内存被预留给内存回收。
min_pages = zone_managed_pages(zone) / 1024;
min_pages = clamp(min_pages, SWAP_CLUSTER_MAX, 128UL);
zone->_watermark[WMARK_MIN] = min_pages;
} else {
/*
* If it's a lowmem zone, reserve a number of pages
* proportionate to the zone's size.
*/
// 对于低内存区,直接使用计算出来的tmp值来设置WMARK_MIN
zone->_watermark[WMARK_MIN] = tmp;
}

/*
* Set the kswapd watermarks distance according to the
* scale factor in proportion to available memory, but
* ensure a minimum size on small systems.
*/
// tmp为tmp除以4 和 zone_managed_pages(zone)乘以watermark_scale_factor除以10000的结果,两者之间的较大值
tmp = max_t(u64, tmp >> 2,
mult_frac(zone_managed_pages(zone),
watermark_scale_factor, 10000));

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

spin_unlock_irqrestore(&zone->lock, flags);
}

/* update totalreserve_pages */
/*
* 设置完内存水位线后,会更新 totalreserve_pages 的值,这个值用于评估系统正常运行时需要使
* 用的内存,该值会作用于overcommit时,判断当前是否允许此次内存分配。见《内存管理-39-lowmem_reserve低端预留内存》
*/
calculate_totalreserve_pages();
}

min_free_kbytes 是 Linux 内核中一个非常关键的参数,它定义了系统在正常运行时,必须保持的最小空闲内存量。换句话说,它指定了内核认为系统应该保留的最小空闲内存量,避免系统因内存过度占用而发生内存溢出、卡死或出现严重的性能问题。

min_free_kbytes 作用:

  1. 保护系统不至于内存过载
    min_free_kbytes 保证了系统会尽量保留一定的内存空间以避免出现内存不足的情况。当系统的空闲内存量低于这个阈值时,内核就会开始启动回收机制,如启动 kswapd(内存回收守护线程)去清理不必要的页面(例如交换到磁盘的页面),或者通过内存压缩(如 zswap)来腾出内存。
  2. 避免过早的内存回收
    在内存压力较小的情况下,系统会尽量避免触发过多的回收工作,防止不必要的性能消耗。只有在系统的空闲内存低于 min_free_kbytes 时,内核才会主动进行内存回收,以确保系统在低内存状态下仍能维持正常运行。
  3. 影响内存水印计算
    由于 min_free_kbytes 定义了系统所需保留的最小空闲内存,它会影响内存水印的设置。水印(watermarks)是内存管理中的一个机制,表示内存的空闲程度,系统会根据这些水印来控制内存回收的时机。例如,min_free_kbytes 会参与计算内存区的 WMARK_MIN(最小水印),从而间接影响 WMARK_LOWWMARK_HIGH 的设置。

水位的计算都可能发生变化,但是大致的意思

  • min 水位:原始是在系统初始化时通过zone->managed_pages 与high 水位之差的sum,进行非线性计算得来;
  • low 水位:通过min + extra + factor;其中extra 和factor 是通过extra_free_kbytes节点和watermark_scale_factor 计算得来;
  • high 水位:通过min +extra + 2 * factor;

min_free_kbytes设的越大,watermark的线越高,同时三个线之间的buffer量也相应会增加。这意味着会较早的启动kswapd进行回收,且会回收上来较多的内存(直至watermark[high]才会停止),这会使得系统预留过多的空闲内存,从而在一定程度上降低了应用程序可使用的内存量。极端情况下设置min_free_kbytes接近内存大小时,留给应用程序的内存就会太少而可能会频繁地导致OOM的发生。

min_free_kbytes 设的过小,则会导致系统预留内存过小。kswapd 回收的过程中也会有少量的内存分配行为(会设上 PF_MEMALLOC)标志,这个标志会允许kswapd使用预留内存;另外一种情况是被OOM选中杀死的进程在退出过程中,如果需要申请内存也可以使用预留部分。这两种情况下让他们使用预留内存可以避免系统进入 deadlock 状态。

1.3 refresh_zone_stat_thresholds

这个函数 refresh_zone_stat_thresholds 主要用于刷新和更新每个内存区(zone)的阈值(threshold),以便为内存回收和分配提供有效的指引。它针对内存区的状态进行更新,特别是关于内存页面的水位(watermark)和每个 CPU 的相关数据。

1.4 setup_per_zone_lowmem_reserve

1.5 calculate_totalreserve_pages

计算各个zone的保留页面,以及系统的总的保留页面,其中会将high watermark看成保留页面。如图

1.6 初始化流程图

2. 快速分配中的水位

内存分配从用户端的gfp_mask 会转换为内部的 alloc_flags,最开始将 alloc_flags 的初始值设为 ALLOC_WMARK_LOW,这就是快速分配时的水位。

从 prepare_alloc_pages() 中可以看到根据进程是否设置 PF_MEMALLOC_NOCMA 来确定是否在CMA 区域分配。

在 get_page_from_freelist() 尝试分配时,依赖之前设定好的 alloc_flags 确定分配逻辑。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
static struct page *
get_page_from_freelist(gfp_t gfp_mask, unsigned int order, int alloc_flags,
const struct alloc_context *ac)
{
//...
///当检测到有外碎片化倾向时,临时提高低水位,提前触发kswapd线程回收内存,
//然后触发kcompacted做内存规整,这样有助于分配到大内存;
//
//无法分配到连续大内存,就认为有碎片化倾向,会从其他迁移类型挪用内存
mark = wmark_pages(zone, alloc_flags & ALLOC_WMARK_MASK);

///返回true,表示满足最低水位,且满足分配要求
///条件分支内容,为处理分配失败
if (!zone_watermark_fast(zone, order, mark,
ac->highest_zoneidx, alloc_flags,
gfp_mask)) {
//...

}
1
2
3
4
#define min_wmark_pages(z) (z->_watermark[WMARK_MIN] + z->watermark_boost)
#define low_wmark_pages(z) (z->_watermark[WMARK_LOW] + z->watermark_boost)
#define high_wmark_pages(z) (z->_watermark[WMARK_HIGH] + z->watermark_boost)
#define wmark_pages(z, i) (z->_watermark[i] + z->watermark_boost)

zone_watermark_fast() 详细可以查看:[Linux内存管理] 第21篇 buddy内存管理之快速分配中的第2.4章节。

__zone_watermark_ok

  • 开始的时候需要确认 alloc_flags 配置了ALLOC_HARDER 或 ALLOC_OOM,表示紧急情况下,可以访问更低水位的内存,甚至部分预留的物理内存;
  • __zone_watermark_unusable_free() 根据 alloc_flags 确定是否保留一些重要内存不让分配,例如ALLOC_HARDER 或 ALLOC_OOM 没有配置时,zone 的nr_reserved_highatomic 部分的内存要保留。再例如,当alloc_flags 没有配置 ALLOC_CMA时,zone 中CMA 区域的内存要保留,不让分配;
  • 如果alloc_flags 配置了ALLOC_HIGH,表示此次分配的优先级很高,可以考虑降低到水位的 1/2;
    • 如果alloc_flags 配置了ALLOC_OOM,水位还可以再下降 1/2,即到标记水位下降 3/4 处;
    • 如果alloc_flags 配置了ALLOC_HARDER,水位还可以再下降 1/4,即到标记水位下降 5/8 处;

3. 慢速分配中的水位

快速分配中,当free pages 低于指定的水位后,表示无法分配到内存从而进入慢速分配时刻。

alloc_flags 将初始化为 ALLOC_WMARK_MIN | ALLOC_CPUSET

在指定了标记水位为 WMARK_MIN 之后,慢速分配会首先唤醒kswapd,然后调用 get_page_from_freelist() 函数尝试分配,流程如快速分配,同样是根据水位、alloc_flags 中的优先级进行分配。

当首次 get_page_from_freelist() 无法分配到内存后,又对alloc_flags 进行修改并再次尝试分配,如果还是不行会进入直接内存回收、直接内存规整的流程。详细可以查看[linux内存管理] 第022篇 buddy内存管理之慢速分配

4. kswapd中的水位检测

TODO

5. 内存规整中的水位检测

TODO