引言

内存管理是Linux内核中最核心且最复杂的子系统之一。内核不仅要管理物理内存的分配与回收,还需兼顾碎片控制、NUMA亲和性、内存热插拔、交换策略、OOM处理等高级话题。本文将从物理页分配的基本单元出发,深入剖析Buddy System如何高效管理连续物理页框,SLAB/SLUB/SLOB allocator如何在内核态避免内部碎片,阐述页面回收机制(kswapd/LRU/反向映射)如何在内存压力下保持系统稳定,并给出NUMA架构下的内存调优策略与应用场景。

1. 物理页管理基础

Linux内核将物理内存以页(Page)为单位管理,默认页大小为4KB(x86_64)。每个物理页由struct page结构体描述(位于mm/page_alloc.c中预分配的mem_map数组),包含引用计数(_mapcount/_refcount)、映射信息、LRU链表节点、所属zone/section等信息。本节聚焦伙伴系统(Buddy System)如何管理页面级分配。

2. 伙伴系统(Buddy System):连续物理页的高效分配

伙伴系统是内核物理页的"顶层分配器",解决的问题是:按2的幂次(n阶,即2ⁿ页)分配连续的物理页。它维护MAX_ORDER个(默认11个,order0到order10)free_area链表数组,每个order维护对应大小的空闲块双向链表。

2.1 分配过程

