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_vmaindex:在映射中的偏移量
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 内存规整与回收
当空闲内存不足时,内核通过以下措施恢复:
- kswapd:后台守护线程,发现内存紧张时触发直接回收
- Direct Reclaim:分配路径同步回收,会造成分配延迟
- Memory Compaction:迁移内存页,规整碎片,为连续分配做准备
- 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:硬限制,超限触发 OOMmemory.soft_limit_in_bytes:软限制,仅在系统内存紧张时生效memory.usage_in_bytes:当前含 page cache 的总使用量memory.stat:详细统计(rss、cache、swap 等)
七、性能优化建议
- 减少内存碎片:使用 slab 缓存常用对象,避免频繁分合页
- 合理使用大页:数据库等应用启用 THP 提升 TLB 命中率
- 调整 swappiness:对延迟敏感应用减小 swappiness(如 10)
- 绑定 NUMA 节点:多处理器系统中使用
numactl或mbind减少跨节点访问 - 预分配策略:关键路径避免触发 direct reclaim
- 监控内存水位:关注
/proc/zoneinfo中 min/low/high 水位
总结
Linux 内核的内存管理系统在数十年演进中形成了分层次、多策略的高效体系。从底层的伙伴系统管理物理页,到 Slab 分配器满足小对象需求,再到虚拟地址空间提供进程隔离,每一层都精心设计以平衡性能与灵活性。理解这些机制对于系统性能调优、内核模块开发以及应用开发都至关重要。深入掌握内存管理,是通往 Linux 内核深层世界的必经之路。

发表评论 取消回复