Linux 内核的内存管理机制是操作系统最核心的组成部分之一。它不仅负责物理内存的分配与回收,还实现了虚拟地址空间、页面交换、Slab 分配器等众多精巧算法。本文将从物理内存管理出发,深入剖析 Linux 内核如何通过虚拟地址空间为进程提供统一的内存视图。

一、物理内存管理

1.1 物理页(Page Frame)

Linux 内核将物理内存划分为固定大小的"页",默认大小为 4KB(可通过 CONFIG_PAGE_SIZE 配置)。每个物理页由一个 struct page 结构体描述,该结构体包含引用计数、标志位、映射信息等字段。系统维护着一个全局数组 mem_map 来管理所有物理页。

struct page 关键字段包括:

  • _refcount:引用计数,当降为 0 时页可被回收
  • _mapcount:被映射到多少个进程的页表
  • flags:状态标志,如 PG_locked、PG_dirty、PG_slab 等
  • mapping:指向 address_space 或 anon_vma
  • index:在映射中的偏移量

1.2 伙伴系统(Buddy System)

伙伴系统是物理内存分配的核心算法。它将空闲内存划分为 11 个链表(order 0-10),分别管理 1、2、4、...、1024 个连续页框。

分配流程:

alloc_pages(gfp_mask, order) -> __alloc_pages()
  -> get_page_from_freelist()  # 从空闲链表获取
  -> __alloc_pages_slowpath()  # 慢路径:内存回收
  -> __alloc_pages_direct_reclaim()  # 直接回收
  -> __alloc_pages_direct_compact()  # 内存规整

当释放内存时,系统检查相邻的"伙伴"页是否空闲,若空闲则合并为更大的块。这种机制有效减少了外部碎片。

1.3 per-cpu 热页缓存

为提升分配效率,内核为每个 CPU 维护了一个 hot page cache(pcp 列表)。当分配单页时,内核优先从 per-cpu 缓存获取,避免全局竞争。当缓存过低时从伙伴系统批量补充,过高时归还。

二、Slab 分配器

2.1 为什么需要 Slab

伙伴系统以页为单位(4KB)分配,但内核经常需要小对象(如 task_struct 约 ~8KB、inode 约 ~600B)。频繁分页会使内部碎片严重,且初始化/销毁对象开销大。Slab 分配器解决了这个问题:它从伙伴系统获取页,切分为小对象槽,缓存已初始化的对象。

2.2 Slab 架构层次

Linux 内核支持三种 Slab 实现:Slab(原始)、Slub(默认)、Slob(嵌入式)。当前主流是 Slub。

三层架构:
┌─────────────────────────┐
│   Slab Cache (缓存层)    │  kmem_cache: 管理同类型对象
├─────────────────────────┤
│   Slab Page (页层)       │  一个或多个连续物理页,切分为对象槽
├─────────────────────────┤
│   Buddy System (伙伴层)  │  物理页分配
└─────────────────────────┘

2.3 kmem_cache 创建与使用

// 创建专用缓存
struct kmem_cache *task_cache = kmem_cache_create("task_struct",
    sizeof(struct task_struct), 0, SLAB_PANIC, NULL);

// 分配对象
struct task_struct *tsk = kmem_cache_alloc(task_cache, GFP_KERNEL);

// 释放对象
kmem_cache_free(task_cache, tsk);

2.4 通用缓存(kmalloc)

kmalloc 提供任意大小的通用分配,内部是一组预创建的 kmem_cache,大小为 8、16、32、...、8MB(通常为 kmalloc-8 到 kmalloc-8192)。内部会选择最接近的缓存。

三、虚拟地址空间

3.1 虚拟内存的意义

虚拟地址空间为每个进程提供独立的、连续的内存视图,隐藏了物理内存碎片,实现了进程间内存隔离,并允许内存超量分配(overcommit)。

3.2 Linux 地址空间布局

在 x86-64 架构(48 位有效地址)下,Linux 采用分体式布局:

0x0000 0000 0000 0000  ┌──────────────────┐
                        │                  │
                        │   用户空间       │  128 TB
                        │   (128TB)        │
                        │                  │
0x0000 7FFF FFFF FFFF  ├──────────────────┤
                        │   非规范区域     │
0xFFFF 8000 0000 0000  ├──────────────────┤
                        │                  │
                        │   内核空间       │  128 TB
                        │   (128TB)        │
                        │                  │
0xFFFF FFFF FFFF FFFF  └──────────────────┘

3.3 页表机制

x86-64 采用四级页表(48 位地址空间时),每级 9 位索引:

  • PML4(Page Map Level 4)
  • PDPT(Page Directory Pointer Table)
  • PD(Page Directory)
  • PT(Page Table)

虚拟地址 48 位 = PML4(9) + PDPT(9) + PD(9) + PT(9) + Offset(12)。CR3 寄存器指向 PML4 基址。

