引言

内存管理是Linux内核中最复杂且最核心的子系统之一。它负责在有限的物理内存资源上为成千上万的用户空间进程和内核本身提供高效、安全的内存分配服务。从早期的简单分页机制,到如今支持NUMA、透明大页(THP)、内存控制组(cgroups v2)等高级特性的现代内存管理器,Linux的内存管理已经发展成为一个多层级的精密体系。

本文将从实战角度系统讲解Linux内核内存管理的核心机制,包括底层页框分配器(Buddy System)、内核对象缓存(SLAB/SLUB)、页面置换与回收策略(LRU)、Out-of-Memory Killer的处理逻辑、连续内存分配器(CMA)、zswap压缩交换,以及容器化环境下的内存隔离实践。每个部分都配有真实场景的性能分析工具和调优参数。

一、内存管理架构总览

1.1 多级页表与虚拟内存

Linux采用多级页表(在x86_64架构下通常为4级:PGD→PUD→PMD→PTE)将进程的虚拟地址空间映射到物理页框。这种设计既节省了页表本身的内存占用(稀疏地址空间下无需为未使用的中间层级分配内存),又实现了内存的按需分配——进程申请内存时仅分配虚拟地址,实际物理页框在首次访问时通过缺页异常(page fault)分配。

每个进程拥有独立的47位(五级页表下57位)虚拟地址空间:用户空间占据低地址区域,内核空间占据高地址。内核空间的映射在所有进程间共享,这意味着TLB在用户态和内核态切换时无需刷新整个缓存。

1.2 Zone与NUMA架构

物理内存按硬件约束划分为不同的Zone:ZONE_DMA(前16MB,兼容旧ISA设备)、ZONE_DMA32(4GB内DMA)、ZONE_NORMAL(直接映射内核空间)、ZONE_HIGHMEM(仅32位系统需要)。在64位系统中,ZONE_NORMAL通常足够大,HIGHMEM已很少使用。

NUMA(非一致性内存访问)架构下,多处理器系统中每个CPU拥有本地内存节点和远端内存节点。Linux通过per-node的buddy allocator和per-node的LRU链表来实现NUMA感知的内存分配,优先从本地节点分配以减少跨节点访问延迟。

1.3 内存管理层次

空间层次:节点(pg_data_t) → Zone(dma/normal/highmem) → 页框(page, 4KB) → 对象(slab cache)

分配路径:用户malloc() → brk()/mmap() → 页错误 → __alloc_pages() → Buddy System → 物理页框 → 进程页表建立映射

分配层次:用户空间通过mmap/brk获取虚拟内存→首次访问触发page fault→内核通过Buddy分配物理页框→内核空间通过kmalloc/vmalloc获取小对象和虚拟连续内存→最终由SLAB/SLUB从页框分配器获取整页并切分为对象缓存。

二、Buddy System 伙伴分配器

2.1 核心原理

Buddy System是Linux内核管理物理页框的基础分配器。它将空闲页框组织为11个链表数组(order 0到order 10),每个链表包含大小为2^order个连续页框的内存块(order 0 = 1页=4KB, order 10 = 1024页=4MB)。这种指数级分块设计使得分配和释放都能在对数时间内完成。

分配过程:要申请N页,内核向上取整到2的幂次order值,在对应order的空闲链表上摘下一块。如果该order链表为空,则递归从更高order链表中取下一块,一分为二(称为"伙伴"),一半返回请求者,另一半插入低一级order链表。

释放过程:释放时检查其"伙伴"页框是否在对应order的空闲链表中。如果是则合并成高一阶的大块,此过程递归进行直到无法合并或达到最大order。判断两块是否为伙伴的依据是:它们大小相同、物理地址连续、且合并后的起始地址对齐到2^(order+1)的边界。

/proc/buddyinfo 查看伙伴系统状态:

$ cat /proc/buddyinfo
Node 0, zone   Normal   212   398  236  107   44   23   10    4    1    0    0

