Linux内核物理内存管理:从伙伴系统到反碎片化技术
1. 项目概述从“黑盒”到“白盒”的探索每次看到服务器上那动辄数百GB的物理内存或者自己电脑里插着的几根内存条你有没有想过一个问题操作系统特别是Linux内核到底是怎么管理这些物理内存的它怎么知道哪块内存是空闲的哪块已经被程序占用了当程序申请内存时内核又是如何从这茫茫的物理地址空间中精准地划出一块给你用的这听起来像是一个庞大的物流仓库管理员的工作而Linux内核就是这个管理员它必须高效、精确、无差错地管理着整个系统的物理内存资源。这个问题远不止是理论上的好奇。理解物理内存管理是深入Linux系统性能调优、排查内存泄漏、编写高性能应用乃至进行内核开发的基石。比如当你遇到系统因为“内存不足”而变慢或者某个进程莫名其妙地吃掉大量内存时如果你只知道用free或top命令看个大概那就像只看到了仓库的进出货记录却不知道货架是怎么摆放、调度规则是什么很难从根本上解决问题。今天我们就来彻底拆解这个“仓库管理系统”看看Linux内核是如何在幕后井然有序地打理这一切的。2. 物理内存管理的核心架构与设计哲学Linux内核管理物理内存并非一蹴而就它是一套经过几十年演进、高度抽象和分层的复杂系统。其核心设计哲学可以概括为通过分层抽象将复杂的硬件差异屏蔽向上提供统一、高效的页面级管理接口。理解这个架构是理解一切细节的前提。2.1 内存管理的起点物理地址空间探测与划分当系统上电内核刚开始启动时它面对的是一个“未知”的物理内存世界。它需要回答几个基本问题系统总共有多少物理内存这些内存的物理地址范围是什么有没有一些特殊的、不能用于通用目的的内存区域比如被BIOS或硬件保留的区域内核通过BIOS在传统BIOS系统上或UEFI在现代系统上提供的信息以及自身对硬件的探测来构建一张物理内存的“地图”。这个过程称为内存探测。获取到的信息会被整理成一个关键的数据结构mem_map数组在较新内核中概念有所演变但本质类似。这个数组的每一个元素都对应着物理内存中的一个页帧。页帧是物理内存管理的基本单位在x86_64架构上通常是4KB4096字节。你可以把它想象成仓库里一个个标准尺寸的货架格子。注意这里说的“页帧”是指物理内存的划分单元而“页面”通常指虚拟内存管理中的概念。一个页帧可以容纳一个页面当页面被映射到物理内存时。在讨论物理内存管理时我们更关注页帧。内核接着会根据获取的信息将物理地址空间划分为几个主要的区域ZONE_DMA用于直接内存访问DMA的区域。一些老旧的ISA设备只能对低16MB物理地址进行DMA操作这部分内存就被划归此区域。ZONE_DMA32在64位系统上用于支持只能对低4GB物理地址进行DMA的32位设备。ZONE_NORMAL内核可以直接映射的“普通”内存区域。在x86_64上通常指从物理地址起始到大约896MB这个值可配置的区域。这部分内存的物理地址和内核虚拟地址之间存在简单的线性偏移关系PAGE_OFFSET因此内核可以快速访问。ZONE_HIGHMEM在32位系统上当物理内存超过内核直接映射区通常是896MB时超出部分就属于高端内存。内核不能直接映射访问它需要通过动态映射。在64位系统上由于虚拟地址空间极其巨大48位或57位通常没有ZONE_HIGHMEM。ZONE_MOVABLE一个特殊的区域包含可移动的页帧。这是为了支持内存热插拔和反碎片化技术。内核可以将这个区域的页帧内容迁移到别处从而腾出连续的物理空间。这种分区管理的目的是为了应对硬件的限制和优化性能。例如来自ZONE_DMA的页帧分配给需要DMA的设备驱动可以确保兼容性。2.2 伙伴系统解决外部碎片的核心引擎物理内存被划分成一个个页帧后内核如何高效地分配和释放它们呢最直接的想法可能是用一个链表把所有空闲页帧串起来需要时取一个。但这样会遇到两个大问题1. 如何快速分配连续多个页帧即一个“大块内存”2. 频繁分配释放后物理内存会变得“千疮百孔”外部碎片导致即使总空闲内存很多也无法分配出一块连续的大内存。Linux内核的答案是伙伴系统。伙伴系统是物理内存分配器的基石它精巧地解决了外部碎片问题。其核心思想是按阶管理内存块被组织成一系列链表每个链表上的内存块大小都是2的幂次方个页帧。这个幂次方称为“阶”。例如0阶链表上的块大小是1个页帧4KB1阶是2个页帧8KB2阶是4个页帧16KB以此类推最大可以到MAX_ORDER通常是10或11即1024或2048个页帧4MB或8MB。伙伴关系每个内存块都有一个“伙伴”。伙伴块是指大小相同、物理地址连续并且合并后能形成一个更大阶的、地址对齐的块的两个内存块。例如两个连续的、地址对齐的4KB块0阶是伙伴它们可以合并成一个8KB块1阶。分配算法当请求分配2^n个页帧时系统首先在n阶空闲链表中查找。如果找到直接分配。如果没找到就向更高阶n1阶寻找。找到后将该大块分裂成两个伙伴块一个用于分配另一个放入低一阶n阶的空闲链表。这个过程可能递归进行。释放与合并当释放一个内存块时系统会立即检查其伙伴块是否也空闲且位于同一阶的空闲链表中。如果是则将这两个伙伴块合并成一个更高阶的大块并放入更高阶的空闲链表。这个过程也会递归进行尽可能合并出更大的连续空闲块。通过这种“分裂-合并”机制伙伴系统极大地减少了外部碎片保证了长时间运行后系统仍然能分配出较大的连续物理内存。每个内存区域ZONE都有自己的伙伴系统数据结构struct zone中的free_area数组来管理本区域内的页帧。2.3 每CPU页帧缓存加速高频小内存分配伙伴系统虽然解决了外部碎片但其操作分裂、合并、链表操作涉及到锁的竞争对于单个页帧4KB这种极其高频的分配请求比如进程创建时分配栈空间直接走伙伴系统效率不够高。为此内核在伙伴系统之上为每个CPU核心都设置了一个每CPU页帧缓存。每个CPU有两个主要的缓存列表pcp-lists[0](MIGRATE_UNMOVABLE): 用于缓存不可移动类型的页帧。pcp-lists[1](MIGRATE_RECLAIMABLE等): 用于缓存可回收或可移动类型的页帧。当内核需要分配单个页帧时它首先尝试从当前CPU的pcp列表中获取。因为pcp列表是每个CPU独有的所以无需加锁速度极快。如果pcp列表空了内核会一次性从伙伴系统中批量“批发”一批比如几十个页帧到pcp列表中然后再从中分配一个给请求者。释放单个页帧时也是先放回pcp列表只有当pcp列表满了达到高水位线才会将一批页帧“退还”给伙伴系统。这个设计是典型的“缓存”思想用空间换时间将全局的、慢速的伙伴系统分配转化为局部的、快速的每CPU缓存分配极大地提升了高频小内存分配的效率。3. 深入伙伴系统与反碎片化技术理解了基本框架我们深入到伙伴系统的实现细节和它如何与现代的反碎片化技术结合。3.1 伙伴系统的数据结构实现在Linux内核中每个内存区域struct zone都有一个struct free_area数组大小是MAX_ORDER。这就是伙伴系统的核心数据结构。struct free_area { struct list_head free_list[MIGRATE_TYPES]; // 不同迁移类型的空闲链表 unsigned long nr_free; // 该阶总的空闲块数 };这里出现了一个新概念MIGRATE_TYPES迁移类型。这是反碎片化技术的关键。传统的伙伴系统只按大小阶组织空闲块但Linux内核将空闲块进一步按“迁移类型”分类MIGRATE_UNMOVABLE不可移动页。例如内核代码、数据、大多数内核数据结构如task_struct。它们通常被钉在固定的物理地址。MIGRATE_RECLAIMABLE可回收页。例如文件缓存page cache。当内存紧张时这些页的内容可以被丢弃如果是干净页或写回磁盘如果是脏页然后页帧本身可以被重新分配。MIGRATE_MOVABLE可移动页。例如用户进程的匿名内存堆、栈。这些页的内容可以被迁移到其他物理页帧。MIGRATE_PCPTYPES这是每CPU缓存使用的类型。MIGRATE_CMA连续内存分配器类型用于预留大块连续物理内存。MIGRATE_ISOLATE隔离类型用于内存热插拔等场景不能被分配。为什么这么分目的是减少不可移动页造成的长期碎片。想象一下如果不可移动的内核数据MIGRATE_UNMOVABLE和可移动的用户进程页面MIGRATE_MOVABLE在物理内存中交错分布经过长时间运行后即使释放了大量用户进程页面它们留下的“空隙”也可能因为被不可移动的页面包围而无法合并成连续大块。这就是“碎片化”。通过按迁移类型隔离内核在分配时尽量从相同类型的链表中分配。例如分配用户内存时优先从MIGRATE_MOVABLE链表中找。这样不可移动的页面会自然地聚集在一起可移动的页面也聚集在一起。当需要分配大块连续内存时如果当前迁移类型的链表无法满足内核会尝试从其他类型的链表中“偷”页面称为“fallback”机制有一个预定义的fallback顺序但这已经是最后手段。通过这种隔离显著降低了长期运行后内存碎片化的程度。3.2 页面分配器的调用链路当内核中的代码比如通过alloc_pages系列函数需要分配物理页帧时整个分配流程是一个层层递进的过程体现了效率优先的原则快速路径每CPU缓存对于单页分配请求order0分配器首先尝试从当前CPU的pcp每CPU页帧缓存列表中获取。这是最快、无锁的路径。伙伴系统路径如果pcp为空或者请求分配多个页order 0则进入伙伴系统分配路径。首先根据分配标志如GFP_KERNEL,GFP_ATOMIC确定可以从哪些内存区域ZONE分配。有一个区域后备列表zonelist按优先顺序排列例如可能优先尝试ZONE_NORMAL不行再试ZONE_DMA32。在选定的区域中从请求的order开始在对应迁移类型的free_area链表中查找空闲块。如果找到则从链表中摘下更新区域统计信息完成分配。如果没找到则尝试“fallback”到其他迁移类型的链表或者向更高阶查找并分裂。慢速路径与回收如果伙伴系统在当前区域的所有迁移类型和阶数上都找不到足够的内存则会触发页面回收机制。这可能包括将pcp列表中的页批量返还给伙伴系统。唤醒kswapd内核线程开始扫描并回收可回收页面如干净的page cache。如果内存压力极大可能会触发更激进的回收甚至OOM Killer来终止进程以释放内存。分配标志GFP Flags的含义调用者通过GFP_标志控制分配行为这对理解内存分配至关重要。__GFP_HIGHMEM允许从高端内存区域分配。__GFP_DMA/__GFP_DMA32要求从DMA区域分配。__GFP_MOVABLE请求可移动的页面。__GFP_RECLAIMABLE请求可回收的页面。__GFP_IO/__GFP_FS允许在回收过程中执行I/O或文件系统操作。__GFP_ATOMIC用于原子上下文不能睡眠分配器会尽力在不触发回收的情况下分配可能从紧急保留池中取。__GFP_ZERO分配后将页面内容清零。__GFP_NOWARN分配失败时不打印警告信息。理解这条调用链路和GFP标志对于诊断“内存分配失败”类问题至关重要。例如在中断处理程序原子上下文中分配内存必须使用GFP_ATOMIC否则会导致内核崩溃。4. 物理内存的“状态机”页帧的状态与生命周期一个物理页帧在内核中并非只有“空闲”和“已用”两种状态。它有一个精细的生命周期由struct page结构体中的标志位和引用计数来管理。理解这些状态是理解内存回收、迁移等高级特性的基础。4.1struct page页帧的身份证每个物理页帧都有一个对应的struct page结构体这些结构体通常存放在一个叫mem_map的数组中或通过其他更复杂的稀疏内存模型组织。struct page是内核管理物理页帧的元数据它包含了页帧的所有管理信息但本身不存储用户数据。数据存储在页帧指向的4KB物理内存中。struct page中几个关键字段flags一长串位图标识页帧的当前状态。例如PG_locked页被锁定通常在进行I/O操作时设置。PG_dirty页的内容已被修改与磁盘上的数据不一致。PG_uptodate页的内容是有效的对于缓存页。PG_lru页在LRU最近最少使用链表上这是页面回收的关键。PG_slab页被slab分配器使用用于分配小对象。PG_swapbacked页的内容可被交换到交换分区。_refcount引用计数。表示有多少个“使用者”正在引用这个页帧。例如一个页帧被映射到用户进程的地址空间计数会增加被多个进程共享如共享内存计数会大于1。当_refcount减为0时表示该页帧可以被释放回伙伴系统。_mapcount页表映射计数。表示这个页帧被映射到多少个页表项PTE中。这对于写时复制Copy-On-Write等机制很重要。mapping指向该页所属的地址空间对象address_space的指针。如果是匿名页如进程堆内存mapping指向一个特殊的匿名地址空间如果是文件缓存页mapping指向对应文件的address_space。private私有数据指针其含义取决于页的类型。例如对于缓冲区页它可能指向缓冲区头。lru链表头用于将页链接到各种LRU链表或伙伴系统的空闲链表中。4.2 页帧的生命周期与状态转换一个页帧的典型生命周期如下空闲Free页帧位于伙伴系统的某个free_area链表中_refcount为0。struct page的大部分字段未初始化或处于默认状态。分配Allocated通过alloc_pages从伙伴系统分配后页帧离开空闲链表。其_refcount被设置为1表示被分配器持有并根据分配标志初始化部分flags。使用In Use页帧被交给某个“消费者”。这可能是用户空间通过页表映射到进程的虚拟地址空间。此时_mapcount会增加mapping可能被设置为匿名地址空间。内核空间作为内核数据结构的存储或者通过kmap等临时映射到内核地址空间。页缓存Page Cache用于缓存文件数据。此时mapping指向文件的address_space页被加入到对应地址空间的基数树中同时也会被加入到LRU链表。Slab/Slub页被交给slab分配器分割成多个小对象如task_struct,inode等。此时flags会设置PG_slab。回收候选Reclaimable对于页缓存或匿名页如果系统启用了交换空间当系统内存紧张时这些页会被内核的页面回收线程kswapd扫描。干净的页缓存未修改可以直接丢弃脏的页缓存需要写回磁盘匿名页需要被交换到交换分区。这些页都位于LRU链表上。活跃LRU链表最近被访问过的页。非活跃LRU链表最近未被访问的页。回收主要从非活跃链表的尾部开始。释放Freed当页帧的所有引用都消失_refcount减为0它会被释放回伙伴系统。如果是slab页会先释放给slab分配器再由slab分配器在适当的时候将整页还给伙伴系统。4.3 LRU链表与页面回收机制LRULeast Recently Used链表是页面回收的核心数据结构。内核维护多组LRU链表对页帧进行粗略的“热度”分类匿名页LRU存储进程的堆、栈等匿名内存页。文件页LRU存储文件缓存页。每组LRU又分为活跃链表和非活跃链表。kswapd内核线程定期或在内存压力事件触发时运行。它的工作流程简化如下计算当前内存压力水平通过watermark检查。系统为每个内存区域设置了三个水位线min,low,high。如果空闲页低于low水位开始扫描LRU链表。扫描顺序和强度取决于内存压力。将疑似“冷”的页从活跃链表尾部移动到非活跃链表。尝试回收非活跃链表尾部的页对于文件页如果是干净的直接丢弃如果是脏的发起写回。对于匿名页尝试交换到交换分区。回收成功的页帧内容已妥善处理页帧已空闲被释放回伙伴系统。持续回收直到空闲页数量回到high水位以上。实操心得理解LRU机制对调优至关重要。通过/proc/sys/vm/swappiness可以调节内核回收匿名页和文件页的倾向性。值越高越倾向于交换匿名页值越低越倾向于丢弃文件缓存。对于数据库服务器依赖大量文件缓存通常建议降低swappiness如10对于内存密集型计算匿名页多可能需要调高。但现代内核的回收逻辑已经非常复杂swappiness只是一个影响因素。5. 高级主题与性能考量物理内存管理不仅仅是分配和释放。在现代系统中它还需要处理更复杂的需求如大页、内存热插拔、内存cgroup等。5.1 大页Huge Pages支持标准页帧是4KB这对于管理大量内存如数百GB来说意味着页表会非常庞大导致TLB转址旁路缓存压力大。TLB是缓存虚拟地址到物理地址映射的硬件单元容量有限。频繁的TLB未命中会严重降低性能。大页通过使用更大的页尺寸如2MB或1GB来缓解这个问题。一个2MB大页相当于512个标准4KB页。使用大页的好处减少页表项映射相同大小的内存需要的页表项更少。提高TLB命中率一个TLB条目可以覆盖更大的地址范围。减少页错误开销处理一次大页页错误可以映射更大区域。Linux内核支持两种大页透明大页内核自动将连续的普通小页合并成一个大页对应用透明。由khugepaged内核线程负责。可以通过/sys/kernel/mm/transparent_hugepage/enabled控制。显式大页需要应用预先通过mmap或shmget系统调用从预留的大页池中分配。大页池需要在系统启动时通过内核参数如hugepages2048预留。注意事项大页并非万能。由于分配单位大可能造成内部碎片例如一个应用只需要3MB但分配了一个2MB大页后剩下的1MB可能浪费。此外透明大页的合并/分裂操作本身也有开销在某些极端负载下可能反而导致性能下降这也是为什么有些数据库或高性能计算场景会选择关闭透明大页改用显式大页进行更精确的控制。5.2 内存热插拔与内存规整内存热插拔允许在系统运行期间动态添加或移除物理内存需要硬件支持。这涉及到将新内存区域加入到现有的内存管理框架中或者将某个内存区域隔离、清空并移除。ZONE_MOVABLE区域的设计很大程度上就是为了支持内存热移除——需要移动的页面可以被迁移到其他区域从而腾出连续的空闲区域供移除。内存规整是内核为了减少碎片、满足大块连续内存分配需求而进行的内存迁移过程。当内核无法分配足够大的连续物理内存时即使总空闲内存足够内存规整机制由kcompactd内核线程执行会被触发。它会尝试将可移动页面MIGRATE_MOVABLE从某个区域迁移走从而在物理地址上“挤”出一块连续的空闲空间。这个过程与伙伴系统的迁移类型隔离相辅相成共同对抗碎片化。5.3 控制组与内存资源隔离在容器化环境中多个容器共享同一个内核。如何公平地分配和限制每个容器的物理内存使用这需要内存控制组。内存cgroup为每个控制组维护一套独立的内存使用统计和限制memory.limit_in_bytes设置内存使用硬限制。超过此限制组内进程的内存分配会失败或触发OOM Killer如果memory.oom_control允许。memory.soft_limit_in_bytes软限制。当系统内存紧张时超过软限制的cgroup会优先被回收内存。memory.usage_in_bytes当前内存使用量统计。memory.stat详细的内存使用统计包括文件缓存、匿名页、交换缓存等细分项。内核的页面回收机制kswapd和OOM Killer都感知cgroup。当某个cgroup达到限制时回收和OOM操作会局限于该cgroup内部不会影响其他cgroup。这实现了容器间的内存隔离和公平性。6. 实战观测与调试物理内存管理理论最终要服务于实践。我们如何观察和验证内核的物理内存管理行为呢以下是一些核心工具和技巧。6.1 通过/proc和/sys接口观测Linux提供了丰富的虚拟文件系统接口来暴露内存管理信息。/proc/meminfo这是最全面的内存使用情况摘要。关键字段解读MemTotal总物理内存。MemFree完全未被使用的内存。这个值通常很小因为Linux会积极使用空闲内存做文件缓存。MemAvailable估算的可用内存。这是比MemFree更有意义的指标它包含了可回收的缓存Cached和SReclaimable。当应用需要内存时这部分缓存可以被快速回收。Buffers块设备缓冲区缓存原始磁盘块。Cached页缓存page cache用于缓存文件数据。Cached Buffers≈ 文件缓存总量。SwapCached被换出过但又换回内存的页面。当再次需要这些页面时可以直接从交换缓存读取速度很快。Active(anon)/Inactive(anon)活跃/非活跃的匿名页。Active(file)/Inactive(file)活跃/非活跃的文件页。Unevictable不可回收的页如mlock的页。Mlocked被锁在内存中的页大小。SwapTotal/SwapFree交换分区总量/剩余量。Dirty等待写回磁盘的脏页大小。Writeback正在写回磁盘的页大小。AnonPages匿名页堆、栈等总量。Mapped被映射到用户空间的页包括文件映射和匿名映射。Shmem共享内存tmpfs等大小。KReclaimable内核可回收内存如slab中的可回收部分。Slabslab分配器使用的总内存SReclaimableSUnreclaim。PageTables页表占用的内存。CommitLimit/Committed_AS基于overcommit策略的提交限制和已提交量。/proc/buddyinfo这是观察伙伴系统碎片情况的最直接工具。它按内存区域和迁移类型显示每个阶order有多少个空闲块。Node 0, zone DMA 1 0 1 0 2 1 1 0 1 1 3 Node 0, zone DMA32 3148 2097 1562 927 486 223 106 38 16 2 0 Node 0, zone Normal 42538 29876 14521 6231 2024 511 123 20 4 0 0每一行代表一个内存区域。后面的数字分别代表order 0, order 1, ..., order 10的空闲块数量。如果高阶比如order 3以上的数字长期为0或很小说明系统存在一定程度的碎片可能难以分配连续的大块内存。/proc/pagetypeinfo提供更详细的按迁移类型划分的伙伴系统信息可以看到每个区域、每个阶、每种迁移类型的空闲页数。/proc/vmstat包含海量的内存管理事件计数器。对于性能分析极其有用。pgalloc_normal普通区域分配的页数。pgfree释放的页数。pgscan_kswapd/pgscan_direct由kswapd和直接回收扫描的页数。pgsteal_kswapd/pgsteal_direct成功回收的页数。oom_killOOM Killer触发的次数。通过定期采样如watch -n 1 cat /proc/vmstat并计算差值可以了解内存管理的动态活动。/sys/kernel/mm/transparent_hugepage透明大页相关控制接口可以查看状态enabled,defrag和统计信息khugepaged的扫描、合并次数等。6.2 使用vmstat和sar进行动态监控vmstat 1每秒输出一次系统概览。swpd使用的交换空间大小。free空闲内存与/proc/meminfo的MemFree类似意义不大。buff/cache缓冲区/缓存大小。si/so每秒从交换分区换入/换出的内存量KB。如果持续大于0说明内存压力很大。inact/active非活跃/活跃内存估计值。cs上下文切换次数。内存回收可能导致cs增高。sar -B 1查看页面调度分页统计。pgpgin/s/pgpgout/s每秒换入/换出的页数包括文件I/O和交换。fault/s每秒缺页异常总数主要次要。majflt/s每秒主要缺页异常数。主要缺页需要从磁盘加载数据代价高昂。如果这个值持续很高说明可能物理内存严重不足或者应用访问模式非常随机。6.3 诊断内存碎片与分配失败当遇到“无法分配连续内存”的错误时常见于驱动或某些需要DMA的应用可以按以下步骤诊断检查/proc/buddyinfo首先确认高阶空闲块是否确实稀少。如果某个区域的order 3以上数字全是0说明该区域碎片严重。检查/proc/pagetypeinfo看是否是特定迁移类型如Unmovable的碎片导致。如果Unmovable类型的块很多且分散会阻碍连续分配。检查内核日志dmesg寻找类似“page allocation failure”或“Out of memory”的错误信息通常会附带当时的GFP标志、order、以及buddyinfo的快照。分析内存使用使用slabtop查看内核对象slab的使用情况。某些内核模块可能分配了大量不可移动的内存导致碎片。使用cat /proc/meminfo | grep -i slab查看slab总量。尝试缓解如果问题由透明大页引起可以尝试临时关闭它echo never /sys/kernel/mm/transparent_hugepage/enabled。调整/proc/sys/vm/下的参数如增加min_free_kbytes确保有足够的最低空闲内存供紧急分配或调整zone_reclaim_mode控制内存回收策略。最根本的可能是应用或驱动本身的问题需要优化其内存分配模式避免频繁分配释放大小不一的内存块。理解Linux内核的物理内存管理就像掌握了服务器内存系统的“地图”和“操作手册”。从宏观的分区管理到微观的页帧状态从高效的伙伴系统分配到复杂的反碎片和回收机制每一层设计都旨在平衡性能、效率和资源利用率。下次当你再面对内存问题时希望这份深入底层的“地图”能帮你更快地定位问题根源做出更有效的决策。