内核使用 pgd_offset()、p4d_offset()、pud_offset()、pmd_offset()、pte_offset_map() 逐级遍历。

3.4 透明大页(THP)

透明大页(Transparent Huge Pages)允许应用程序使用 2MB 甚至 1GB 的大页,无需修改应用代码。内核自动将可合并的连续 4KB 页升级为 2MB 大页,减少 TLB miss 和页表开销。

/sys/kernel/mm/transparent_hugepage/enabled = "madvise"
# 可选:always / madvise / never

四、内存分配策略

4.1 vmalloc 与 kmap

vmalloc 允许分配虚拟连续但物理不连续的内存,适用于需要大量内存但不需要 DMA 的场景(如模块加载)。它通过修改页表将不连续的物理页映射到连续的虚拟地址空间。

kmap / kmap_atomic 用于映射高端内存(32 位时代遗留,64 位系统中所有内存都是 lowmem,kmap 退化为直接映射)。

4.2 GFP 标志详解

GFP(Get Free Pages)标志定义了分配行为:

  • GFP_KERNEL:标准内核分配,允许睡眠等待
  • GFP_ATOMIC:原子分配,不可睡眠,用于中断上下文
  • GFP_DMA:DMA 兼容分配,限于 16MB 以下
  • GFP_DMA32:32 位 DMA 兼容分配
  • GFP_NOIO:不发起 I/O 来回收内存
  • GFP_NOFS:不发起文件系统操作

4.3 内存规整与回收

当空闲内存不足时,内核通过以下措施恢复:

  1. kswapd:后台守护线程,发现内存紧张时触发直接回收
  2. Direct Reclaim:分配路径同步回收,会造成分配延迟
  3. Memory Compaction:迁移内存页,规整碎片,为连续分配做准备
  4. OOM Killer
/proc/sys/vm/min_free_kbytes = 67584   # 保留的最小空闲
/proc/sys/vm/swappiness = 60          # 交换倾向 0-100
/proc/sys/vm/overcommit_memory = 0    # 超量分配策略

五、内存映射(mmap)

5.1 文件映射

mmap 系统调用可将文件映射到进程地址空间,实现"文件即内存"。映射初始为未加载状态,访问时触发 page fault,内核将文件内容从磁盘读入页面缓存。

void *addr = mmap(NULL, length, PROT_READ|PROT_WRITE,
                  MAP_PRIVATE, fd, 0);
// 修改内存即修改文件(时延写入)
munmap(addr, length);

5.2 匿名映射

匿名映射(MAP_ANONYMOUS)不与任何文件关联,用于分配大块内存(如 glibc 的 malloc 使用 mmap 分配大内存)。新映射的页面初始为零(清零页)。

5.3 page fault 处理流程

访问未映射地址 -> #PF (Page Fault)
  -> do_page_fault()
    -> handle_mm_fault()
      -> handle_pte_fault()
        -> do_fault()
          -> do_read_fault()    # 文件映射读缺失
          -> do_shared_fault()  # 写时复制(COW)
          -> do_anonymous_fault() # 匿名页第一次访问
        -> do_swap_page()      # 被换出的页面
        -> do_wp_page()        # 写时复制 (CoW)

5.4 写时复制(Copy-on-Write)

fork() 时子进程共享父进程物理页,标记为只读。当任一进程尝试写时触发 #PF,内核才真正复制一份。这极大减少了 fork 的开销。

六、内存 cgroup

内存 cgroup(memcg)限制了容器/进程组的内存使用。关键指标包括:

  • memory.limit_in_bytes:硬限制,超限触发 OOM
  • memory.soft_limit_in_bytes:软限制,仅在系统内存紧张时生效
  • memory.usage_in_bytes:当前含 page cache 的总使用量
  • memory.stat:详细统计(rss、cache、swap 等)

七、性能优化建议

  1. 减少内存碎片:使用 slab 缓存常用对象,避免频繁分合页
  2. 合理使用大页:数据库等应用启用 THP 提升 TLB 命中率
  3. 调整 swappiness:对延迟敏感应用减小 swappiness(如 10)
  4. 绑定 NUMA 节点:多处理器系统中使用 numactl 或 mbind 减少跨节点访问
  5. 预分配策略:关键路径避免触发 direct reclaim
  6. 监控内存水位:关注 /proc/zoneinfo 中 min/low/high 水位

总结

Linux 内核的内存管理系统在数十年演进中形成了分层次、多策略的高效体系。从底层的伙伴系统管理物理页,到 Slab 分配器满足小对象需求,再到虚拟地址空间提供进程隔离,每一层都精心设计以平衡性能与灵活性。理解这些机制对于系统性能调优、内核模块开发以及应用开发都至关重要。深入掌握内存管理,是通往 Linux 内核深层世界的必经之路。

点赞(0) 打赏

评论列表 共有 0 条评论

暂无评论
立即
投稿

微信公众账号

微信扫一扫加关注

发表
评论
返回
顶部