1. 内存管理概述
内存是计算机系统的核心资源,操作系统内核的内存管理子系统负责分配、回收、映射和保护内存资源。Linux 内存管理的设计兼顾了多个目标:物理内存的高效利用、虚拟内存的灵活映射、内核空间的高速分配,以及用户空间的隔离保护。
理解内存管理需要建立分层的视角:最底层是物理页框(page frame)的分配,由伙伴系统管理;之上是对象级分配,由 SLAB/SLUB 分配器提供;再之上是虚拟内存映射,通过页表实现地址转换。
2. 页与页框管理
Linux 以页(page)为基本单位管理内存。传统 x86 架构使用 4KB 页(可通过 PAE 扩展到 2MB),x86_64 支持 4KB、2MB 和 1GB 大页。ARM64 支持 4KB、16KB 和 64KB 页。
内核使用 struct page 描述每个物理页框。这是一个关键数据结构,但它并不直接包含页内数据——而是记录页的元数据:页框号(PFN)、引用计数、映射计数、页框状态标志等。
/* include/linux/mm_types.h */
struct page {
/* 联合:根据页的使用方式,不同字段生效 */
union {
struct { /* Page cache and anonymous pages */
struct list_head lru;
struct address_space *mapping;
pgoff_t index;
unsigned long private;
};
struct { /* slab, slob and slub */
union {
struct list_head slab_list;
struct { /* Partial pages */
struct page *next;
int pages; /* Nr of pages left */
int pobjects; /* Approximate count */
};
};
struct kmem_cache *slab_cache; /* not slob */
void *freelist; /* first free object */
union {
void *s_mem; /* slab: first object */
unsigned long counters; /* SLUB */
struct { /* SLUB */
unsigned inuse:16;
unsigned objects:15;
unsigned frozen:1;
};
};
};
/* ... 其他联合体成员用于不同场景 */
};
/* 引用计数 - 原子操作 */
atomic_t _refcount;
/* 页标志 */
unsigned long flags;
/* 虚拟内存映射信息 */
struct list_head lru;
};
物理内存被划分为多个区域:ZONE_DMA(低端地址,用于遗留设备)、ZONE_DMA32(32位可寻址区域)、ZONE_NORMAL(常规可直接映射区域)、ZONE_HIGHMEM(32位系统的高端内存)。每个区域独立管理空闲页框。
3. 伙伴系统
伙伴系统(Buddy System)是内核管理物理页框的核心算法。它将空闲页框组织成大小为 2^n 的块(1、2、4、8、16...页),满足分配需求的同时最小化外部碎片。
3.1 伙伴系统原理
两个页框是"伙伴"当且仅当:
- 它们大小相同(都是 2^n 页)
- 它们的物理地址连续
- 第一个页框的物理地址是 2^n * PAGE_SIZE 的倍数
/* mm/page_alloc.c - 伙伴系统核心分配函数 */
struct page *__alloc_pages(gfp_t gfp, unsigned int order,
int preferred_nid, nodemask_t *nodemask)
{
struct page *page;
unsigned int alloc_flags = ALLOC_WMARK_LOW;
/* 快速路径:从 per-cpu 页面缓存分配 */
page = get_page_from_freelist(alloc_mask, order, alloc_flags, ac);
if (likely(page))
goto out;
/* 慢速路径:可能需要回收内存或等待 */
page = __alloc_pages_slowpath(alloc_mask, order, ac);
out:
/* 清零页面(如果请求了 __GFP_ZERO) */
if (gfp_flags & __GFP_ZERO)
clear_page(page_address(page));
return page;
}
/* 从空闲列表获取页面 */
static struct page *rmqueue(struct zone *zone, unsigned int order,
gfp_t gfp_flags)
{
struct page *page;
/* 尝试从当前 order 分配 */
if (likely(order == 0)) {
/* 单页使用 pcp (per-cpu pageset) 缓存 */
page = rmqueue_pcplist(zone, gfp_flags);
} else {
/* 多页使用伙伴系统 */
page = rmqueue_buddy(zone, order);
}
return page;
}
4. SLAB 分配器
内核需要频繁分配和释放小对象(如 struct task_struct、struct inode 等)。直接使用伙伴系统分配 4KB 页框会造成严重的内部碎片,而且频繁的构造/析构开销很大。
SLAB 分配器解决了这个问题:它预先从伙伴系统分配页框,将其分割成固定大小的对象池,并维护空闲对象列表。这样,小对象分配只需从空闲列表取出一个对象,无需与伙伴系统交互。
4.1 SLAB 核心概念
/* mm/slab.c - SLAB 分配器核心数据结构 */
struct kmem_cache {
/* 每 CPU 缓存数组,加速无锁分配 */
struct array_cache __percpu *cpu_cache;
/* SLAB 链表 */
struct list_head slabs_full; /* 完全分配的 slab */
struct list_head slabs_partial; /* 部分空闲的 slab */
struct list_head slabs_free; /* 完全空闲的 slab */
unsigned int object_size; /* 对象实际大小 */
unsigned int size; /* 对齐后大小(含元数据) */
unsigned int align; /* 对齐要求 */
/* 构造函数/析构函数 */
void (*ctor)(void *);
/* 每个 slab 的对象数 */
unsigned int num;
/* GFP 标志 */
gfp_t allocflags;
const char *name; /* 调试用名称 */
};
/* 分配对象 */
void *kmem_cache_alloc(struct kmem_cache *cachep, gfp_t flags)
{
void *objp;
/* 尝试从 per-cpu 缓存分配(无锁快速路径) */
objp = ____cache_alloc(cachep, flags);
if (unlikely(!objp))
/* 慢速路径:从 slab 分配 */
objp = __cache_alloc_slowpath(cachep, flags);
/* 调用构造函数(如果有) */
if (cachep->ctor && objp)
cachep->ctor(objp);
return objp;
}
/* 释放对象 */
void kmem_cache_free(struct kmem_cache *cachep, void *objp)
{
/* 返回到 per-cpu 缓存或 slab */
__cache_free(cachep, objp, __builtin_return_address(0));
}
SLAB 分配器严格处理对齐问题,确保对象不会跨缓存行(cache line)。这对于多核性能至关重要,因为伪共享(false sharing)
会导致严重的缓存一致性开销。默认对齐到 L1 缓存行大小(通常 64 字节)。5. 页表与地址转换
现代操作系统使用虚拟内存为每个进程提供独立的地址空间。页表(Page Table)是实现虚拟地址到物理地址转换的数据结构。x86_64 使用四级页表(Linux 5.10+ 支持五级),ARM64 使用三级或四级。
5.1 x86_64 四级页表
/* arch/x86/include/asm/pgtable_types.h */
/* 页表项结构(PTE) */
typedef struct { unsigned long pte; } pte_t;
/* PTE 标志位 */
#define _PAGE_PRESENT (1UL << 0) /* 页在内存中 */
#define _PAGE_RW (1UL << 1) /* 可读写 */
#define _PAGE_USER (1UL << 2) /* 用户态可访问 */
#define _PAGE_PWT (1UL << 3) /* Write Through */
#define _PAGE_PCD (1UL << 4) /* Cache Disable */
#define _PAGE_ACCESSED (1UL << 5) /* 已访问(用于页面置换)*/
#define _PAGE_DIRTY (1UL << 6) /* 已修改(需要写回)*/
#define _PAGE_PSE (1UL << 7) /* Page Size Extension (2MB/1GB) */
#define _PAGE_GLOBAL (1UL << 8) /* 全局页(TLB 不刷新)*/
/* 页表遍历 - 虚拟地址转物理地址 */
unsigned long virt_to_phys(void *addr)
{
pgd_t *pgd;
p4d_t *p4d;
pud_t *pud;
pmd_t *pmd;
pte_t *pte;
unsigned long pfn;
pgd = pgd_offset(current->mm, (unsigned long)addr);
if (pgd_none(*pgd) || pgd_bad(*pgd))
return 0;
p4d = p4d_offset(pgd, (unsigned long)addr);
if (p4d_none(*p4d))
return 0;
pud = pud_offset(p4d, (unsigned long)addr);
if (pud_none(*pud) || pud_bad(*pud))
return 0;
pmd = pmd_offset(pud, (unsigned long)addr);
if (pmd_none(*pmd) || pmd_bad(*pmd))
return 0;
/* 大页检查 */
if (pmd_large(*pmd)) {
pfn = pmd_pfn(*pmd);
return (pfn << PAGE_SHIFT) | ((unsigned long)addr & ~PMD_MASK);
}
pte = pte_offset_kernel(pmd, (unsigned long)addr);
if (!pte_present(*pte))
return 0;
pfn = pte_pfn(*pte);
return (pfn << PAGE_SHIFT) | ((unsigned long)addr & ~PAGE_MASK);
}
5.2 TLB 与缓存
页表遍历需要 4 次内存访问,代价高昂。转换检测缓冲区(TLB)是 CPU 内部的硬件缓存,存储最近使用的虚拟到物理映射。当 TLB 命中时,地址转换几乎零开销;未命中则需要遍历页表(Page Walk)。
内核使用 invlpg 指令或重载 CR3 刷新 TLB。大页(2MB/1GB)减少页表层级,提高 TLB 效率,这对数据库等内存密集型应用至关重要。
6. vmalloc 与连续内存
伙伴系统保证分配的物理页框物理连续,但这在内存碎片化后变得困难。vmalloc 提供另一种选择:分配虚拟连续但物理离散的内存。
/* mm/vmalloc.c - vmalloc 实现 */
void *vmalloc(unsigned long size)
{
return __vmalloc_node_flags(size, NUMA_NO_NODE,
GFP_KERNEL);
}
/* vmalloc 内部使用页表映射 */
static void *__vmalloc_node(unsigned long size, unsigned long align,
gfp_t gfp_mask, pgprot_t prot,
int node, const void *caller)
{
struct vm_struct *area;
void *addr;
unsigned long real_size = size;
size = PAGE_ALIGN(size);
if (!size || (size >> PAGE_SHIFT) > total_pages)
return NULL;
/* 在 vmalloc 空间保留虚拟地址区域 */
area = __get_vm_area_node(size, align, VM_ALLOC, VMALLOC_START,
VMALLOC_END, node, gfp_mask, caller);
if (!area)
return NULL;
addr = area->addr;
area->no_vm = true;
/* 分配物理页并建立映射 */
if (__vmalloc_area_node(area, gfp_mask, prot, node) < 0) {
remove_vm_area(addr);
kfree(area);
return NULL;
}
return addr;
}
仅在内核需要大块内存(如加载内核模块、分配大数组)且 kmalloc 失败时使用。vmalloc 分配的内存访问速度较慢(需要页表遍历),且总空间受限于 vmalloc 区域大小(通常 128MB-1GB)。
7. SLUB:现代替代方案
传统 SLAB 分配器代码复杂,调试困难。Linux 2.6.22 引入 SLUB 作为默认分配器,它简化设计同时保持性能。SLUB 不再有显式的 slab 链表,而是使用页(struct page)直接跟踪对象。
/* mm/slub.c - SLUB 简化设计 */
struct kmem_cache {
struct kmem_cache_cpu __percpu *cpu_slab;
/* 同 SLAB 的元数据 */
unsigned int size; /* 对象大小(含元数据) */
unsigned int object_size; /* 用户请求大小 */
unsigned int offset; /* 空闲指针偏移 */
/* 每个 slab 的对象数 */
unsigned int oo; /* 编码的值 */
/* 部分空闲 slab 列表 */
struct kmem_cache_node *node[MAX_NUMNODES];
const char *name;
};
/* per-cpu 缓存 - 无锁快速路径 */
struct kmem_cache_cpu {
void **freelist; /* 指向第一个空闲对象 */
struct page *page; /* 当前 slab 页 */
struct page *partial; /* 部分空闲的备用 slab */
};
查看系统 SLUB 缓存状态的命令:
$ cat /proc/slabinfo | head -20
# name <active_objs> <num_objs> <objsize> <objperslab> <pagesperslab>
task_struct 1234 1500 6848 12 2 : tunables 0 0 0 : slabdata 125 125 0
inode_cache 5678 8000 600 13 2 : tunables 0 0 0 : slabdata 615 615 0
dentry 8901 10000 192 21 1 : tunables 0 0 0 : slabdata 476 476 0
8. 小结
本章探索了 Linux 内核内存管理的核心机制。我们学习了伙伴系统如何高效管理物理页框,SLAB/SLUB 如何优化小对象分配,以及页表如何实现虚拟到物理地址的转换。
关键要点:
- 伙伴系统管理物理页框,平衡分配效率与外部碎片
- SLAB/SLUB提供对象级分配,消除内部碎片,支持对象缓存
- 页表实现虚拟内存映射,TLB 加速地址转换
- vmalloc在物理内存不连续时提供虚拟连续空间
内存管理子系统的选择反映了经典的设计权衡:速度与空间、简单与功能、通用与专用。理解这些权衡是成为优秀系统程序员的关键。下一章将探讨内核模块编程,学习如何扩展内核功能。