Skip to content

物理内存管理

物理内存空间可以看作是一个地址数组,每个地址的大小取决于计算机的位数(32 位、64 位等)。

主板和 BIOS 程序在上电时:

  1. 检查和扫描设备
  2. 初始化各种设备(内存条和各种 IO 设备的寄存器和缓冲区)
  3. 将这些存储空间拼接成连贯的物理内存空间

生成的必要信息,这些信息放置在一些可以在后续操作

内核虚拟地址

在操作系统引导时,BIOS 将物理内存空间信息告知操作系统,包括:内存地址的分区和各个硬件设备的地址范围;操作系统可以在其中确定内存条设备的内存范围从而得知主内存的范围,并建立物理内存的管理结构。

具体地,在系统启动时,内核通过 BIOS/UEFI 或设备树(Device Tree)获取物理内存布局(如 RAM 范围,存储在 /proc/iomem)。内核构建 mem_map 数组,mem_map 是一个全局数组,使用 struct page 跟踪每个物理页面(4KB)。

c
struct page {
    unsigned long flags; // 页面状态
    atomic_t _count;    // 引用计数
    void *virtual;      // 对应的虚拟地址(可能为空)
    ...
};

由于 CPU 访问内存必须经过 MMU,而 MMU 的映射规则由操作系统维护,操作系统需要通过 MMU 将使用的内存页映射到真实的物理内存页上,因此内核访问内存时依然通过虚拟地址进行访问,这个虚拟地址称为内核虚拟地址;内核和用户空间都需要通过虚拟地址访问内存条和设备寄存器,内核虚拟地址通过页表线性映射,而用户虚拟地址通过多级页表映射。

内存分配

内存分配和组织算法是物理内存管理的主要内容,linux 的主内存使用伙伴系统和 slab 分配器进行管理。早期的系统中使用了简单的位图方法。

位图方法

将物理内存条划分为头+体的两块,头使用 bit 位标记了体中的某一块内存是否被分配使用,体中的某块内存可以直接与上层的内存页大小保持一致,采用 4k 为一块进行管理和分配;该算法实现简单,但是在新增分配的时候需要使用 O(n) 时间查找目标,时间效率不高。如今已经基本不再使用。

伙伴系统

buddy system 使用多级链表数组管理空闲块,是一种用于分配连续物理页框的算法。可以用于解决位图方法在分配内存时的速度问题。内核为分配页框调用 __get_free_pages()alloc_pages() 等接口时,就会走这个算法。当释放的两个"伙伴"空闲块连续时,可以合并为更大的块。

系统将空闲页面分组为 11 个块链表,每个块链表分别包含大小为 1、2、4、8、16、32、64、128、256、512 和 1024 个连续页框的页块。最大可以申请 1024 个连续页框,对应 4MB 大小的内存。

分配过程:

  1. 根据请求大小,找到对应的块链表
  2. 如果链表为空,则向更大的块链表申请
  3. 将大块分割成两个小块,一个用于分配,另一个加入较小的块链表
  4. 如果仍然没有合适的块,则继续向更大的块链表申请

分配过程:

空闲块(1024KB)
→ 分配 128KB 时 → 找不到正好128KB,就分成两个 512KB → 再分两个 256KB → 再分两个 128KB

分配成功:128KB
其"伙伴":另一个 128KB 仍空闲

当释放该 128KB,如果它的"伙伴"也空闲,则两者合并为 256KB,继续向上尝试合并。

释放过程:

  1. 将释放的块加入对应的块链表
  2. 检查是否有相邻的空闲块
  3. 如果有,则合并成更大的块,并加入更大的块链表
  4. 重复步骤 2-3,直到无法继续合并

优点:快速分配和释放,有效减少内存碎片,支持大块内存分配;缺点:可能造成内部碎片,合并操作可能较慢,不适合小块内存分配。

SLAB/SLUB/SLOB 分配器

