尧图精选

Linux 内存管理机制:从虚拟内存到物理页框的完整剖析

🕒 发布时间:2026/9/8 1:27:12 📁 来源:尧图网络
1. 引言为什么需要深入理解 Linux 内存管理内存管理是操作系统最核心、最复杂的子系统之一。Linux 作为一个通用的多任务、多用户操作系统必须同时满足以下看似矛盾的需求让每个进程都以为自己独占一块连续的大内存同时又要高效地复用有限的物理内存既要保证内核自身运行的稳定又要防止用户程序越界破坏系统既要支持 GB 级甚至 TB 级的大内存服务器又要在只有几百 MB 内存的嵌入式设备上良好运行。为了实现这些目标Linux 内核构建了一套层次分明、高度抽象的体系向上通过虚拟内存、地址空间和映射接口为进程提供隔离且灵活的编程模型向下通过页框管理、伙伴系统、Slab 分配器和页面回收机制精细地管理每一块物理内存。理解这套机制不仅有助于排查内存泄漏、OOM、性能抖动等线上故障也是进行内核开发、系统调优和性能优化的重要基础。本文将从虚拟地址和物理地址的关系出发沿着「地址翻译——物理页框分配——内核对象分配——进程地址空间——页面异常与写入——内存回收——反碎片化——大页与 NUMA——用户态与内核态分配接口——故障排查」这条主线系统性地剖析 Linux 内存管理机制。文中会结合内核关键数据结构、核心代码路径和实际调优参数尽量在原理和工程实践之间取得平衡。2. 内存管理基础概念2.1 虚拟地址、物理地址与总线地址在 Linux 中地址通常分为三类。物理地址是内存条上真实存在的地址由内存控制器和 CPU 共同访问虚拟地址是 CPU 执行指令时使用的地址每个进程都有自己独立的虚拟地址空间在部分架构上还存在总线地址也就是外设经由总线看到的地址在 x86 上通常与物理地址一致但在部分嵌入式架构中可能存在偏移。CPU 发出的是虚拟地址必须经过 MMUMemory Management Unit内存管理单元翻译为物理地址后才能真正访问内存。翻译的基本单位是页Pagex86_64 架构下常规页大小为 4 KB同时支持 2 MB 和 1 GB 的大页。翻译关系存放在页表Page Table中由操作系统维护MMU 在硬件层面完成查表过程。2.2 页、页框与页表项虚拟内存按页组织物理内存按页框Page Frame组织。一个虚拟页通过页表项PTEPage Table Entry映射到一个物理页框。页表项除了保存物理页框号之外还包含一组权限和状态位常见的有PresentP页是否在物理内存中。Read/WriteR/W内存映射未导出请跳转【最新版本原文】页是否可写。User/SupervisorU/S用户态是否可访问。AccessedA页是否被访问过用于页面老化与回收。DirtyD页是否被写过用于回写与换出。Linux 对页表进行了抽象通过多层级的 pte 操作宏屏蔽不同架构的差异使核心内存管理代码能够跨平台复用。3. 虚拟内存与进程地址空间3.1 进程地址空间布局在 64 位 Linux 系统上一个进程的虚拟地址空间理论可达 128 TB 甚至更大。以传统布局为例地址空间大致可以分为以下几块代码段Text Segment存放可执行指令通常只读。数据段Data Segment存放已初始化的全局变量和静态变量。BSS 段存放未初始化的全局变量和静态变量内存初始值默认为 0。堆Heap由 brk 系统调用向上增长供 malloc 分配内存使用。内存映射区mmap 区用于动态库映射、文件映射和匿名内存映射。栈Stack存放函数调用栈、局部变量向下增长。内核空间位于地址空间顶部不同进程共享用户态不可直接访问。3.2 用户空间与内核空间的划分x86_64 架构下用户态地址空间通常位于低地址部分内核空间位于高地址部分。传统 4 级页表下用户空间和内核空间各占一半启用 5 级页表后格局会发生变化。内核空间对所有进程共享同一套页表因此在进程上下文切换时内核部分的页表无需切换只需切换用户态部分的页表基址寄存器从而降低了上下文切换开销。3.3 查看进程内存布局通过/proc/[pid]/maps可以查看某个进程的详细内存映射每一行表示一个虚拟内存区域包含起止地址、权限、偏移、映射文件等信息。下面的命令展示了当前 shell 进程的部分内存布局。cat /proc/self/maps输出形式大致如下00400000-0040b000 r-xp 00000000 fd:01 123456 /usr/bin/cat 0060a000-0060b000 r--p 0000a000 fd:01 123456 /usr/bin/cat 0060b000-0060c000 rw-p 0000b000 fd:01 123456 /usr/bin/cat 7f0000000000-7f0000021000 rw-p 00000000 00:00 0 [heap] 7fff00000000-7fff00021000 r-xp 00000000 fd:01 654321 /usr/lib/libc.so.6 7ffffffde000-7ffffffff000 rw-p 00000000 00:00 0 [stack]每一列分别表示起始结束地址、访问权限、文件偏移、设备号、inode 号和映射路径。理解 maps 的格式是后续分析进程内存问题的基本功。4. MMU 与分页机制4.1 MMU 的职责MMU 是 CPU 内部的硬件单元负责在每次内存访问时把虚拟地址翻译为物理地址并检查访问权限。翻译过程依赖操作系统写入的页表。MMU 的翻译不是简单的线性计算而是需要多次查表因此为了加速MMU 内部设计了 TLBTranslation Lookaside Buffer转换后备缓冲区来缓存最近的翻译结果。当 CPU 访问一个虚拟地址时MMU 首先查询 TLB。如果命中则直接得到物理地址如果未命中就需要遍历内存中的多级页表这个过程称为页表遍历Page Table Walk。页表遍历成功后翻译结果会写回 TLB后续访问相同页即可加速。4.2 x86_64 的四级页表x86_64 架构典型情况下使用四级页表在启用 LA57 特性时使用五级页表。一个 48 位虚拟地址被拆分为五个部分PGD 索引9 位定位页全局目录。PUD 索引9 位定位页上级目录。PMD 索引9 位定位页中间目录。PTE 索引9 位定位页表项。页内偏移12 位定位 4 KB 页内的具体字节。虚拟地址48 位 | PGD | PUD | PMD | PTE | Offset | | 9 | 9 | 9 | 9 | 12 |每一级页表都占用一个物理页页表项指向下一级页表的物理地址。由于页表占用的内存可能很大Linux 和硬件都支持按需分配页表只在真正需要时才为某一级页表分配物理页。4.3 TLB 刷新与地址空间切换由于 TLB 缓存的是虚拟地址到物理地址的映射而不同进程使用不同的虚拟地址空间因此进程切换时需要刷新 TLB 中属于旧进程的条目。x86 架构通过切换 CR3 寄存器来切换页表基址切换 CR3 本身会隐式刷新不属于全局页的 TLB 条目。为了减少 TLB 刷新开销内核会把内核空间的页表项标记为全局Global 位这样进程切换时内核部分的 TLB 条目得以保留。5. 物理内存管理框架5.1 内存节点 Node在现代服务器上尤其是 NUMANon-Uniform Memory Access非一致性内存访问架构下不同 CPU 访问不同内存的速度不同。Linux 用pg_data_t结构表示一个内存节点Node每个节点拥有自己的内存区域、页框管理结构和回收状态。每个 CPU 都归属于某个节点访问本节点内存速度最快访问远端节点内存则较慢。通过以下命令可以查看系统的 NUMA 拓扑numactl --hardware在单节点系统上整个系统只有一个 Node内存管理逻辑可以在不感知 NUMA 的情况下运行。5.2 内存区域 Zone即便在一个节点内部物理内存也不是完全等价的。由于历史原因和 DMA 设备寻址能力限制Linux 把物理内存划分为多个区域ZoneZONE_DMA低地址内存兼容老式 DMA 设备。ZONE_DMA3232 位设备可寻址的内存。ZONE_NORMAL内核直接映射的常规内存区域。ZONE_HIGHMEM32 位系统上的高端内存64 位系统通常不存在。ZONE_MOVABLE用于内存热插拔和反碎片化的可移动内存区域。每个 Zone 内部维护空闲页列表、水位线和各类统计信息。Zone 的划分使得内核可以在分配内存时优先选择合适的区域并为内存回收提供按区域的决策依据。5.3 页框与 struct page物理内存被划分为一个个页框通常大小为 4 KB。每一个物理页框都对应一个struct page结构用于描述该页的元数据包括引用计数、映射信息、LRU 链表指针、所属内存区域等。struct page本身也要占用内存在系统启动阶段内核会为所有物理页建立对应的struct page数组。可通过/proc/meminfo查看系统物理内存概况cat /proc/meminfo输出中包含MemTotal、MemFree、MemAvailable、Buffers、Cached等关键指标。其中MemAvailable比MemFree更能反映系统实际可用的内存因为它把可以回收的缓存也计入其中。6. Buddy 伙伴系统6.1 设计目标Buddy 伙伴系统是 Linux 物理页框分配的核心算法主要解决两个问题高效地满足不同大小的连续物理页请求以及尽量避免外部碎片。Buddy 系统以连续页框为单位进行分配分配粒度是 2 的幂次方个页框称为一个阶Order。例如 order 0 表示 1 个页框order 3 表示 8 个连续页框。6.2 伙伴系统的组织方式每个 Zone 维护一组空闲列表每个空闲列表对应一个 order。分配时内核从满足要求的 order 空闲列表中取出一个连续页块如果没有合适大小的空闲块就向更高 order 的空闲列表查找并将大块递归地一分为二直到得到所需大小的小块。释放时内核检查释放块的伙伴是否也空闲如果空闲则合并为更大的块并继续向上合并直到伙伴处于使用状态或达到最大 order。判断两个块是否为伙伴依据的是它们的物理地址是否满足特定条件。这种二进制分裂与合并的特性正是「伙伴」系统名称的由来。6.3 GFP 标志内核在调用 Buddy 系统分配内存时需要传递 GFPGet Free Page标志指定分配行为和约束GFP_KERNEL常规内核内存分配可能睡眠允许触发回收和换出。GFP_ATOMIC原子上下文分配不允许睡眠常用于中断和自旋锁保护路径。GFP_NOWAIT不等待立即返回不触发回收。__GFP_ZERO分配后清零内存。__GFP_HIGHMEM允许从高端内存分配。GFP 标志同时影响分配路径是否允许进入慢速路径、是否允许触发页面回收以及从哪个 Zone 开始分配是理解内核内存行为的关键。6.4 分配与释放核心接口内核中最底层的页分配接口是alloc_pages和__free_pages。下面是一个简单示例演示分配 2 的 order 次方个物理页并释放。#include linux/gfp.h #include linux/mm.h struct page *pages; pages alloc_pages(GFP_KERNEL, 3); /* 分配 2^3 8 个连续页 */ if (!pages) { pr_err(alloc_pages failed\n); return -ENOMEM; } /* 使用 page_address 获取内核虚拟地址 */ void *addr page_address(pages); memset(addr, 0, 8 * PAGE_SIZE); __free_pages(pages, 3);实际开发中更常用的是基于 Buddy 系统的封装接口get_free_pages和free_pages它们直接返回内核虚拟地址使用更方便。#include linux/gfp.h #include linux/slab.h unsigned long addr; addr __get_free_pages(GFP_KERNEL, 2); /* 分配 4 个连续页 */ if (!addr) return -ENOMEM; strcpy((char *)addr, hello page); free_pages(addr, 2);7. Slab / Slub 分配器7.1 为什么需要 SlabBuddy 系统以页为单位分配内存如果内核频繁地申请和释放几十字节的小对象直接使用 Buddy 系统会造成极大的内存浪费和性能开销。Slab 分配器建立在 Buddy 系统之上以「对象缓存」的方式管理小内存对象的分配适用于内核中大量重复创建的对象例如inode、dentry、task_struct等。7.2 Slab 的核心概念Slab 分配器维护多个缓存Cache每个缓存对应一类对象。一个缓存由多个 Slab 组成一个 Slab 通常由一个或多个连续物理页构成内部被切分为若干相同大小的对象槽位。Slab 有三种状态空闲 Slab、部分满 Slab 和全满 Slab。分配时优先从部分满 Slab 中取对象释放时把对象归还到所属 Slab当 Slab 全空时可将其归还给 Buddy 系统。每个 CPU 还拥有本地对象缓存Per-CPU Cache分配和释放小对象时优先操作本地缓存减少了跨 CPU 竞争锁的开销。这个设计在高并发内核路径上非常重要。7.3 Slub 的改进Linux 2.6 之后引入 Slub 分配器并逐渐成为默认实现。Slub 保留了 Slab 的缓存概念但简化了元数据管理去掉了部分复杂的着色和队列机制把空闲对象组织为链表显著降低了元数据开销和代码复杂度同时针对大对象和调试场景做了优化。7.4 常用分配接口内核中常用的kmalloc和kfree就是基于 Slub 的通用大小分配接口。下表列出了常用的内核内存分配接口及其特点。接口连续性是否可能睡眠典型用途kmalloc物理连续取决于 GFP 标志小对象、普通内核对象vmalloc虚拟连续物理可不连续可能睡眠大块内存kzalloc物理连续取决于 GFP 标志需要清零的小对象kmem_cache_alloc物理连续取决于 GFP 标志频繁创建的对象下面是一个使用kmem_cache创建专用对象缓存的示例#include linux/slab.h struct my_object { int id; char name[32]; }; static struct kmem_cache *my_cache; static int __init my_init(void) { my_cache kmem_cache_create(my_object_cache, sizeof(struct my_object), 0, SLAB_HWCACHE_ALIGN, NULL); if (!my_cache) return -ENOMEM; struct my_object *obj kmem_cache_alloc(my_cache, GFP_KERNEL); if (!obj) { kmem_cache_destroy(my_cache); return -ENOMEM; } obj-id 1; strcpy(obj-name, slab demo); kmem_cache_free(my_cache, obj); return 0; } static void __exit my_exit(void) { kmem_cache_destroy(my_cache); }7.5 查看 Slab 信息可以通过/proc/slabinfo查看系统当前的 Slab 缓存情况cat /proc/slabinfo | head -20输出包含每个缓存的名称、活动对象数、总对象数、对象大小和 Slab 数量等信息是分析内核内存占用和对象泄漏的常用工具。8. 进程地址空间管理8.1 mm_struct 与 vm_area_struct每个进程的地址空间由mm_struct结构描述该结构保存页表指针、代码段/数据段边界、内存映射树、引用计数等核心信息。地址空间内部的具体映射区域则由vm_area_structVMA描述。一个 VMA 表示一段连续的、具有相同属性和映射来源的虚拟地址区间例如一段代码段、一段堆或一个 mmap 文件映射。VMA 的关键字段包括起止地址、访问权限、映射标志、映射文件对象和对应的文件偏移等。进程的大量 VMA 通过红黑树组织以支持快速查找任意虚拟地址所属的映射同时通过链表组织以支持遍历全部映射。8.2 堆的扩展brk 与 mmap用户态调用malloc分配小内存时glibc 通常优先通过brk系统调用向上扩展堆顶。当请求较大时glibc 会改用mmap在内存映射区创建一块新的映射。映射创建之后物理页并不会立即分配而是延后到进程真正访问对应页时由缺页异常处理程序完成分配。brk直接调整进程mm_struct中的堆边界而mmap则新建一个 VMA。两者的共同点是都只修改地址空间描述不立即分配物理内存这也是 Linux 内存管理「惰性分配」思想的体现。8.3 内核中的地址空间操作内核描述地址空间时使用mm_struct、vm_area_struct以及页表三级结构。分配物理页、更新页表、处理写时复制等操作都围绕这三者展开。理解 VMA 与页表的关系是理解缺页异常、mmap 和内存回收的关键。9. 缺页异常处理9.1 缺页异常的分类进程访问一个虚拟地址时如果页表项中不存在有效映射CPU 会触发缺页异常Page Fault。Linux 的缺页异常处理程序需要根据异常的原因和上下文判断如何处理。常见情形包括访问未映射地址通常导致段错误SIGSEGV。映射存在但物理页尚未分配按需求分配物理页并填充页表。写只读页触发写时复制复制页面后重新映射为可写。访问已被换出的页从交换分区换入物理页。访问文件映射但页未加载从磁盘读取文件内容到页缓存。9.2 匿名页与文件页从内容来源看内存页可分为匿名页Anonymous Page和文件页File Page。匿名页不与文件关联例如堆、栈和 mmap 匿名映射当其内容需要换出时写入交换分区文件页与文件的某段区域对应换出时直接回写到原文件或丢弃后重新读取。匿名页和文件页在回收策略上的差异是理解系统内存行为的重要线索。9.3 主缺页与次缺页缺页异常还可分为主缺页Major Fault和次缺页Minor Fault。主缺页表示必须访问磁盘才能完成例如从交换分区换入或从文件读取数据次缺页表示只需在内存中完成例如写时复制、零页合并或者页已经在页缓存中。主缺页会带来明显的 I/O 延迟因此频繁的主缺页通常是性能问题的信号可以通过perf stat或sar -B观察。sar -B 1 510. 写时复制 COW10.1 fork 与页表复制当进程调用fork创建子进程时内核并不立即复制父进程的全部物理内存而是复制父进程的页表并把父子双方的页表项都标记为只读。此时父子进程共享相同的物理页。只有当其中一方尝试写入某个共享页时CPU 才会触发写保护缺页异常内核此时为写入方分配一个新物理页复制原页内容并修改对应页表项为可写。这种机制称为写时复制Copy On WriteCOW。10.2 COW 的实现要点COW 能大幅降低 fork 的开销因为大多数情况下子进程 fork 之后很快执行 exec 加载新程序真正需要修改的内存页很少。COW 生效的关键在于页表项被置为只读的同时需要在内核中记录这些页是「可写共享」的以便在写保护异常中识别并执行复制而不是简单报错。此外COW 也用于mmap(MAP_PRIVATE)私有映射。进程可以把文件以只读共享方式映射写入时触发 COW 复制到匿名页对文件的修改不会回写从而实现进程私有副本。11. 内存映射 mmap11.1 mmap 的类型mmap系统调用是用户态申请和管理内存的重要接口分为两类匿名映射不与文件关联映射区域初始为 0用于 malloc 大块内存、共享内存等。文件映射把文件内容映射到虚拟地址空间读写内存等价于读写文件。文件映射又可以按共享属性分为共享映射MAP_SHARED和私有映射MAP_PRIVATE。共享映射的修改会写回文件私有映射的写操作触发 COW不影响原文件。11.2 mmap 的内存效率使用mmap映射文件读入页面时采用按需加载只有真正访问到对应页时才从磁盘读取。对于大文件随机访问场景mmap 可以避免read系统调用的多次拷贝和页缓存重复管理有时能获得更好的性能和更简洁的代码。但 mmap 也有代价写文件映射可能触发大量脏页和复杂的回写逻辑映射区域的地址对齐和长度受页大小限制。下面的 C 示例使用 mmap 把文件映射到内存并统计字符。#include sys/mman.h #include sys/stat.h #include fcntl.h #include unistd.h #include stdio.h int main(int argc, char **argv) { int fd open(argv[1], O_RDONLY); if (fd 0) { perror(open); return 1; } struct stat st; fstat(fd, amp;st); char *data mmap(NULL, st.st_size, PROT_READ, MAP_PRIVATE, fd, 0); if (data MAP_FAILED) { perror(mmap); return 1; } long count 0; for (off_t i 0; i lt; st.st_size; i) { if (data[i] a) count; } printf(a count: %ld\n, count); munmap(data, st.st_size); close(fd); return 0; }11.3 用户态 malloc 与内核的协作glibc 的malloc并非每次都直接陷入内核。对于小内存请求glibc 维护自己的空闲块链表和线程缓存tcache优先在用户态完成分配只有当空闲内存不足时才通过brk或mmap向内核申请更大的内存块再由 glibc 切分。因此用户态看一个进程的 RSS 常常大于其实际 malloc 的活跃内存量原因之一就是 glibc 和内核的惰性分配及延迟归还策略。12. 页面回收与 LRU12.1 为什么要回收页面系统运行时间越长未被主动释放的内存页越多。为了应对新的内存分配请求Linux 必须回收一部分「不活跃」的页面。回收对象包括文件缓存页、匿名页、Slab 缓存等。回收的目标是在保证工作集不被频繁换出的前提下尽可能维持空闲内存水位避免频繁进入慢速分配路径。12.2 LRU 链表Linux 使用 LRULeast Recently Used最近最少使用链表管理可回收页。传统的 LRU 分为活跃链表和非活跃链表页面在「访问」和「不访问」之间升降级。文件页和匿名页分别维护自己的活跃和非活跃链表以支持差异化的回收策略。页面的 Accessed 位由硬件设置内核周期性扫描并清除该位从而判断页面是否在最近一轮扫描中被访问过。内核提供/proc/meminfo中的Active(file)、Inactive(file)、Active(anon)、Inactive(anon)统计反映不同类型页在 LRU 链表中的分布。12.3 两代 LRU 与工作集简单 LRU 存在一个经典问题一次性顺序读入的大量文件页可能把真正热点的内存页挤出活跃链表。Linux 通过「两次机会」机制缓解页面先进入非活跃链表若在后续扫描中被再次访问则晋升到活跃链表若持续未被访问则成为回收候选。活跃链表中的页若长时间未被访问也会降级到非活跃链表。这样既保留了热点又允许大规模顺序 I/O 的缓存被快速回收。13. 交换机制 Swap13.1 交换分区与交换文件当系统物理内存紧张时内核可以把不活跃的匿名页写入交换分区Swap Partition或交换文件Swap File腾出物理页给更需要的对象使用。这个过程称为换出Swap Out。当进程再次访问被换出的页时触发缺页异常内核从交换介质把数据换入Swap In。Swap 的代价是磁盘 I/O相比内存访问慢多个数量级因此 Swap 使用量过大往往意味着物理内存不足。13.2 swappiness 参数vm.swappiness控制内核回收匿名页和文件页的倾向。取值范围通常为 0 到 100值越大越倾向回收匿名页越小越倾向回收文件缓存。通过sysctl可以查看和调整cat /proc/sys/vm/swappiness sysctl vm.swappiness10需要强调的是把 swappiness 设置为 0 并不等于禁用 Swap它只是大幅降低匿名页换出的优先级。是否使用 Swap 以及如何配置需要结合业务延迟要求和内存压力综合判断。13.3 交换缓存与换入换出当同一匿名页被多个进程共享时换入换出过程需要维护引用一致性。交换缓存负责跟踪物理页与交换槽位的对应关系避免重复换入和重复写入。现代内核还支持 zswap 和 zram即在真正写入磁盘之前先把压缩后的页面保存在内存中的压缩缓存区从而降低 Swap I/O 延迟。14. 内存规整 Compaction14.1 内存碎片问题Buddy 系统擅长按 2 的幂次方分配连续页但长时间运行后仍可能产生外部碎片虽然系统整体空闲内存足够却找不到一大块连续物理页。对于需要高阶连续页的场景例如内核大的 DMA 缓冲区、透明大页等碎片会导致分配失败。14.2 内存规整原理内存规整Memory Compaction是内核的反碎片机制它通过移动可移动页把碎片化的空闲页合并成连续的大块。规整过程中内核会扫描迁移源区域和目标区域使用页面迁移机制把数据从源页复制到目标页更新页表和映射再释放源页。只有标记为可移动的页才能参与规整因此内核在分配时会把页面分类为可移动、可回收、不可移动等类型。14.3 触发方式内存规整可以由分配请求在慢速路径中触发也可以由内核线程kcompactd后台执行还可以通过/proc/sys/vm/compact_memory手动触发echo 1 /proc/sys/vm/compact_memory此外内核提供vm.compaction_proactiveness参数控制主动规整的积极程度在高阶分配敏感的场景中可以适当调高。15. OOM Killer15.1 OOM 的产生当内存回收、换出和规整都无法满足内存分配请求时内核会触发 OOMOut Of Memory内存耗尽机制。如果系统真的无内存可用总要有进程被牺牲否则整个系统将无法继续运行。OOM Killer 就是负责在极端内存压力下选择并终止进程的内核机制。15.2 选择牺牲进程的依据OOM Killer 为每个进程计算一个 OOM 分数oom_score分数越高越可能被杀。分数计算综合考虑进程的内存占用、存活时间、优先级、是否 root 用户等因素。用户可以通过/proc/[pid]/oom_score查看分数通过oom_score_adj手动调整分数从而影响 OOM 选择。cat /proc/self/oom_score echo -1000 /proc/self/oom_score_adj # 降低被杀概率oom_score_adj取值范围通常为 -1000 到 1000值越小越不容易被杀。把关键业务的oom_score_adj调低是运维中常见的保护手段。15.3 避免误杀与容量规划OOM Killer 只是极端情况的最后防线不能替代容量规划和内存治理。系统应关注长期内存增长、缓存膨胀、内存泄漏和 cgroup 限制等。配合监控告警和 cgroup 内存隔离可以在 OOM 发生前进行干预。16. 大页机制 HugePages 与 THP16.1 大页的优势页大小越大页表项越少页表遍历层级越短TLB 能覆盖的地址范围也越大。对于内存占用大且访问模式集中的应用例如数据库、JVM 和虚拟化负载使用 2 MB 或 1 GB 大页可以显著降低 TLB miss提高内存访问性能。16.2 静态大页 HugePages静态大页需要在系统启动后提前预留固定数量的物理内存页。预留的内存只能用于大页映射普通进程无法使用。可以通过内核启动参数或 sysctl 配置sysctl vm.nr_hugepages1024 cat /proc/sys/vm/nr_hugepages应用需要通过mmap配合MAP_HUGETLB标志或挂载 hugetlbfs 文件系统来使用大页。16.3 透明大页 THP透明大页Transparent Huge PagesTHP由内核在后台自动合并连续的小页为大页对应用透明无需修改代码。THP 能降低页表开销但也可能带来额外的内存规整、页面拆分和延迟波动。对于延迟敏感的应用有时建议关闭 THP 或调整为madvise模式。cat /sys/kernel/mm/transparent_hugepage/enabled echo madvise /sys/kernel/mm/transparent_hugepage/enabled17. NUMA 架构下的内存管理17.1 NUMA 拓扑与访问代价在多路服务器上内存被划分到不同的节点每个 CPU 访问本地节点的内存最快访问远端节点内存需要经过处理器间互联延迟更高、带宽更低。Linux 的 NUMA 支持贯穿了物理页分配、回收、调度和绑定等多个层面。17.2 NUMA 分配策略内核提供多种 NUMA 内存策略default、bind、interleave、preferred等。默认策略优先在进程当前运行的节点上分配内存绑定策略强制在指定节点分配交错策略把内存轮流分配到多个节点以获得更好的聚合带宽。用户可以通过numactl或mbind系统调用指定策略。numactl --interleaveall ./my_program17.3 NUMA 的常见问题NUMA 架构下容易出现两类问题一是内存分配过于集中在少数节点导致远端访问增加、延迟升高二是进程被调度到与内存不同的节点造成频繁跨节点访问。通过绑定 CPU 和内存节点、合理配置策略可以缓解这些问题。分析时可使用numastat查看各节点的内存使用和本地/远端分配比例。numastat18. 内核内存分配接口全景18.1 kmalloc 与 kfreekmalloc用于分配物理连续的小块内存基于 Slub 分配器实现。保证物理连续意味着可以直接用于 DMA 等要求连续物理地址的场景。其缺点是单个分配大小受限制通常最大不超过几 MB。void *buf kmalloc(1024, GFP_KERNEL); if (!buf) return -ENOMEM; memset(buf, 0, 1024); kfree(buf);18.2 vmalloc 与 vfreevmalloc用于分配大块内存虚拟地址连续但底层物理页可以不连续。它通过修改页表把多个分散物理页映射到一段连续的虚拟地址区间。由于物理不连续vmalloc 分配的内存不适合直接用于需要物理连续的 DMA 操作且分配过程会修改页表、触发 TLB 刷新频繁分配会带来更大开销。void *buf vmalloc(16 * 1024 * 1024); if (!buf) return -ENOMEM; memset(buf, 0, 16 * 1024 * 1024); vfree(buf);18.3 kmem_cache 专用缓存对于内核中频繁创建的对象应创建专用缓存而不是每次都使用通用 kmalloc。专用缓存可以通过对齐、构造函数等手段优化性能和内存利用率同时在调试时更容易统计和追踪对象。18.4 内存分配选型建议选择内核内存分配接口时可以遵循以下经验小对象且需要物理连续时用kmalloc需要清零时用kzalloc频繁创建同一类对象时用kmem_cache_alloc需要大块虚拟连续内存且不要求物理连续时用vmalloc需要整页连续物理内存时用alloc_pages或get_free_pages。同时必须根据上下文选择正确的 GFP 标志避免在原子上下文中睡眠。19. 用户空间内存接口全景19.1 malloc、calloc 与 freemalloc是 C 语言最常用的堆内存分配函数但它是 glibc 提供的用户态接口而不是系统调用。malloc 内部维护空闲块结构通过brk和mmap向内核申请内存。对于小块内存glibc 优先复用空闲块和线程缓存对于大块内存则直接 mmap释放时通过 munmap 归还内核。19.2 内存碎片与 malloc 调优长时间运行的服务进程可能出现用户态内存碎片表现为进程 RSS 不断增长但实际活跃对象并未增加。可以通过MALLOC_ARENA_MAX限制 arena 数量减少多线程下内存的过度保留也可以通过malloc_trim主动向内核归还空闲内存。分析用户态内存时可使用 valgrind、massif、heaptrack 等工具。19.3 共享内存进程间共享内存可以通过 System V 共享内存shmget、shmat或 POSIX 共享内存shm_open、mmap实现。共享内存的本质是让多个进程的虚拟地址映射到相同的物理页因而无需通过内核拷贝数据是高效 IPC 的基础。共享内存页属于匿名映射参与匿名页的回收与 LRU 管理。20. 内存屏障与一致性20.1 为什么需要内存屏障现代 CPU 为了性能会进行指令乱序执行编译器也会在保持单线程语义的前提下重排指令。在多核并发访问共享内存时这些重排可能导致其他核观察到的内存操作顺序与代码书写顺序不一致进而引发难以复现的并发错误。内存屏障Memory Barrier用于约束内存操作的可见顺序。20.2 内核中的内存屏障Linux 内核提供了smp_rmb、smp_wmb、smp_mb等屏障接口分别约束读、写和全序。对于普通的设备驱动和内核模块开发优先使用内核提供的锁、原子操作和完成量等同步原语它们内部已经包含必要的内存屏障只有在实现无锁数据结构或底层同步机制时才需要直接使用屏障。20.3 缓存一致性与 MESI多核 CPU 的各级缓存通过缓存一致性协议保持数据一致x86 常用类 MESI 协议。硬件层面的缓存一致性保证了单个地址的写入最终会被所有核心观察到但它不保证多个地址操作的相对顺序这正是内存屏障存在的意义。理解硬件一致性协议和软件内存模型的区别有助于避免把「最终一致」误认为「顺序一致」。21. 内存压力与内核参数调优21.1 水位线机制每个 Zone 维护三条水位线WMARK_MIN、WMARK_LOW和WMARK_HIGH。当空闲内存低于WMARK_LOW时内核唤醒kswapd进行后台回收当分配请求连WMARK_MIN都无法满足时进入慢速分配路径在请求上下文中同步回收甚至触发 OOM。水位线可通过/proc/sys/vm/min_free_kbytes间接调整。cat /proc/sys/vm/min_free_kbytes21.2 kswapd 与直接回收kswapd是每个节点上的后台回收线程在空闲内存低于低水位时被唤醒异步回收页面尽量避免用户请求进入同步回收路径。如果分配速度过快kswapd 来不及回收分配请求自身会执行直接回收Direct Reclaim导致分配延迟明显上升。持续的直接回收通常意味着系统内存压力较大。21.3 常用内核内存参数参数作用典型场景vm.swappiness控制匿名页换出倾向调整 Swap 使用策略vm.min_free_kbytes影响 Zone 水位线控制空闲内存保留量vm.dirty_ratio脏页占比阈值控制回写激进程度vm.dirty_background_ratio后台回写启动阈值平滑 I/O 压力vm.vfs_cache_pressure控制 dentry/inode 缓存回收文件密集型负载调优vm.overcommit_memory内存过度分配策略控制 malloc 行为修改内核参数时应遵循「先观察、后微调、再验证」的原则避免盲目套用网上的参数组合。每个参数的表现都与具体负载强相关应当结合监控数据逐步收敛。22. 内存监控与故障排查22.1 核心指标分析 Linux 内存问题时应优先关注以下指标MemFree、MemAvailable、Buffers、Cached、SwapTotal、SwapFree、Active、Inactive、Dirty、Slab。这些信息都可以从/proc/meminfo获取。22.2 常见工具free -h快速查看内存和 Swap 概况。top / htop实时查看进程内存占用和系统整体压力。vmstat观察 Swap 换入换出、内存和 CPU 活动。smem按 PSS/USS 统计进程真实内存占用。slabtop查看内核 Slab 缓存占用情况。perf统计缺页异常、缓存 miss 等内核事件。free -h vmstat 1 10 slabtop -o22.3 典型案例内存不断增长面对进程内存持续增长的问题可以按以下步骤排查。首先确认增长的是用户态堆还是文件缓存使用/proc/[pid]/smaps查看各 VMA 的 RSS 和 PSS 变化其次判断是否为内存泄漏借助 valgrind 或 heaptrack 分析用户态分配使用 kmemleak 排查内核泄漏再次检查是否存在缓存膨胀例如数据库缓存、日志缓冲或全局字典最后结合业务模型确认是否真正需要扩容。23. 总结Linux 内存管理是一个环环相扣的复杂体系。虚拟内存、页表与 MMU 构成了「隔离与保护」的基础Buddy 系统和 Slub 分配器解决了「高效分配与碎片控制」的问题VMA、缺页异常和写时复制支撑了进程地址空间的灵活管理LRU、Swap、Compaction、kswapd 和 OOM Killer 共同组成「回收与兜底」的闭环大页、NUMA 和各类调优参数则为高性能和大规模场景提供了优化空间。在实际工作中理解这套机制的意义不在于记住每个函数的实现细节而在于建立一条清晰的推理链当内存出现异常时能够沿着「用户态还是内核态、虚拟地址还是物理地址、匿名页还是文件页、惰性分配还是主动回收、单节点还是 NUMA」这些维度快速定位问题的层次和根因。建议读者结合/proc/meminfo、/proc/slabinfo、/proc/[pid]/maps和/proc/[pid]/smaps等接口在真实系统中持续观察内存行为。只有把原理和观测数据结合起来才能真正掌握 Linux 内存管理的精髓。
上一篇/下一篇内容由系统自动关联 返回资讯列表 →