引言

内存管理是Linux内核最核心也最复杂的子系统之一。从物理页面的分配与回收,到内核对象的细粒度管理,再到虚拟地址空间的映射与维护,整个内存管理栈涵盖了从硬件TLB到软件cgroup的多个层次。本文将以Binder System为起点,逐步深入SLUB分配器、vmalloc、OOM Killer、NUMA感知、cgroup限制和KSM等关键机制,结合源码分析和生产环境调优实践,帮助读者建立完整的内存管理知识体系。

1. 物理页面管理:Buddy System(伙伴系统)

伙伴系统是Linux物理内存管理的基础。它将连续的物理页面按2的幂次分组,管理11个不同阶(order 0-10)的页面块链表。阶为n的块包含2^n个连续页面(order 0=1页,order 10=1024页=4MB)。

分配时,系统从满足需求的最小阶链表中取出一个块。如果该阶链表为空,则从更高阶链表中取一个块,一分为二,取其一,将另一个放入低阶链表。回收时,检查相邻的"伙伴块"是否空闲,若空闲则合并为更大的块,逐级向上。

伙伴块判断的逻辑在__find_buddy_pfn中实现:两个页面帧号互为伙伴当且仅当它们只在第n位不同,即 buddy_pfn = pfn XOR (1 << order)。这个设计保证了O(1)的伙伴查找。

/proc/buddyinfo提供了各zone各阶空闲块数量的实时视图:

$ cat /proc/buddyinfo
Node 0, zone   Normal   216   985  1788  1276  531  210  104  48  22  10  3

解读:order 0(单页)有216个空闲块,order 1(双页)有985个,高阶块越少说明内存碎片化越严重。生产环境中若大阶块持续为0且频繁触发直接内存回收,可能意味着存在内存碎片或内存泄漏。

2. 内核对象分配:SLAB/SLUB/SLOB分配器

Buddy System以页(通常4KB)为最小分配单位,但内核中大量对象远小于一页(如task_struct约1.8KB、inode约592字节、dentry约192字节)。为高效管理小对象,内核实现了SLAB分配器的三种变体:SLAB(原始)、SLUB(默认)、SLOB(嵌入式)。

SLUB(Unqueued Slab Allocator)是当前的默认分配器。它的核心思想是将一页(或多个连续页面)切分为固定大小的object,使用free_list链表管理空闲object。每个CPU拥有独立的local slab和full/patch list以减少锁竞争。

SLUB的关键特性:

  • 每个cache(kmem_cache)对应一种对象大小,内核启动时创建大量专用cache(如task_struct_cachep、dentry_cache)和通用cache(kmalloc-8/16/32/64/128/.../8192)
  • 每个page属于一个slab,包含多个object和一个free_list
  • per-CPU partial链表减少全局锁争用
  • Red Zone检测越界访问,Freelist Hardening防止Use-After-Free
  • /proc/slabinfo提供cache使用统计:活跃对象数、总对象数、页面数、objs_per_slab等

kmalloc()底层就是选择合适的kmem_cache来分配,所以kmalloc(kbytes*1024)实际分配的是最接近的cache:kmalloc-32、kmalloc-64、kmalloc-128、...、kmalloc-8192。若要分配更大的内存,只能使用vmalloc或页分配器。

3. vmalloc:非连续物理内存的虚拟连续映射

kmalloc在物理上要求连续(DMA和32位系统上尤其严格),而vmalloc只需要虚拟地址连续,物理上可以通过多个不连续的页面拼接。vmalloc通过修改页表建立虚拟到物理的映射。

vmalloc的代价是:不连续的物理内存导致TLB未命中增多,且无法被DMA设备直接访问(除非使用scatter/gather I/O)。适用于需要大缓冲区但对物理连续性要求不高的场景:模块加载、视频帧缓冲、内核镜像解压等。

vmalloc分配器自身也是一个小的内存分配子系统,使用红黑树管理已分配的虚拟地址区间(area),通过get_vm_area找到合适的虚拟地址空洞,然后alloc_pages逐页分配物理页面并建立映射。vmalloc_area的链表:vmalloc → get_vm_area → __vmalloc_node_range → __vmalloc_area_node → alloc_page逐页 → map_vm_area。

4. 内存碎片整理(Memory Compaction)

长时间运行的系统中,物理页面因被不同进程/内核路径锁定和释放,导致连续性变差:可能总空闲页面能满足大块分配需求,但它们不连续,且高阶伙伴块链表为空。

内存碎片整理子系统(由/proc/sys/vm/compact_memory触发或kcompactd内核线程自动执行)扫描每个zone,利用页面迁移将可移动页面(用户空间匿名页、可回收页缓存等)聚合到一端,释放出来的连续页面可以拼凑成高阶块。

碎片整理的关键限制:页面必须可被迁移(不可移动的页面如内核持久数据结构会阻碍迁移)。/proc/pagetypeinfo展示了每个阶每个migratetype页面的分布情况,可帮助诊断碎片成因。

5. OOM Killer:内存耗尽时的自适应策略

当系统内存极度紧张,直接回收、碎片整理都无法释放足够内存时,OOM Killer被触发。它通过oom_badness()计算每个进程的"坏分数"(0-1000,对应0.0%-100.0%),选择得分最高的进程终止。

评分因子的演进策略:

  • 基础分 = 进程占用的总页面(物理页 + swap) / 系统总页面 × 1000
  • 调整因子:mmap共享内存打折扣、大页打折扣、CAP_SYS_ADMIN进程加权、oom_score_adj用户可干预
  • mm-oom:支持cgroup OOM killer,在cgroup层面独立触发OOM而无需等待全局OOM
  • panic_on_oom:全局策略控制是否panic而非kill