输出从左到右依次为order 0到order 10的空闲块数量。如果高阶order(8-10)始终为0而低阶order有大量碎片,说明存在内存碎片化问题。

2.2 分配掩码 (GFP flags)

kmalloc和alloc_pages等分配函数通过GFP flags控制内存分配行为:

  • GFP_KERNEL:标准内核分配,允许睡眠等待页面回收。最常用但可能在内存紧张时引发递归回收。
  • GFP_ATOMIC:原子分配,禁止中断和IO。用于中断处理、自旋锁上下文等不可睡眠的场景。分配失败概率较高。
  • GFP_NOIO/GFP_NOFS:禁止发起IO/文件系统操作,避免回写循环导致的死锁。
  • GFP_HIGHUSER/HIGHMEM_USER:用户空间分配,可访问HIGHMEM。
  • __GFP_ZERO:返回清零的安全内存。
  • __GFP_NOWARN/__GFP_NORETRY:失败不告警/不重试,适合可选分配路径。

2.3 页面迁移与内存规整 (Memory Compaction)

长期运行的系统会产生外部碎片——虽然总空闲内存充足,但物理上分散在不连续页框中,导致无法分配高阶连续内存块(如透明大页需要2MB连续内存)。Linux通过Memory Compaction(内存规整)解决此问题:

/proc/sys/vm/compact_memory:手动触发全系统内存规整

/proc/sys/vm/compaction_proactiveness:(5.12+)控制内核主动触发规整的积极程度(0-100,默认0关闭主动规整)

规整分为两种策略:页面迁移(将可移动页框搬到一侧,另一侧形成连续空闲块)和页面回收(直接回收可回收页)。内核在低阶内存分配失败、透明大页分配失败、或khugepaged守护进程执行时会触发。

三、SLAB/SLUB/SLOB 对象分配器

3.1 SLAB 设计哲学

SLAB分配器(及其继任者SLUB)运行在Buddy System之上,专门用于内核频繁分配释放的小对象(如task_struct、inode、dentry等)。如果这些对象每次诞生消亡都走Buddy System的页级分配,会产生大量的分配内部碎片和初始化/销毁开销。

SLAB将一个或多个连续页框划入一个Cache,Cache内部将该内存区域切分为等大小的对象槽位。对象被获取后存放于Cache的slot中,释放时不归还Buddy而是标记为free并回到Cache的freelist。这带来了两个显著优势:消除了内部碎片和摊薄了构造/析构成本。

3.2 SLAB vs SLUB

SLAB是最早的实现,由SunOS的Jeff Bonwick设计并引入Linux 2.1.23。它通过kmem_cache管理多种大小的对象类型,每个queue通过硬件缓存着色(cache coloring)减少CPU cache line的false sharing。缺点是多处理器场景下queue管理复杂、debug开销高、metadata占用较大。

SLUB是Linux 2.6.23开始默认的分配器,由Christoph Lameter设计。核心简化思路是将per-CPU和per-node的管理结构大幅精简:每个CPU只维护一个active slab和一个partial list;memory node维护一个partial slabs链表。SLUB的关键优化包括:

  • 合并了per-cpu freelist,减少了lock contention
  • Freelist以嵌入式指针方式紧跟在page结构体中
  • 空闲对象内部存储freelist链表指针
  • 支持red zoning(对象尾部放magic number检测越界)、poisoning(释放时填充0x5a帮助检测use-after-free)

SLOB是最简实现,使用first-fit策略管理内存,专为嵌入式设备设计。

3.3 常用的 kmalloc slab 大小

内核维护了一组预建的kmem_cache,名为kmalloc-32到kmalloc-131072,常见大小有32, 64, 128, 256, 512, 1024, 2048, 4096, 8192, 16384, 32768, 65536, 131072字节。调用kmalloc(100)实际从kmalloc-128的cache中分配。

3.4 /proc/slabinfo 分析

