1. 引言:理解Linux中的内存直接回收
内存直接回收(Direct Reclaim)是Linux操作系统内存管理中的一项关键机制。当系统可用内存降至极低水平,且一个进程尝试分配新的内存时,内核会触发直接回收,这是一个同步的过程,意味着触发内存分配的进程会暂停执行,直接参与到回收内存的操作中 。直接回收的目标是快速腾出足够的内存以满足当前的分配请求。与此相对的是后台回收(Background Reclaim),通常由kswapd
内核线程异步执行 。
内存直接回收在防止系统因内存耗尽而崩溃方面发挥着至关重要的作用,它像一道安全阀,在系统即将触发更严厉的内存不足(Out-of-Memory,OOM)杀手之前尝试恢复内存 。当系统内存压力增大,无法立即满足内存分配请求时,内核会首先尝试通过后台回收来释放内存。然而,如果后台回收的速度跟不上内存消耗的速度,或者由于内存碎片等原因无法有效回收足够的内存,那么当新的内存分配请求到来且可用内存低于某个临界点时,直接回收机制就会被激活。直接回收的主要目的是在OOM杀手介入之前,尽力保证系统能够继续运行。
理解直接回收的机制对于诊断和解决Linux系统中的内存相关性能问题至关重要。频繁的直接回收往往是系统面临内存压力的信号,可能导致应用程序响应延迟增加,甚至系统整体性能下降。因此,深入了解直接回收的触发条件、工作流程以及如何对其进行配置和监控,对于系统管理员和性能工程师来说都是非常重要的。
2. 内存压力机制与直接回收的触发条件
Linux内核将物理内存划分为不同的内存区域(Memory Zones),例如ZONE_NORMAL
、ZONE_DMA
和ZONE_HIGHMEM
等,以便于管理具有不同特性的内存 。每个内存区域都关联着三个关键的水位线(Watermarks):最小水位线(min)、低水位线(low)和高水位线(high)。这些水位线在内存管理中扮演着重要的角色。
最小水位线(pages_min
) 是触发直接回收的关键阈值 。当一个内存区域的可用页数下降到或低于这个水位线时,任何尝试从该区域分配内存的进程都会触发直接回收。这意味着系统已经处于严重的内存压力之下,必须立即采取行动来释放内存。
低水位线(pages_low
) 是唤醒kswapd
后台回收守护进程的阈值 。当可用内存降至低水位线以下时,kswapd
会开始异步扫描内存页,并尝试回收内存,直到可用内存达到高水位线。
高水位线(pages_high
) 是kswapd
停止后台回收的目标阈值 。一旦可用内存达到或超过高水位线,kswapd
就会进入睡眠状态,直到可用内存再次低于低水位线。
vm.min_free_kbytes
参数控制着系统中需要保持的最小空闲内存量(以KB为单位)。这个参数的值会影响到每个内存区域的pages_min
水位线,因为它决定了系统在每个低内存区需要保留的最小空闲页数。因此,增加vm.min_free_kbytes
的值会提高pages_min
阈值,使得系统在更早的时候就认为内存压力较大,这可能会导致直接回收的频率降低 。然而,过高的vm.min_free_kbytes
也可能导致系统更频繁地触发后台回收,甚至可能因为预留了过多的内存而导致系统过早地进入内存不足的状态。
除了达到pages_min
水位线,其他情况也可能触发直接回收。例如,当系统尝试分配大块连续内存(高阶分配)失败时,也可能触发直接回收 。这是因为即使系统总的空闲内存足够,但可能由于内存碎片化而无法找到足够大的连续内存块。在这种情况下,内核会尝试通过直接回收来整理内存,以满足高阶分配的需求。因此,内存碎片化问题本身也可能成为触发直接回收的原因 。即使总的可用内存看起来还不错,严重的内存碎片也可能导致直接回收的发生。
3. 内存直接回收的具体工作流程
当一个内存分配请求到来时,如果buddy system的空闲列表无法满足需求,内核会进入内存分配的慢速路径 。首先,内核会尝试使用低水位线作为阈值再次进行分配。如果分配仍然失败,说明内存可能略有不足,此时页分配器会唤醒kswapd
内核线程,进行异步的页面回收,并再次尝试分配。如果异步回收后分配仍然失败,则表明内存短缺非常严重,内核会首先尝试异步的内存规整(Memory Compaction)。如果异步内存规整后分配仍然不成功,内核才会发起直接内存回收。
直接回收的执行最终会调用shrink_node()
函数,该函数是针对特定NUMA节点启动内存回收的核心 。在shrink_node()
内部,系统需要决定是回收匿名页(由交换空间支持)、文件页(来自页缓存)还是两者兼而有之。这个决定受到多种因素的影响,包括回收优先级、非活跃文件页的数量、系统整体内存压力、vm.swappiness
参数以及一个称为“分数成本模型”(Fractional Cost Model)的机制 。分数成本模型试图估计回收每种类型页面所带来的I/O成本,并考虑到工作集重引用、脏页写回以及vm.swappiness
的设置。vm.swappiness
参数的值越高,内核就越倾向于回收匿名页。
接下来,shrink_node()
函数会遍历节点上所有活跃的内存控制组(Memory Cgroups),并为每个控制组调用shrink_lruvec()
函数。每个lruvec
包含其自身的匿名页和文件页的LRU(Least Recently Used,最近最少使用)列表。对于每个lruvec
,get_scan_count()
函数会根据之前做出的匿名页/文件页平衡决策和当前的回收优先级,确定需要扫描的每个可回收LRU列表(活跃匿名、非活跃匿名、活跃文件、非活跃文件)中的页数 。
实际的页面扫描和回收工作由shrink_list()
函数完成,它会循环扫描LRU列表的尾部,每次最多扫描SWAP_CLUSTER_MAX
(通常是32)个页面 。根据页面的标志(例如,页表项中的活跃位)以及是否允许特定类型(匿名或文件)的页取消激活,页面可能会被取消激活(从活跃列表移到非活跃列表)、保留(移回列表头部,如果最近被访问过)或回收(释放或准备驱逐)。扫描会持续到所有列表的预算(由get_scan_count()
确定)耗尽为止。在回收了最初的目标页数后,系统可能会继续扫描以消耗剩余的预算,并调整文件页和匿名页的预算以保持由分数成本模型确定的原始比例。最后,如果交换空间可用且非活跃匿名列表较低,shrink_list()
可能会额外扫描32个活跃匿名页。
当一个页面被选中进行回收时(通常来自非活跃列表),会调用shrink_inactive_list()
函数 。首先,该页面会从任何使用它的进程中解除映射。如果页面是干净的(自上次从磁盘读取后未被修改),它可以立即被释放并返回给页分配器。如果页面是脏的(已被修改),处理方式取决于它是文件页还是匿名页。回收脏文件页通常更为复杂,因为可能涉及死锁。通常,这些页面应该由专门的刷新线程写回磁盘。然而,在某些情况下,kswapd
(以及可能在某些情况下的直接回收)可能会发起立即写回(pageout()
)。如果无法立即写回,该页面可能会被标记一个回收标志并轮转回活跃列表。如果脏页是匿名页,则会分配交换空间(如果需要),并将该页面写出到交换空间(pageout()
)。页面的写回和回收标志会被设置,并启动一个异步写操作。然后该页面会被移回非活跃列表的头部。一旦写回完成,folio_end_writeback()
会注意到回收标志,并将该页面移到非活跃列表的尾部。
直接回收会持续进行这个扫描LRU列表和回收页面的过程,直到释放了足够的内存来满足最初的分配请求(即,内存区域中的可用内存上升到min
水位线以上)。值得注意的是,内核对直接内存回收的尝试次数是有限制的,例如在4.12及更高版本的内核中,最多尝试16次 。
4. 性能影响:平衡与潜在瓶颈
直接回收最主要的性能影响是它引入的延迟,因为触发内存分配的进程会被同步阻塞,直到有足够的内存被回收 。这种同步阻塞可能会导致应用程序的响应时间显著增加,尤其是在频繁发生直接回收的情况下。如果内核需要多次尝试直接回收才能获得足够的内存,那么内存分配的延迟可能会累积,对系统性能造成更严重的负面影响 。例如,如果每次直接回收尝试平均耗时10毫秒,并且内核尝试了16次,那么单次内存分配的延迟就可能增加到160毫秒。
值得注意的是,即使系统的总空闲内存看起来还足够,内存碎片化也可能导致直接回收的发生,并引发性能问题 。当系统需要分配一块大的连续内存时,即使有很多小的空闲内存块分散在各处,也无法满足这个需求,从而触发直接回收或内存规整。
/proc/vmstat
文件中的allocstall
统计信息可以反映直接页面回收发生的次数 。较高的allocstall
值通常意味着系统正经历着较为频繁的直接回收,这可能是内存压力过大或内存碎片化严重的迹象。
在某些极端情况下,如果系统触发了直接回收,但是没有任何可回收的页面(例如,没有启用交换空间,并且所有内存都被不可回收的进程占用),那么系统可能会发生挂起 。这是因为触发分配的进程会一直等待内存回收完成,但内核却无法回收任何内存。
5. 与核心内存管理子系统的交互
5.1 交换(Swap)
直接回收机制在内存压力下可能会选择将匿名内存交换到磁盘,以释放RAM空间 。vm.swappiness
参数控制着内核在内存压力下使用交换空间的倾向性,这个参数会影响直接回收过程中匿名页被交换出去的可能性 。较低的swappiness
值可能会延迟交换的发生,从而可能导致在内存压力下更多地直接回收文件支持的缓存。如果交换空间被禁用,而直接回收需要释放匿名内存,那么很可能会导致内存不足(OOM)错误,并可能导致进程终止或系统崩溃 。
5.2 分页(Paging)
分页是指在RAM(物理内存)和二级存储(如硬盘或SSD)之间移动数据的过程,以管理有限的物理内存资源 。直接回收可以包括将脏的匿名内存页分页到交换空间 。直接回收是由低于最小水位线的内存触发的,而kswapd
的后台分页则在低于低水位线时开始 。因此,直接回收是响应严重内存短缺的一种更直接和更强力形式的分页。
5.3 内存不足(OOM)杀手
直接回收是内核在调用OOM杀手之前采取的一个步骤 。如果直接回收未能释放足够的内存,并且内存规整也失败了,那么内核会调用OOM杀手来选择并终止一个或多个进程,以回收内存 。直接回收是防止OOM杀手这种极端措施的关键机制。
6. 配置和调整直接回收的相关参数
Linux提供了一系列的sysctl
参数,可以用来配置和调整直接回收的行为:
vm.min_free_kbytes
: 控制系统中需要保持的最小空闲内存量,直接影响pages_min
水位线,从而影响直接回收的触发 。增加此值可以减少直接回收的频率,但也可能导致后台回收更加频繁 。vm.swappiness
: 影响内核回收匿名页和文件支持页的平衡 。较低的值会优先回收文件缓存,这可能导致在内存压力下更多地直接回收缓存。vm.vfs_cache_pressure
: 控制内核回收用于缓存目录和inode对象的内存的倾向性 。增加此值可能会导致在直接回收期间更积极地回收这些缓存 。watermark_scale_factor
: 定义内存水位线之间的距离,影响kswapd
唤醒和直接回收触发的时机 。增加此因子可以在触发直接回收之前提供更大的空闲内存缓冲 。extfrag_threshold
: 影响内核在高阶分配时选择内存规整还是直接回收 。调整此参数有助于缓解由碎片引起的直接回收造成的性能问题。
针对不同的工作负载类型,可能需要调整这些参数。例如,对于内存密集型应用,增加vm.min_free_kbytes
可能有助于减少直接回收的频率。对于大量依赖文件缓存的应用,可能需要调整vm.vfs_cache_pressure
和vm.swappiness
来找到最佳平衡点。禁用交换空间会增加直接回收的压力,并提高OOM错误的风险 。
7. 诊断和解决因频繁内存直接回收导致的系统性能问题
频繁的直接回收通常是系统内存压力过大的表现。可以通过以下性能指标来判断是否发生了频繁的直接回收:
/proc/vmstat
中的pgscan_direct
和pgsteal_direct
值较高 。pgscan_direct
表示进程直接扫描的页数,pgsteal_direct
表示直接回收的页数。allocstall
统计信息增加 。- 在内存密集型任务期间,系统响应缓慢或无响应 。
可以使用标准的Linux监控工具来诊断这些问题:
vmstat
: 观察内存使用情况、交换活动和页面回收统计信息 。top
和htop
: 识别消耗大量内存的进程 。
更深入的分析可以使用基于eBPF的工具:
drsnoop
: 跟踪直接回收事件,显示哪些进程触发了回收以及相关的延迟 。drsnoop
提供了进程级别的直接回收分析粒度。
缓解频繁直接回收的策略包括:
- 增加
vm.min_free_kbytes
以保持更大的空闲内存缓冲 。 - 通过增加
vm.min_free_kbytes
或考虑透明大页(Transparent Huge Pages,THP)设置来解决内存碎片化问题(尽管THP在某些工作负载下有时会加剧碎片化)。 - 优化应用程序的内存使用,以降低整体内存压力 。
- 如果禁用或配置过于激进,考虑启用或调整交换空间 。
8. 不同操作系统中类似的内存回收机制比较
8.1 Windows
Windows也采用了内存回收策略,包括使用分页文件作为虚拟内存 。Windows后期版本引入了“内存提供和回收”功能,用于降低临时表面的内存开销,尤其是在图形密集型应用中 。此外,.NET CLR中的垃圾回收器也负责自动内存管理 。
8.2 macOS
macOS的内存管理包括统一缓冲区缓存(Unified Buffer Cache,UBC),用于缓存文件I/O 。macOS还具有“可清除内存”的概念,允许应用程序分配可以在内存压力下被操作系统回收的内存 。活动监视器中的内存压力指示器是评估内存健康状况的关键指标 。
8.3 内存回收机制比较表
操作系统 | 机制(们) | 关键概念 | 配置/调整 | 监控工具 |
---|---|---|---|---|
Linux | 直接回收, kswapd | 水位线, LRU列表, 交换 | vm.* sysctl参数, 交换 | vmstat , top , htop , drsnoop |
Windows | 分页, 内存提供/回收 | 分页文件, 虚拟内存 | 分页文件设置 | 任务管理器, 性能监视器 |
macOS | UBC, 可清除内存, 内存压缩 | 内存压力, 交换 | 有限的用户级调整 | 活动监视器 |
导出到 Google 表格
9. 基于eBPF的内存直接回收探测方法和工具
eBPF(扩展伯克利包过滤器)是一种强大的内核观测技术,无需修改内核或加载内核模块即可实现 。eBPF非常高效,开销很低,非常适合用于跟踪内核事件 。
eBPF可以用来监控与内存相关的内核事件,包括直接回收 。例如,drsnoop
工具(基于bcc/eBPF)可以跟踪mm_vmscan_direct_reclaim_begin
和mm_vmscan_direct_reclaim_end
这两个跟踪点 。这些跟踪点提供了关于直接回收开始和结束的时间、回收的顺序以及回收的页数等信息 。
通过使用libbpf等库或bpftrace等框架,可以创建自定义的eBPF程序,以更精细地分析直接回收,例如跟踪导致直接回收的调用栈,或监控特定的分配标志 。
10. 结论:通过理解直接回收优化Linux内存管理
内存直接回收是Linux内核在内存极度紧张时采取的关键措施,它通过同步回收内存来满足当前的分配需求,是防止系统崩溃的重要保障。理解直接回收的触发条件、工作流程以及性能影响,对于诊断和解决Linux系统中的内存相关问题至关重要。
为了优化Linux系统的内存管理,并尽量减少频繁直接回收带来的负面影响,系统管理员需要仔细配置相关的内核参数,例如vm.min_free_kbytes
、vm.swappiness
和vm.vfs_cache_pressure
等。这些参数的合理设置应根据具体的应用场景和工作负载进行调整。
此外,利用vmstat
、top
、htop
等标准工具以及drsnoop
等基于eBPF的专业工具,可以帮助我们监控系统的内存使用情况和直接回收的发生频率,从而更好地理解系统的内存压力状况。通过深入分析直接回收的行为,并结合对相关内存管理概念(如交换、分页和OOM)的理解,我们可以更有效地诊断和解决系统性能问题,最终实现更稳定、更高效的Linux系统运行状态。