oom_score_adj(范围-1000到1000)是运维利器:将关键服务(如数据库)设为-1000可避免被OOM Killer选中,UI进程设为正值加速终止。

6. NUMA感知内存分配

NUMA架构下,CPU访问本地节点的内存速度快于跨节点访问。Linux通过NUMA Balancing(自动页面迁移)和NUMA-Aware分配策略来优化。

分配策略由/proc/sys/kernel/numa_balancing和/sys/devices/system/node/node*/meminfo控制:

  • MPOL_DEFAULT:从本地节点分配,fallback到跨节点
  • MPOL_BIND:限定在指定节点集合中分配
  • MPOL_INTERLEAVE:交替分配在指定节点中
  • MPOL_PREFERRED:优先本地节点

NUMA Balancing内核线程(khugepaged类似机制)扫描进程地址空间,发现远程访问频繁的页面后自动迁移到本地节点。可以通过taskset、numactl或mbind系统调用控制进程的NUMA策略。

在大内存系统中(>128GB),若NUMA感知缺失,跨节点访问延迟可能增长2-3倍,显著影响数据库和缓存性能。

7. 内存cgroup(memcg):精细化内存隔离

v2 memory控制器允许为cgroup设置硬限制(memory.max)、软限制(memory.high,触发回收压力但不OOM)和swap限制(memory.swap.max)。

当cgroup内存使用超过memory.high时,内核加强回收力度;超过memory.max时触发cgroup OOM,仅终止cgroup内的进程。这比全局OOM Killer更可控:一个容器OOM不会影响宿主机或其他容器。

memory.stat提供了详细的回收统计:anon(匿名页)、file(页缓存)、kernel_stack、pagetables、percpu、sock等分别计量。长期监控memory.stat中的workingset_refault可以判断是否存在内存抖动。

8. KSM(Kernel SamePage Merge):内存去重

KSM在内核中扫描可合并的页面(标记为MADV_MERGEABLE,通常由KVM/QEMU使用),将内容相同的页面合并为一页。这在一台宿主机运行大量相同OS的虚拟机时效果显著,可节省40%-60%内存。

KSM的工作机制:

  • ksmd内核线程周期性扫描注册区域
  • 使用红黑树存储稳定页面(已验证不会被修改),不稳定候选树用于新发现页面
  • /sys/kernel/mm/ksm/提供pages_shared、pages_sharing、pages_unshared、pages_volatile等统计

KSM的开销是CPU使用率。ksm_thread每轮扫描页数由pages_to_scan控制,扫描间隔由sleep_millisecs控制。在CPU受限场景下,KSM可能不如直接给虚拟机分配足够内存。

9. 生产环境调优实战

基于上述原理,以下是常见生产环境调优参数和策略:

9.1 回收与脏页

# 增加脏页刷新阈值,减少突发I/O
vm.dirty_ratio = 40
vm.dirty_background_ratio = 10

# 调整回收倾向:0=优先换出文件缓存,100=匿名页和文件缓存均衡
vm.swappiness = 10  # 数据库服务器设为10-20,桌面设为60

9.2 Huge Pages

# 启用2MB大页减少TLB缺失
echo 1024 > /proc/sys/vm/nr_hugepages

# 或THP(透明大页),但某些数据库(如Redis、PostgreSQL)建议关闭
echo never > /sys/kernel/mm/transparent_hugepage/enabled
echo never > /sys/kernel/mm/transparent_hugepage/defrag

9.3 OOM防御

# 关键服务oom_score_adj=-1000(永不kill)
echo -1000 > /proc/$(pidof mysqld)/oom_score_adj

# 内存紧张时倾向kill大内存进程而非小进程
vm.oom_kill_allocating_task = 1

9.4 监控与诊断

# 实时碎片概览
watch -n 1 cat /proc/buddyinfo

# SLUB分配器诊断
cat /proc/slabinfo | head -20

# PSI(Pressure Stall Information)感知内存压力
cat /proc/pressure/memory
some avg10=0.25 avg60=0.10 avg300=0.02 total=123456789

PSI是较新的内核特性(Linux 4.20+),通过some/full stall时间量化资源竞争程度。内存some pressure表示至少一个任务因内存延迟stall,full pressure表示所有任务都stall。avg10=0.25表示过去10秒25%的任务因内存延迟等待。

10. 总结与展望

Linux内存管理从底层的伙伴系统到高层的cgroup隔离,形成了一套自洽且高效的分层体系。理解Buddy System的2幂分组、SLUB的per-CPU slab管理、vmalloc的页表映射、OOM Killer的自适应评分、NUMA的拓扑感知、cgroup的软/硬限制和KSM的去重机制,对于系统调优和故障排查至关重要。

展望未来,Linux内存管理正在向以下方向演进:

  • Memory Folios:将Compound Page统一管理,简化Folio/Slab交互(Linux 5.16+推进)
  • MGLRU(Multi-Generational LRU):替代传统双链表LRU,更精准的页面回收决策(Linux 5.18+引入
  • PVMEM:CXL/ Persistent Memory的一等公民支持,拓展了持久内存管理
  • eBPF Observability:通过BPF程序追踪内存分配热点,实时诊断分配器行为

内存管理的复杂性是永恒的,但Linux社区通过持续创新和工程化,让开发者能够在不了解全部细节的情况下也能写出高效的内存使用者——这正是优秀基础设施的价值所在。

点赞(0) 打赏

评论列表 共有 0 条评论

暂无评论
立即
投稿

微信公众账号

微信扫一扫加关注

发表
评论
返回
顶部