$ cat /proc/slabinfo | head -20
slabinfo - version: 2.1
# name            <active_objs> <num_objs> <objsize> <objperslab> <pagesperslab> : ... 
dentry              384452  418811     192   21    1 : ...
inode_cache         113420  118536     632   26    2 : ...
buffer_head         244243  244243     104   37    1 : ...

通过slabtop可以实时监控各slab cache的变化速度。

四、页面置换与回收

4.1 LRU 算法与双层链表

当系统空闲内存低于阈值时,内核通过kswapd守护进程异步或直接通过分配者同步执行页面回收。Linux使用改进的LRU(Least Recently Used)算法来选择牺牲页面。

Linux的解决方案是Two-List LRU:

  • Active List:近期被访问过的页面。由硬件引用位驱动,被访问时页面留在或晋升至Active List头部。
  • Inactive List:近期未被访问的页面。在扫描过程中被访问过的页面从Inactive移到Active尾部。Inactive List尾部的页面是最久未被访问的,最优先被回收。

4.2 交换(Swap)机制

swap有三种形式:Swap Partition(传统方式)、Swap File(文件形式的swap,更灵活)、zram/zswap(压缩的内存交换设备)。

swappiness参数控制swap倾向: /proc/sys/vm/swappiness 取值0-200,默认60。对于Redis、Elasticsearch等需要大量工作集驻留内存的应用,通常将swappiness设为1-10甚至0。

4.3 页面回收水位与直接回收 (Direct Reclaim)

每个Zone有三个水位标线(min, low, high):

  • free > high:页面充足,无回收操作
  • low < free <= high:kswapd开始异步回收
  • min < free <= low:kswapd加大回收力度
  • free <= min:发生Direct Reclaim——分配进程自身暂停,同步执行页面回收

Direct Reclaim是性能杀手。进程被阻塞在回收路径上,且回收可能涉及IO操作。

4.4 页面回收调优参数

  • /proc/sys/vm/vfs_cache_pressure(默认100):控制dentry/inode cache的回收积极性
  • /proc/sys/vm/zone_reclaim_mode(默认0):NUMA模式下本地节点耗尽时的行为
  • /proc/sys/vm/extfrag_threshold(默认500):是否跳过compaction的概率容忍值

五、OOM Killer 机制

5.1 触发流程

当系统所有Zone内存告急、kswapd和Direct Reclaim都无法回收足够页面时,内核触发Out-of-Memory Killer选择并终止进程以释放物理内存。

badness评分机制:

  • 占内存比例:进程使用的virtual memory占比越高,越容易被选为OOM目标
  • oom_score_adj:-1000到1000的用户可配置偏移。-1000表示永不kill(关键系统服务、SSH等)
  • 特权进程(root进程)评分乘以0.7倍(更不容易被选中)

5.2 OOM Killer 防护配置

设置进程OOM保护:echo -1000 > /proc/[pid]/oom_score_adj

强制OOM前回写buffer:/proc/sys/vm/oom_kill_allocating_task设为1会直接杀死触发OOM的进程而非实行评分搜索。

通过cgroup限制内存导致的OOM:容器内存超限时由cgroup OOM独立触发、不执行系统级OOM Killer。

六、zram 与 zswap 压缩交换

6.1 zram:内存中的块设备

zram将一块内存区域作为压缩块设备。写入zram的数据在页级别按LZO/LZ4/ZSTD算法实时压缩后驻留物理内存。典型压缩比约2:1-3:1。

现代云厂商配置示例:

\$ modprobe zram num_devices=1
\$ echo lz4 > /sys/block/zram0/comp_algorithm
\$ echo 2G > /sys/block/zram0/disksize
\$ mkswap /dev/zram0
\$ swapon /dev/zram0 -p 100

6.2 zswap:压缩的写回缓存

