一、Linux内存管理架构总览

Linux内核的内存管理子系统是整个操作系统中最复杂、最核心的模块之一。它在进程虚拟地址空间与物理RAM之间构建了一层精密的抽象,既要保证内存访问的高效性,又要在多进程环境下确保隔离性与公平性。

整个体系可以划分为四个层次:用户空间的内存malloc分配器(如ptmallc、jemalloc)、内核虚拟内存管理层(VMA、页表、缺页处理)、物理页分配层(Buddy分配器、Slab引擎)、以及硬件辅助层(TLB、MMU、HugePage)。

二、Buddy分配器:物理页的高效组织与分配

Buddy分配器是Linux物理内存分配的基础设施,它解决的本质问题是外部碎片。其核心思想是将物理内存按照2的幂次方组织成11个链表队列(order 0 ~ order 10),分别管理1页、2页、4页……最多1024页(4MB)的连续内存块。

当内核请求分配N个页面时,Buddy分配器从最接近且不小于N的order链表中取出一个块。如果该order没有空闲块,则从更高一级order中取出一个块,将其对半切割:一半分配给请求者,另一半放入低一级的空闲链表中。释放时,分配器会检查相邻的"伙伴"块是否空闲,如果空闲则向上合并形成更大的块,这正是"Buddy"一词的由来。

伙伴关系的判定基于物理地址:两个大小相同的块互为伙伴,当且仅当它们 addresses 之差恰好等于块大小。例如,一个order为5(32页)的块,其伙伴地址与其相差 32 * 4096 = 128KB。

Buddy分配器的效率极高,单次分配操作的时间复杂度为O(log n),其中n为最大order数。然而,它无法完全避免内部碎片——当请求不是2的幂次方时,会有部分内存被浪费。比如,申请65页会分配到128页的块,将近一半被内部碎片消耗。这也解释了为什么内核倾向于使用Slab引擎处理小对象高频分配。

三、Slab分配器:小对象高频分配的终极武器

Slab分配器建立在Buddy分配器之上,专门解决内核中大量小对象的频繁创建与销毁。其设计哲学是对象缓存与复用:预先从Buddy分配器申请若干页面,切分成固定大小的对象槽位,放入Slab缓存池中。分配时直接返回空闲对象,释放时不真正归还内存,而是标记为空闲状态放入_partial链表。

Slab缓存有三种状态:_full(所有对象被占用)、_partial(部分空闲)、_empty(全部空闲)。分配器优先从_partial中分配,若则从_empty中取一个升级为_partial;当_empty积累过多时,内核会周期性释放这些内存回Buddy系统(由slab_shrink回调触发)。

每个Slab缓存对应一个kmem_cache结构体,包含对象大小、构造函数(ctor)、着色偏移(color offset)、以及本地CPU缓存队列(array_cache)。着色是一种精巧的优化手段:通过在对象起始地址引入随机偏移,使得不同Slab中同一偏移的行对象映射到CPU缓存的不同缓存行,减少伪共享冲突。现代x86处理器通常64字节或128字节缓存行对齐,着色可以提升密集型数据结构分配场景下5%~15%的性能。

四、虚拟内存映射:VMA与页表的多层体系

每个用户进程拥有独立的虚拟地址空间(x86_64下可达128TB),这个空间由一系列VMA(Virtual Memory Area)来描述。VMA由vm_area_struct表示,记录了起始地址、结束地址、访问权限(读/写/执行)、映射类型(文件映射/匿名映射)、以及回写策略。

VMA组织为红黑树与双向链表的双重结构:红黑树用于O(log n)的快速查找(给定地址是否在某个VMA内),链表用于全遍历。当发生缺页中断时,内核首先通过红黑树定位VMA,判断该地址是否合法、是否有权限;若合法则分配物理页,并在页表中建立映射。

页表采用四级结构(PGD→PUD→PMD→PTE),每级9位索引(共39位),可映射512GB物理内存。64位模式下实际使用48位虚拟地址(256TB),通过符号扩展兼容。Modern处理器支持多种页大小:常规4KB、大页2MB或1GB(HugePage),后者可以显著减少TLB Miss率。

TLB(Translation Lookaside Buffer)缓存最近的虚拟→物理地址映射。一旦TLB未命中(TLB Miss),CPU需要"遍历页表"(Page Table Walk)到内存中逐级加载页表项。在密集随机内存访问场景下,HugePage能将TLB覆盖率提升512倍(4KB→2MB),部分数据库与DPDK应用因此获得显著性能收益。不过,HugePage需要预留连续物理内存,且不支持动态分配,这是工程上的代价。

五、OOM Killer:内存耗尽时的最后防线

当系统物理内存被耗尽、且Buddy分配器无法分配任何页面时,OOM Killer(Out-Of-Memory Killer)被激活。它的任务非常残酷:选择一个进程杀死,释放其占用的全部内存,以维持系统的生存。

OOM Killer的核心逻辑在mm/oom_kill.c中的select_bad_process()函数。它遍历系统中所有进程,对每个进程调用oom_badness()计算一个"badness score"。分数越高,越倾向于被杀死。

