从物理内存到虚拟地址的炼金术
在计算机的世界里,内存管理如同一位精明的空间规划师,将有限的物理内存转化为无限的虚拟空间。Linux内核的内存管理系统堪称工程艺术的巅峰之作,它不仅要处理TB级物理内存的分配,还要为每个进程创建独立的虚拟宇宙。本章将深入Linux 6.x内存子系统,揭示其如何实现纳秒级分配与TB级扩展的魔法。
核心问题驱动:
// mm/page_alloc.c
struct zone {
struct free_area free_area[MAX_ORDER]; // 11个阶数(0-10)
};
struct free_area {
struct list_head free_list[MIGRATE_TYPES]; // 迁移类型链表
unsigned long nr_free; // 空闲页数
};
// 阶数对应页数
#define MAX_ORDER 11 // 2^10 = 1024页(4MB)
表:伙伴系统阶数与内存块大小(4KB页)
阶数 | 页数 | 大小 | 典型用途 |
---|---|---|---|
0 | 1 | 4KB | 小对象分配 |
1 | 2 | 8KB | slab缓存 |
2 | 4 | 16KB | 小文件缓存 |
5 | 32 | 128KB | 用户进程栈 |
8 | 256 | 1MB | DMA缓冲区 |
10 | 1024 | 4MB | 透明大页 |
// 核心分配函数
struct page *__alloc_pages(gfp_t gfp_mask, unsigned int order)
{
// 1. 快速路径:尝试低阶分配
page = get_page_from_freelist(gfp_mask, order, alloc_flags);
if (likely(page))
goto out;
// 2. 慢速路径:内存回收
page = __alloc_pages_slowpath(gfp_mask, order);
out:
return page;
}
enum migratetype {
MIGRATE_UNMOVABLE, // 不可移动页(内核栈)
MIGRATE_MOVABLE, // 可移动页(用户内存)
MIGRATE_RECLAIMABLE, // 可回收页(文件缓存)
MIGRATE_PCPTYPES, // per-CPU页
MIGRATE_HIGHATOMIC, // 高阶原子分配
MIGRATE_CMA, // 连续内存分配器
};
不同类型内存隔离管理,避免碎片化
kmem_cache ───▶ slab ───▶ 对象(object)
(缓存描述符) (内存页) (实际分配单元)
// 创建task_struct缓存
struct kmem_cache *task_struct_cache =
kmem_cache_create("task_struct", sizeof(struct task_struct),
ARCH_MIN_TASKALIGN, SLAB_PANIC|SLAB_ACCOUNT, NULL);
// 分配task_struct
struct task_struct *tsk = kmem_cache_alloc(task_struct_cache, GFP_KERNEL);
表:kmalloc vs kmem_cache分配延迟(ns)
操作 | kmalloc(128) | kmem_cache | 提升 |
---|---|---|---|
分配 | 120 | 18 | 6.7x |
释放 | 85 | 15 | 5.7x |
总时间 | 205 | 33 | 6.2x |
0x0000000000000000 ─┐
│ 用户空间(128TB)
0x00007FFFFFFFFFFF ─┤
│ 空洞
0xFFFF800000000000 ─┤
│ 内核空间(128TB)
0xFFFFFFFFFFFFFFFF ─┘
struct mm_struct {
struct vm_area_struct *mmap; // VMA链表
struct rb_root mm_rb; // VMA红黑树
pgd_t *pgd; // 页全局目录
unsigned long task_size; // 用户空间大小
unsigned long start_code, end_code; // 代码段
unsigned long start_data, end_data; // 数据段
unsigned long start_brk, brk; // 堆
unsigned long start_stack; // 栈
};
0x00400000 ─┐
├─ .text (代码段)
0x00600000 ─┤
├─ .data (数据段)
0x00800000 ─┤
├─ .bss (未初始化数据)
0x00A00000 ─┤
├─ 堆(heap) ↑
0x02000000 ─┤
│ ...
├─ 栈(stack) ↓
0x7FFFFFFF ─┘
CR3 → PML4 → PDPT → PD → PT → 物理页
# 获取当前进程CR3值
(gdb) p $cr3
$1 = 0x187f000
# 转换虚拟地址0x7ffff7a3e000
(gdb) set $pml4_index = (0x7ffff7a3e000 >> 39) & 0x1FF
(gdb) set $pdpt_index = (0x7ffff7a3e000 >> 30) & 0x1FF
(gdb) set $pd_index = (0x7ffff7a3e000 >> 21) & 0x1FF
(gdb) set $pt_index = (0x7ffff7a3e000 >> 12) & 0x1FF
# 逐级查询页表
(gdb) x /gx 0x187f000 + 8*$pml4_index
0x187f0e8: 0x800000001878e067 # PML4条目
(gdb) x /gx 0x1878e000 + 8*$pdpt_index
0x1878ed0: 0x800000001877c067 # PDPT条目
(gdb) x /gx 0x1877c000 + 8*$pd_index
0x1877c28: 0x8000000018607067 # PD条目
(gdb) x /gx 0x18607000 + 8*$pt_index
0x186077a0: 0x8000000037a3e167 # PT条目
# 计算物理地址
物理地址 = (0x37a3e167 & 0xFFFFF000) | (0x7ffff7a3e000 & 0xFFF)
= 0x37A3E000
// 页表项标志位
#define _PAGE_PRESENT 0x001 // 页存在
#define _PAGE_RW 0x002 // 可写
#define _PAGE_USER 0x004 // 用户空间
#define _PAGE_PWT 0x008 // 写通
#define _PAGE_PCD 0x010 // 禁用缓存
#define _PAGE_ACCESSED 0x020 // 已访问
#define _PAGE_DIRTY 0x040 // 已修改
#define _PAGE_PSE 0x080 // 大页标志
#define _PAGE_GLOBAL 0x100 // 全局页
测试场景 | 4KB页 | 2MB大页 | 提升 |
---|---|---|---|
页表遍历开销 | 24 cycles | 12 cycles | 2x |
TLB miss率 | 0.8% | 0.02% | 40x |
数据库事务 | 12,500 TPS | 15,800 TPS | 26% |
// mm/khugepaged.c
static void collapse_huge_page(struct mm_struct *mm, unsigned long address)
{
// 1. 检查是否可合并
if (!khugepaged_scan_pmd(mm, vma, addr))
return;
// 2. 分配2MB大页
huge_page = alloc_pages(HPAGE_PMD_ORDER, ...);
// 3. 复制小页内容
copy_page_to_huge_page(huge_page, pages, address);
// 4. 替换页表项
set_pmd_at(mm, address, pmd, pmd_mkhuge(pfn_pmd(pfn, prot)));
}
内存浪费:小内存应用被迫使用2MB大页
# 查看大页内存碎片
$ grep Huge /proc/meminfo
AnonHugePages: 102400 kB
HugePages_Free: 512 # 空闲大页
延迟波动:khugepaged合并操作引入不可预测延迟
// 合并操作可能阻塞
down_write(&mm->mmap_sem); // 获取写锁
OOM风险:大页不可交换,加剧内存压力
# 禁用透明大页
echo never > /sys/kernel/mm/transparent_hugepage/enabled
# 专用大页配置
echo 1024 > /proc/sys/vm/nr_hugepages # 预留1024个大页
// 泄漏模块代码
static int leak_init(void)
{
char *buf1 = kmalloc(128, GFP_KERNEL); // 未释放
char *buf2 = kmalloc(256, GFP_KERNEL);
kfree(buf2); // 只释放buf2
return 0;
}
内存布局:
+------------+------------+------------+
| 红区(16B) | 对象(128B) | 红区(16B) |
+------------+------------+------------+
^ ^ ^
影子字节 正常使用 影子字节
[ 158.456789] BUG: KASAN: slab-out-of-bounds in kmalloc_oob+0x3a/0x50
[ 158.457123] Write of size 1 at addr ffff88800a234f00 by task insmod/256
[ 158.457456]
[ 158.457789] CPU: 1 PID: 256 Comm: insmod Tainted: G W 6.3.0
[ 158.458123] Hardware name: QEMU Standard PC, BIOS 1.0.0
[ 158.458456] Call Trace:
[ 158.458789] dump_stack+0x94/0xce
[ 158.459123] print_address_description+0x1f/0x2b0
[ 158.459456] ? kmalloc_oob+0x3a/0x50
[ 158.459789] __kasan_report+0x10c/0x120
[ 158.460123] ? kmalloc_oob+0x3a/0x50
[ 158.460456] kasan_report+0xe/0x20
[ 158.460789] kmalloc_oob+0x3a/0x50
[ 158.461123] ? 0xffffffffc0005000
[ 158.461456] do_one_initcall+0x50/0x1f0
# 查看泄漏地址影子状态
(gdb) p/x *(unsigned char *)kasan_mem_to_shadow(0xffff88800a234f00)
$1 = 0xf3 # 11110011:前4字节可访问,后4字节不可访问
# 正常区域影子值
(gdb) p/x *(unsigned char *)kasan_mem_to_shadow(0xffff88800a234e80)
$2 = 0x00 # 00000000:全部可访问
建筑学隐喻:
伙伴系统是钢筋水泥供应商
slab分配器是预制件工厂
页表是建筑蓝图
虚拟地址空间是摩天大楼
KASAN是建筑质量检测仪
在下一期中,我们将深入探讨:
彩蛋:我们将用FUSE编写一个简易文件系统,体验VFS的抽象之美!
本文使用知识共享署名4.0许可证,欢迎转载传播但须保留作者信息
技术校对:Linux 6.4.15源码、Intel 64架构手册
实验环境:QEMU 8.1.0, KASAN enabled kernel