
[[linux内存管理] 第023篇 watermark详解
iliuqi0. 前言
简单来说,在使用zoned page frame allocator
分配页面时,会将可用的free pages
与zone
的watermark
进行比较,以便确定是否分配内存。
同时watermark
也用来决定kswapd
内核线程的睡眠与唤醒,以便对内存进行检索和压缩处理。
回忆一下之前提到过的struct zone
结构体:
1 | struct zone { |
可以看出,总共有三种水印,并且只能通过特定的宏来访问。
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水位是一个安全值,内存回收的目标是将空闲内存恢复到这个水平。
那在什么情况下我们需要调整内存水位呢?
- 避免性能下降:
通过合理设置内存水位,可以避免因内存不足而导致的性能下降。
例如,在内存紧张时,提前唤醒 kswapd 进行内存回收,可以减少直接内存回收(direct reclaim)的频率,从而降低进程的内存分配延迟。
- 提高系统稳定性:
内存水位机制确保系统在内存不足时能够及时采取措施,避免OOM错误的发生。通过调整水位值,可以平衡内存使用和系统性能,提高系统的整体稳定性。
- 适应不同业务场景:
对于不同的业务场景,可以通过调整内存水位来优化内存管理。例如,对于需要大量缓存的业务,可以适当提高low水位,以更早地进行内存回收,保持更多的空闲内存。
1 | ~ # cat /proc/zoneinfo | grep -E "Node|min|low|high|managed" |
1. 水位的初始化
内核在初始化阶段会调用 init_per_zone_wmark_min() 来进行每个zone的内存水位线初始化,同时也会设置zone 的 lowmem_reserve。
1 | int __meminit init_per_zone_wmark_min(void) |
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 | /** |
对于 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 水位进行更新。
一共三处调用此函数:
- init_per_zone_wmark_min
1 | int __meminit init_per_zone_wmark_min(void) |
- /proc/sys/vm/min_free_kbytes节点
1 | int min_free_kbytes_sysctl_handler(struct ctl_table *table, int write, |
- /proc/sys/vm/watermark_scale_factor节点
1 | int watermark_scale_factor_sysctl_handler(struct ctl_table *table, int write, |
关于这两个节点的详细介绍,后续单独出文章讲述。
下面我们专心于这个函数
1 | void setup_per_zone_wmarks(void) |
1.2.1 __setup_per_zone_wmarks
1 | static void __setup_per_zone_wmarks(void) |
min_free_kbytes
是 Linux 内核中一个非常关键的参数,它定义了系统在正常运行时,必须保持的最小空闲内存量。换句话说,它指定了内核认为系统应该保留的最小空闲内存量,避免系统因内存过度占用而发生内存溢出、卡死或出现严重的性能问题。
min_free_kbytes
作用:
- 保护系统不至于内存过载:
min_free_kbytes
保证了系统会尽量保留一定的内存空间以避免出现内存不足的情况。当系统的空闲内存量低于这个阈值时,内核就会开始启动回收机制,如启动kswapd
(内存回收守护线程)去清理不必要的页面(例如交换到磁盘的页面),或者通过内存压缩(如 zswap)来腾出内存。 - 避免过早的内存回收:
在内存压力较小的情况下,系统会尽量避免触发过多的回收工作,防止不必要的性能消耗。只有在系统的空闲内存低于min_free_kbytes
时,内核才会主动进行内存回收,以确保系统在低内存状态下仍能维持正常运行。 - 影响内存水印计算:
由于min_free_kbytes
定义了系统所需保留的最小空闲内存,它会影响内存水印的设置。水印(watermarks)是内存管理中的一个机制,表示内存的空闲程度,系统会根据这些水印来控制内存回收的时机。例如,min_free_kbytes
会参与计算内存区的WMARK_MIN
(最小水印),从而间接影响WMARK_LOW
和WMARK_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 | static struct page * |
1 |
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