zswap是写回缓冲层(writeback cache),不是块设备。当kswapd准备将匿名页swap到磁盘时,zswap拦截该操作,将页压缩存到内存池里。当被访问时解压读回(fast path)。只有内存池满时,最久的压缩页才写出到后端swap设备。

启用:echo 1 > /sys/module/zswap/parameters/enabled,可配置压缩算法(lz4/zstd)和内存池分配器(z3fold/zbud)。

七、CMA 连续内存分配器

CMA(Contiguous Memory Allocation)允许在系统预留一段可复用的连续大内存区域。该区域在未被申请时可供普通进程使用;一旦设备驱动需要连续物理内存做DMA时,内核回收或迁移该区域中的可移动页面。

八、高级特性与实战优化

8.1 透明大页(THP)

Transparent Huge Pages将多个连续小页(4KB)动态合并为2MB或1GB的大页,以减少TLB Miss和页表遍历开销。

always模式始终尝试合并;madvise通过madvise(MADV_HUGEPAGE)显式标记区域。

THP碎片化风险:数据库和虚拟化环境(Oracle、SAP HANA、KVM业界默认)通常建议关闭THP,由应用使用显式hugetlbfs。

8.2 PSI 资源压力信息

PSI(Pressure Stall Information,4.20+内核)提供了精准的资源排队等待分析:

\$ cat /proc/pressure/memory
some avg10=2.37 avg60=0.70 avg300=0.37 total=284735655
full avg10=1.11 avg60=0.20 avg300=0.09 total=203143796
  • some:至少一个任务因内存压力被阻塞的比值
  • full:所有任务同时因内存阻塞的比值(严重)

8.3 容器内存管理(cgroup v2)

  • memory.max:硬限制,触发即做cgroup内部OOM
  • memory.high:软限制,超限时执行受限的回收而非OOM
  • memory.low:尽力而为的保护量
  • memory.min:绝对不回收区域
  • swap.max:独立控制cgroup的swap使用量

九、实战排错与性能监控

9.1 诊断内存泄漏

slab层面:slabtop -o持续观察特定cache的active_objs是否有增速不回落。

Page Table层面:grep -e 'AnonPages' -e 'Shmem' /proc/meminfo观测匿名页趋势。若RSS持续增长但无法定位到具体进程,需kmemleak检测内核泄漏。

9.2 监控工具矩阵

free -h / vmstat -s:快速概览内存总量/分类

vmstat 1 / sar -r 1:实时页面错误、回收速率

sar -B 1:页面统计表(pgpgin/pgpgout/pswpin/pswpout)

ps --sort=-%mem -eo pid,user,rss,comm:快速排序RSS

9.3 内核参数最佳实践速查

数据库专用:swappiness=1, THP=never, zone_reclaim_mode=1, min_free_kbytes(提升)

Redis/Memcached:swappiness=0(或1), vm.overcommit_memory=1, THP=never

容器平台:启用memory cgroup v2, 启用PSI监控, 合理设置limits

桌面开发:swappiness=60(default), THP=madvise, zswap启用

IoT/嵌入式:启用zram, 使用SLUB, CMA预留给视频

十、总结

Linux内核内存管理是一个经数十年演进的多层精密体系:Buddy System解决物理页框分配与碎片问题,SLAB/SLUB加速小对象的频繁创建销毁,LRU+二级链表高效管理页面冷热状态,CMA和compaction确保连续内存需求可控,OOM Killer和PSI提供最后的兜底保护与可观测性。

在现代容器化环境中,内存管理的边界已超越单机的物理约束——cgroup v2的层次化内存账户、异步回收与回收压力传递、以及cgroup级别的OOM隔离,使得编排系统能够像一个"分布式内存池"般进行管理。建议读者亲自部署一套带PSI监控和eBPF memleak探测的观察体系,从指标驱动转向内核行为诊断。

点赞(0) 打赏

评论列表 共有 0 条评论

暂无评论
立即
投稿

微信公众账号

微信扫一扫加关注

发表
评论
返回
顶部