当内核请求一个2ⁿ页块时:

  1. 在free_area[n]的链表中查找空闲块,若有直接摘下并从伙伴系统中移除
  2. 若empty,向(n+1)阶申请;若仍无,继续向上查找直到order10
  3. 当高阶块被拆分为n阶目标块时,剩余的"伙伴"块继续挂入对应阶的空闲链表
  4. 核心操作是page与buddy page在物理页地址上的异或检查:buddy_pfn = pfn XOR (1 << order>

2.2 释放过程

释放时,内核检查该块的"伙伴"是否也空闲——若伙伴空闲则合并为2^(order+1)块。这是伙伴系统名字的来源。合并操作递归向上直到对应阶的伙伴被占用或到order10。

2.3 伙伴系统的碎片问题

尽管伙伴系统高效地管理了"内部碎片"(最多浪费一半请求大小)和"伙伴间碎片",但外部碎片仍是一大挑战。外部碎片指:由于分配/释放的时序导致空闲页不连续,使得无法分配请求大小的连续块。

Linux通过页面迁移(Page Migration)内存规整(Memory Compaction)来缓解外部碎片:

  • 可移动页面(MOVABLE):用户态页面和页缓存(Page Cache)可通过页面迁移放到其他位置
  • 不可移动页面(UNMOVABLE):内核数据/内核栈等无法迁移,是碎片的根源
  • kmemcg调节:通过cgroup memory限制碎片产生的速率

Linux 5.x引入了/proc/sys/vm/compaction_period_killis和/proc/sys/vm/extfrag_threshold等参数控制自动规整频率。Linux 6.x还引入MGLRU(Multi-Generational LRU),优化了页面置换决策。

3. SLUB分配器:小对象分配的核心设计

伙伴系统管理页级粒度分配,但内核数据的分配需求多是小对象(几十到几KB)。SLUB(Scalable Linux Unqueued B allocator)作为SLAB的改进版,是当前Linux默认的小对象分配器。

3.1 SLUB基本原理

SLUB从伙伴系统整页分配页面,将页面切成等大小的slab。每个slab有三种状态:full、partial、empty。每个CPU维护本地缓存(per-CPU partial slab)和本地空闲缓存(per-CPU freelist)。分配时优先从CPU本地分配以避免缓存抖动;释放时优先归还CPU本地。

3.2 性能优化设计

  • per-CPU缓存:减少NUMA远端内存访问
  • 本地freelist:CPU本地维护空闲对象链表,分配/释放O(1)
  • slab着色(Slab Coloring):不同slab的同序号对象的cache line偏移不同,减少CPU Cache ping-pong
  • O(1) object空闲链表:使用union将空闲链表内嵌在对象内存中(类似free-list-embedded)
  • order-0分配优化:对于小对象slab,直接使用order0单页(减少伙伴系统开销)

3.3 kmem_cache与内核内存分类

Linux通过kmem_cache_create()创建大小专用的缓存池(kmem_cache)。常见的kmem_cache包括:task_struct、mm_struct、inode、dentry、file、vm_area_struct等。每种类型的内核对象使用独立的缓存,避免不同大小对象共用slab产生的外部碎片。

可通过/proc/slabinfo查看各kmem_cache的活跃对象数、对象大小、每slab页数等信息:

name                 : tunables
dentry             1205184  1369920     192   21    2 : ...
inode_cache         31248   33600   1012    8    2 : ...

4. Zone与Node:NUMA架构的内存管理

4.1 Zone的划分

一个NUMA节点(Node)的内存按Zone划分:

  • ZONE_DMA:前16MB,设备DMA必须使用的内存
  • ZONE_DMA32:4GB内,32位DMA设备使用
  • ZONE_NORMAL:直接映射区,内核线性映射!(x86_64)
  • ZONE_MOVABLE:可移动区(虚拟zone)
  • ZONE_DEVICE:设备内存(持久内存等)

ZONE_NORMAL的物理页有内核线性映射(page_address即可用),是内核最常申请的zone。ZONE_DMA32/ZONE_DMA在内核启动早期初始化特定pool,优先级低于ZONE_NORMAL。

4.2 NUMA感知分配

NUMA中每个CPU有本地内存节点和远端节点。本地访问延迟低~100ns,远端~300ns。Linux伙伴系统对每Node每Zone维护独立的free_area数组,分配时优先从本地Node(freelist),失败后fallback到其他Node(按距离顺序)。

4.3 NUMA策略(mempolicy)

可通过set_mempolicy()/mbind()设置进程NUMA策略:

  • MPOL_BIND:分配必须来自指定nodes集合
  • MPOL_PREFERRED:优先本地节点
  • MPOL_INTERLEAVE:交替分配(平衡带宽)
  • MPOL_DEFAULT:本地优先(默认策略)

4.4 NUMA Auto-balancing

Linux 3.13后引入Auto NUMA Balancing:周期性扫描进程页面访问频率:

  • 扫描线程(knumad)识别NUMA hinting faults(页面被远端CPU频繁访问)
  • 自动将页面migrate到访问的CPU所在node
  • 超过阈值后整个进程迁移到高频节点

5. 页面回收机制:kswapd与LRU算法

5.1 LRU双链表

Linux内核把每个zone的页面维护在LRU双链表:

  • LRU_INACTIVE_ANON:不活跃匿名页(进程堆/栈)
  • LRU_ACTIVE_ANON:活跃匿名页
  • LRU_INACTIVE_FILE:不活跃文件页(Page Cache/Buffered IO)
  • LRU_ACTIVE_FILE:活跃文件页
  • LRU_UNEVICTABLE:不可回收页(mlock/驱动内存)

页面第一次加入inactive链表,被访问2次后提升到active。这种"二次机会法"避免一扫而过的页面(如全表扫描)占据内存。

5.2 kswapd与direct reclaim

内核后台线程kswapd周期性检查各zone水位:

  • high水位:充裕,kswapd休眠
  • low水位:kswapd唤醒,异步回收至high
  • min_water:kswapd紧急回收;进程分配触发同步direct reclaim阻塞等待回收

direct reclaim是应用延迟抖动的主要原因——进程停下来等内存回收完成。

5.3 Reclaim算法

回收优先级(从轻到重):

  1. 释放空闲slab对象(cache shrinks)
  2. 回收不活跃文件页(Page Cache)
  3. 回收不活跃匿名页(swap)
  4. 回收活跃页(强行降级)
  5. OOM killer(最后手段)

反向映射(reverse mapping)是高效回收一个page的关键:找到所有映射该page的虚拟地址并修改PTE。Linux通过anon_vma(匿名页)和 Priority(文件页)两种机制实现反向映射。

6. OOM Killer与cgroup内存控制

6.1 OOM Killer

当系统内存耗尽且无法回收时,OOM Killer根据oom_score(基于内存占用、运行时间、线程数、oom_score_adj等)选择oom_score最高的进程强制杀死。

6.2 cgroup memory

v1/v2 cgroup允许对一组进程限制内存使用(limit_in_bytes/memory.max)。超过limit时触发cgroup OOM(而非系统OOM)。v2引入high/max分层:

  • memory.high:超过触发回收压力(throttle但不OOM)
  • memory.max:超过直接OOM
  • memory.low:保护不被其他人挤占
  • memory.min:预留不可回收

这是容器/云原生环境下内存隔离的关键机制。

7. 内存热插拔与大页内存

7.1 内存热插拔

Linux支持在线添加/移除物理内存(通过ACPI/memory-probe)。关键步骤:

  • 新增Zone/DSection并入伙伴系统,先标记为OFFLINE
  • 调用add_pages()写入内核的mem_section
  • 迁移原有页到其他node(非必须)
  • 标记为ONLINE,伙伴系统开始分配

7.2 HugePage与THP

HugePage(大页)将页从4MB(2MB)/1GB等级,减少TLB缺失与页表级数,对数据库、DPDK等场景显著提升性能。

配置方法:

# 静态:内核启动参数hugepages=1024 hugepageszM=2M
# 动态:echo 1024 > /proc/sys/vm/nr_hugepages

THP(Transparent HugePage)内核自动将连续4KB页合并为大页:

  • madvise模式:仅在madvise(MADV_HUGEPAGE)标记区域启用
  • always模式:全量启用

对Kafka/DPDK等延迟敏感应用,THP可能导致内存碎片化和direct reclaim延迟抖动,建议关闭(always_never)并改用静态大页。

8. 生产环境内存调优实战

8.1 数据库场景

  • 设置vm.swappiness=1,尽量避免swap导致的性能抖动
  • 配置静态HugePage(nr_hugepages略小于物理内存)让数据库常驻
  • 关闭THP:echo never > /sys/kernel/mm/transparent_hugepage/enabled
  • vm.dirty_ratio/vm.dirty_background_ratio调节脏页刷新频率

8.2 网络数据包处理(DPDK)

  • 预留1GB大页内核启动参数:default_hugepagesz=1G hugepagesz=1G hugepages=16
  • 隔离空闲CPU核(isolcpus),DPDK PMD线程独享
  • 关闭irqbalance:避免中断核心切换导致TLB刷新
  • 禁用NUMA auto-balancing避免页面意外迁移

8.3 容器化场景(Kubernetes)

  • 设置memory requests/limits保持一致(QoS Guaranteed)
  • 监控container_memory_working_set_bytesRSS控制真实使用
  • Pod OOM触发后若频繁重启,应检查JVM堆/Xmx是否大于cgroup limit
  • 使用VerticalPodAutoscaler(VPA)自动推荐内存参数

9. 总结

Linux内核内存管理的核心设计思想:伙伴系统管理物理页级分配+SLUB处理小对象分配+LRU/反向映射实现页面回收。三者协同确保系统利用率与性能的最优平衡。

生产环境调优的关键实践包括:减少碎片化、禁用THP保证延迟、使用大页优化TLB、合理设置NUMA策略、使用cgroup隔离容器。

点赞(0) 打赏

评论列表 共有 0 条评论

暂无评论
立即
投稿

微信公众账号

微信扫一扫加关注

发表
评论
返回
顶部