SLAB系列分配器是 Linux 内核中用于管理小块内存的分配器,主要用于内核对象的分配和释放。它基于以下思想:内核对象在创建和销毁 时,需要频繁地分配和释放内存,如果每次都使用伙伴系统,会造成很大的开销。Linux 内核为适应不同场景和架构,提供了多种内存分配器。

  • SLAB 分配器:最早引入的对象缓存分配器。为每种对象类型维护多个 slab 缓存池,采用链表管理部分满、已满和空 slab。优点是分配和释放效率高,支持对象构造/析构函数,适合多处理器并发。缺点是实现复杂,内存碎片相对较多。
  • SLUB 分配器:SLAB 的改进版,当前主流内核的默认分配器。简化了 slab 管理结构,取消了链表,采用更高效的 bitmap 管理空闲对象,提升了分配/释放性能,减少了碎片。支持 NUMA 架构,调试和统计信息更丰富。
  • SLOB 分配器:面向嵌入式和资源受限设备的极简分配器。实现简单,适合小内存场景,但分配效率和并发能力较弱,不适合高性能服务器。
分配器适用场景优点缺点
SLAB通用/多核服务器并发高、支持构造析构实现复杂、碎片较多
SLUB主流服务器/桌面高效、碎片少、易维护早期兼容性问题
SLOB嵌入式/小内存设备简单、占用资源极低性能低、无并发优化

现代 Linux 内核默认采用 SLUB 分配器,用户可通过内核配置选项选择合适的分配器。

对于小于一页的内存分配请求,Linux 提供了以下机制:

  1. kmalloc,基于 SLUB 分配器,支持不同大小的缓存,支持内存对齐要求,适用于内核对象
  2. vmalloc,分配虚拟内存,不保证物理内存连续,适用于大块内存,性能较低
  3. 内存池,预分配内存块,快速分配和释放,减少内存碎片,适用于特定场景

内存回收

内存回收是物理内存管理中的重要环节,确保系统在内存紧张时能够及时释放和回收内存资源,避免内存耗尽导致系统崩溃。

回收对象

当系统内存不足时,内核会尝试回收一部分不再活跃的物理页面。页面回收分为主动回收和被动回收:主动回收:内核定期扫描内存,将长期未被访问的页面移出内存;被动回收:当分配内存失败时,触发回收机制。

  1. 匿名页。主要是进程的堆、栈等私有数据。这类页面不能直接丢弃,只能通过写入 swap(交换分区/文件)后释放物理内存;
  2. 页缓存。用于缓存文件内容、文件映射等。这类页面可以直接丢弃(如果未修改)或回写到磁盘(如果被修改过),回收后可立即释放物理内存;
  3. 内核缓存。即 SLAB 分配器的缓存,包括 dentry(目录项)、inode、内核对象等。这些缓存用于加速内核操作,内存紧张时可以回收部分未被使用的对象。
  4. 除此之外,Linux 还有一些特殊的内存池或缓存(如网络缓冲区、内存池等),但它们本质上也是属于"缓存"类资源,回收方式类似。

内核通过 LRU(最近最少使用)算法维护活跃页和不活跃页列表,优先回收不活跃页。

内存交换

当物理内存不足时,内核会将部分匿名页写入交换分区(swap space),以释放物理内存。被 swap 出的页面在需要时可以重新加载到内存。优点:提升系统的容错能力,防止内存耗尽。缺点:频繁 swap 会导致系统变慢(swap storm)。

OOM Killer(Out-Of-Memory Killer)

当所有回收和 swap 手段都无法满足内存需求时,内核会启动 OOM Killer,选择性地终止某些进程以释放内存。

  • 选择标准:优先杀死占用内存多、优先级低的进程。
  • 触发时机:分配内存失败且无法回收更多内存时。

内存回收尝试过程

  1. 进程申请内存,系统发现空闲内存不足。
  2. 内核尝试回收页面和缓存。
  3. 仍然不足时,尝试 swap。
  4. 如果 swap 也无法满足,触发 OOM Killer,终止部分进程。

通过多层次的回收和保护机制,Linux 能够在高负载和内存紧张的情况下保持系统的稳定性。

内存压缩

当系统内存压力较大时,内核会将部分不活跃的页面进行压缩,压缩后的数据仍然保存在内存中。这样可以在不增加物理内存的情况下,存储更多的页面内容。需要访问被压缩页面时,内核会自动解压还原。

  • zswap:内核自带的压缩后交换缓存。被换出的页面先压缩存放在内存的 zswap 区域,只有当 zswap 区域满了才真正写入 swap 设备。
  • zram:将一块内存虚拟成块设备,作为压缩 swap 区使用。常用于嵌入式、低内存设备。