OOM评分公式在Linux 5.x之后的内核中大致为:

points = (totalpages * 100) / (进程已占用的RSS   1)

由于RSS在分母,占用内存越多的进程得分越高。然而内核引入了调节因子:

  • Squared Root调节:长期占用内存但未暴涨的进程,得分会被平方根压缩,避免杀死稳定的内存大户。
  • OOM Score Adj:每个进程可以设置/proc/[pid]/oom_score_adj(范围-1000~1000),使得管理员或systemd可以手动干预:设为-1000则永不杀死,设为1000则优先杀死。
  • CAP_SYS_ADMIN特权:特权进程的得分会乘以一个衰减因子,降低被杀概率。
  • 无子进程倾向:没有子进程的monolithic任务比有着多子进程的进程更易于被杀死,避免产生孤儿进程。

杀死目标进程后,内核还会设置内存节流:让剩余进程处于短暂的内存饥饿状态,防止立刻又触发下一轮OOM。这是Linux 5.18引入的OOM reaper机制——先异步回收被杀死进程的内存,然后再将CPU让渡给调度器。

六、内存cgroup:容器化时代的内存隔离

Linux的内存cgroup(Control Group)实现了进程组的资源隔离与限额。在容器场景下(Docker、K8s),内存cgroup是最重要的约束机制之一。

内存cgroup提供三个关键参数:

  • memory.limit_in_bytes:硬性内存上限。超过这个限制时,触发cgroup级别的OOM(而非系统级OOM),杀死cgroup内的一个进程,但不会影响其他容器或系统进程。
  • memory.soft_limit_in_bytes:软性内存上限。系统内存紧张时,超过软限制的进程组会被内核优先回收内存(页面回收),而不会直接OOM。这是一个"尽力而为"的限制。
  • memory.swappiness:控制该cgroup的页面回收匿名内存与文件缓存的比例。高swappiness倾向于swap匿名内存,低swappiness倾向于回收文件页面。

cgroup v2在内存控制方面做出了重大改进:

  • 统一了memory.low、memory.high、memory.max、memory.min四个阈值,分别表示保护值、节流值、硬上限、硬保护值。
  • 引入异步节流:当内存使用超过memory.high时,内核会逐步节流该cgroup的内存分配,通知用户态回收内存(通过PSI - Pressure Stall Information),而非直接OOM。
  • 提供更精确的内存统计(anon、file、kernel_stack、pagetables等)。

七、内核调优参数与实战策略

sysctl中与内存和OOM相关的核心参数:

  • vm.overcommit_memory:控制内存分配策略。0=启发式overcommit(默认,内核粗略评估剩余内存);1=始终overcommit(允许任何大小的malloc,常用于科学计算);2=严格模式(CommitLimit = Swap RAM * overcommit_ratio / 100),超过则直接返回ENOMEM,不触发OOM。
  • vm.overcommit_ratio:物理内存用于overcommit的百分比(默认50),仅在overcommit_memory=2时生效。
  • vm.panic_on_oom:0=默认(启动OOM Killer);1=系统panic;2=系统panic(即使cgroup OOM也不调用OOM Killer)。设置为2可防止重要服务被意外杀死(但系统会重启)。
  • vm.oom_kill_allocating_task:0=默认(遍历评分);1=直接杀死触发OOM的进程,避免遍历开销,适合高内存吞吐量但低延迟要求的系统。

实用建议:

  • 数据库场景(Redis、MySQL):设置overcommit_memory=2,并合理计算CommitLimit;设置oom_score_adj=-1000防止数据库进程被OOM;开启HugePage减少TLB Miss。
  • 容器/微服务场景:设置memory.limit_in_bytes上限,预留headroom防止系统OOM;使用cgroup v2的memory.high进行节流;监控内存PSI指标提前预警。
  • Java/JVM场景:除了设置RSS limit外,还需启用cgroup v2的内存感知功能(-XX: UseContainerSupport),让JVM读取cgroup限制而非主机总内存,避免申请过量内存触发OOM。
  • 高频Shell脚本场景:设置ulimit -v(虚拟内存限制)以防止fork炸弹之类的问题。

八、总结

Linux内存管理是一个从硬件到应用层的纵深体系:Buddy分配器解决碎片化、Slab引擎优化小对象分配、页表与VMA构建虚拟地址抽象、OOM Killer充当系统最后防线、内存cgroup实现资源隔离。每层设计都是性能、安全、稳定性的折中。

在生产环境中,理解这些机制并合理配置参数,是避免内存相关故障的关键。OOM不是"随机发生的灾难",而是系统资源分配模型的必然结果。掌握了OOM评分体系与cgroup限制策略,我们就能在内存资源稀缺时做出更理性的取舍——让不太重要的进程为关键服务让路,而不是凭运气随机杀死一个进程。

点赞(0) 打赏

评论列表 共有 0 条评论

暂无评论
立即
投稿

微信公众账号

微信扫一扫加关注

发表
评论
返回
顶部