通过内存压缩,Linux 能够在不增加物理内存的前提下,提高内存利用率,延缓 swap 到磁盘的时机,减少 I/O,但是这是时间换空间的思路,压缩和解压会消耗 CPU 资源。

适用场景

  • 内存资源有限、磁盘 I/O 成本高的场景(如嵌入式设备、虚拟机)。
  • 希望提升 swap 性能、减少磁盘写入的服务器和桌面系统。

内存碎片

内存碎片是指物理内存空间由于频繁分配和释放,导致出现无法被有效利用的小块空闲内存,影响大块内存的分配效率,可能导致分配失败(如驱动、DMA、HugePage 需求)。内存碎片主要分为两类:

  • 外部碎片:指内存空间中存在许多不连续的小块空闲区域,虽然总空闲内存足够,但无法满足需要大块连续内存的分配请求。
  • 内部碎片:指分配的内存块大于实际需要,导致块内部有未被使用的空间。

产生原因

  • 频繁的内存分配和释放,尤其是不同大小的内存块混合分配时。
  • 长时间运行的系统,内存分配模式复杂。
  • 大量小对象或大对象交替分配。

常见的优化措施

  • 伙伴系统:通过合并相邻空闲块,减少外部碎片。
  • SLAB/SLUB 分配器:为不同类型和大小的对象建立专用缓存,减少内部碎片。
  • 内存紧缩(Compaction):内核支持将分散的空闲页面移动、合并为大块连续空间(如 echo 1 > /proc/sys/vm/compact_memory)。
  • 大页(HugePage)机制:减少页表项数量,降低碎片化概率。

内存紧缩

内存紧缩(Memory Compaction)是 Linux 内核为减少外部碎片、提升大块连续物理内存(如大页 HugePage、DMA、设备驱动等场景)分配成功率而引入的机制。其核心思想是将分散的空闲页面移动、合并为更大的连续空闲块。内存紧缩会扫描物理内存,将分散的活跃页面迁移到一侧,将空闲页面集中到另一侧,从而形成大块的连续空闲内存。

触发时机

  • 自动触发:当内核检测到大块内存分配失败时,会自动尝试内存紧缩。
  • 手动触发:可以通过写入 /proc/sys/vm/compact_memory 主动请求紧缩,例如:
    echo 1 > /proc/sys/vm/compact_memory
  • 也可通过 echo 1 > /sys/kernel/mm/compact_memory(不同内核版本路径略有差异)。

内存监控和调优

  • /proc 文件系统:

    • /proc/meminfo:显示系统整体内存使用情况。
    • /proc/buddyinfo:展示各阶空闲内存块分布,便于分析碎片。
    • /proc/slabinfo:SLAB/SLUB 分配器的缓存对象统计。
    • /proc/vmstat:虚拟内存统计,包括紧缩、回收等信息。
  • 常用命令工具:

    • free:查看内存和 swap 使用情况。
    • top/htop:实时监控进程内存占用。
    • vmstat:虚拟内存、进程、CPU 等多维度统计。
    • smempspmap:分析进程级内存分布。
  • 内存压力检测:

    • /proc/pressure/memory(PSI):内存压力指标(新内核支持)。
  • 常见内核参数(可通过 sysctl 或 /proc/sys/vm/ 配置):

    • vm.swappiness:控制内存与 swap 的平衡,值越大越倾向于使用 swap。
    • vm.min_free_kbytes:设置系统预留的最小空闲内存,防止内存耗尽。
    • vm.dirty_ratio/vm.dirty_background_ratio:控制脏页写回磁盘的阈值。
    • vm.overcommit_memory/vm.overcommit_ratio:控制内存超分配策略。
    • vm.compaction_proactiveness:控制内存紧缩的积极性。

思路

  • 根据实际业务负载调整 swappiness,避免频繁 swap。
  • 监控碎片和紧缩效果,合理配置大页和紧缩参数。
  • 定期分析 slab 缓存,防止内核对象泄漏。
  • 关注内存压力指标,及时发现和处理内存瓶颈。
  • 对于高性能场景,合理配置 HugePage、NUMA 策略等。

通过科学的监控和调优,能够有效提升 Linux 系统的内存利用率和整体性能,降低 OOM 风险,保障关键业务的稳定运行。