Linux内存管理-malloc虚拟内存到物理映射详细分析
malloc虚拟内存到物理映射详细分析
概述
本文档详细分析了Linux内核中malloc分配虚拟内存后,进行实际读写操作时如何进行物理内存映射的完整过程,重点分析了buddy分配器在其中的作用,并澄清了slab分配器的角色。
1. 背景知识
1.1 按需分页机制
- 虚拟内存分配:malloc只分配虚拟地址空间,不立即分配物理内存
- 延迟分配:物理内存在首次访问时通过页面故障机制按需分配
- 优势:节省物理内存,提高系统性能
1.2 内存分配器角色
- Buddy分配器:负责以页为单位分配物理内存,支持不同order
- Slab分配器:负责内核小对象分配,不参与用户匿名页分配
2. 页面故障处理流程
2.1 从虚拟地址访问到do_page_fault的完整路径
当用户程序访问malloc分配但未映射的虚拟地址时,会触发以下完整的调用链:
用户程序访问虚拟地址↓
CPU检测到页表项不存在或权限不足↓
CPU产生页面故障异常(Page Fault Exception)↓
硬件保存现场并跳转到异常向量表↓
内核异常处理入口(arch/*/kernel/entry_*.S)↓
page_fault异常处理程序↓
do_page_fault() [架构相关 - arch/*/mm/fault.c]↓
handle_mm_fault() [通用处理 - mm/memory.c]↓
__handle_mm_fault()
2.1.1 硬件层面的故障触发机制
MMU页表查找过程:
- 地址转换请求:CPU执行访存指令时,MMU开始地址转换
- 页表遍历:MMU按照页表层次结构查找对应的页表项
- PGD (Page Global Directory) → PUD (Page Upper Directory) → PMD (Page Middle Directory) → PTE (Page Table Entry)
- 故障条件检测:
- 页表项不存在(Present位为0)
- 权限不足(如写只读页面)
- 访问用户页面但处于内核模式等
异常产生与处理:
// 硬件自动完成的操作
1. 保存故障地址到CR2寄存器(x86)或相应寄存器
2. 保存错误码(包含故障原因)
3. 保存当前执行上下文(寄存器状态)
4. 跳转到页面故障异常处理程序入口
2.1.2 内核异常处理入口
汇编层面的处理(以x86为例):
// arch/x86/kernel/entry_64.S
ENTRY(page_fault)// 保存寄存器状态// 获取故障地址和错误码// 调用C语言处理函数call do_page_fault// 恢复现场并返回
END(page_fault)
2.2 已映射虚拟内存的访问实现
对于已经建立物理映射的虚拟内存,访问过程完全不同:
2.2.1 正常的地址转换过程
用户程序访问已映射的虚拟地址↓
MMU进行地址转换↓
查找TLB(Translation Lookaside Buffer)↓
TLB命中 → 直接获得物理地址 → 访问物理内存↓
TLB未命中 → 硬件页表遍历 → 找到有效PTE → 更新TLB → 访问物理内存
2.2.2 TLB的作用机制
TLB缓存机制:
- 目的:缓存最近使用的虚拟地址到物理地址的映射
- 结构:硬件维护的高速缓存,包含虚拟页号和对应的物理页框号
- 命中率:通常达到95%以上,大大提高地址转换效率
页表遍历过程(TLB未命中时):
// 硬件自动完成的页表遍历(以4级页表为例)
1. 从CR3寄存器获取PGD基地址
2. 用虚拟地址的[47:39]位索引PGD,获取PUD地址
3. 用虚拟地址的[38:30]位索引PUD,获取PMD地址
4. 用虚拟地址的[29:21]位索引PMD,获取PTE地址
5. 用虚拟地址的[20:12]位索引PTE,获取物理页框号
6. 物理页框号 + 页内偏移[11:0] = 最终物理地址
2.3 __handle_mm_fault函数分析
位置:mm/memory.c:3342
核心逻辑:
static int __handle_mm_fault(struct mm_struct *mm, struct vm_area_struct *vma,unsigned long address, unsigned int flags)
{pgd_t *pgd;pud_t *pud; pmd_t *pmd;pte_t *pte;// 1. 检查是否为大页面if (unlikely(is_vm_hugetlb_page(vma)))return hugetlb_fault(mm, vma, address, flags);// 2. 逐级分配页表pgd = pgd_offset(mm, address);pud = pud_alloc(mm, pgd, address);if (!pud) return VM_FAULT_OOM;pmd = pmd_alloc(mm, pud, address);if (!pmd) return VM_FAULT_OOM;// 3. 透明大页处理if (pmd_none(*pmd) && transparent_hugepage_enabled(vma)) {int ret = create_huge_pmd(mm, vma, address, pmd, flags);if (!(ret & VM_FAULT_FALLBACK))return ret;}// 4. PTE分配与处理if (unlikely(pmd_none(*pmd)) &&unlikely(__pte_alloc(mm, vma, pmd, address)))return VM_FAULT_OOM;pte = pte_offset_map(pmd, address);return handle_pte_fault(mm, vma, address, pte, pmd, flags);
}
2.4 handle_pte_fault函数分析
位置:mm/memory.c:3275
故障类型判断:
static int handle_pte_fault(struct mm_struct *mm,struct vm_area_struct *vma, unsigned long address,pte_t *pte, pmd_t *pmd, unsigned int flags)
{pte_t entry = *pte;if (!pte_present(entry)) {if (pte_none(entry)) {// 匿名页面故障if (vma_is_anonymous(vma))return do_anonymous_page(mm, vma, address, pte, pmd, flags);else// 文件页面故障return do_fault(mm, vma, address, pte, pmd, flags, entry);}// 交换页面故障return do_swap_page(mm, vma, address, pte, pmd, flags, entry);}// 写保护故障(COW)if (flags & FAULT_FLAG_WRITE) {if (!pte_write(entry))return do_wp_page(mm, vma, address, pte, pmd, ptl, entry);}return 0;
}
3. 匿名页面分配详细过程
3.1 do_anonymous_page函数分析
位置:mm/memory.c:2670
完整流程:
static int do_anonymous_page(struct mm_struct *mm, struct vm_area_struct *vma,unsigned long address, pte_t *page_table, pmd_t *pmd,unsigned int flags)
{struct mem_cgroup *memcg;struct page *page;spinlock_t *ptl;pte_t entry;// 1. 检查共享标志if (vma->vm_flags & VM_SHARED)return VM_FAULT_SIGBUS;// 2. 只读访问使用零页面if (!(flags & FAULT_FLAG_WRITE) && !mm_forbids_zeropage(mm)) {entry = pte_mkspecial(pfn_pte(my_zero_pfn(address),vma->vm_page_prot));// 设置零页面映射goto setpte;}// 3. 准备匿名VMAif (unlikely(anon_vma_prepare(vma)))goto oom;// 4. 分配物理页面page = alloc_zeroed_user_highpage_movable(vma, address);if (!page)goto oom;// 5. 内存控制组计费if (mem_cgroup_try_charge(page, mm, GFP_KERNEL, &memcg))goto oom_free_page;// 6. 设置页面状态__SetPageUptodate(page);// 7. 构造PTE项entry = mk_pte(page, vma->vm_page_prot);if (vma->vm_flags & VM_WRITE)entry = pte_mkwrite(pte_mkdirty(entry));// 8. 获取页表锁并检查page_table = pte_offset_map_lock(mm, pmd, address, &ptl);if (!pte_none(*page_table))goto release;// 9. 更新统计计数inc_mm_counter_fast(mm, MM_ANONPAGES);// 10. 建立反向映射page_add_new_anon_rmap(page, vma, address);// 11. 提交内存控制组计费mem_cgroup_commit_charge(page, memcg, false);// 12. 添加到LRU链表lru_cache_add_active_or_unevictable(page, vma);setpte:// 13. 设置页表项set_pte_at(mm, address, page_table, entry);// 14. 更新MMU缓存update_mmu_cache(vma, address, page_table);pte_unmap_unlock(page_table, ptl);return 0;release:mem_cgroup_cancel_charge(page, memcg);page_cache_release(page);goto unlock;
oom_free_page:page_cache_release(page);
oom:return VM_FAULT_OOM;
}
3.2 关键优化机制
- 零页面共享:只读访问使用全局零页面,节省内存
- COW机制:写入时才分配真实物理页面
- 延迟分配:只在实际访问时分配物理内存
4. Buddy分配器系统化分析
4.1 Buddy分配器基本原理
4.1.1 核心设计思想
伙伴系统算法:
- 基本单位:以页面(通常4KB)为基本分配单位
- 分配粒度:支持2^order个连续页面的分配(order从0到MAX_ORDER-1)
- 伙伴关系:相同大小的两个相邻内存块互为伙伴
- 合并机制:释放时自动与伙伴块合并,减少外部碎片
4.1.2 数据结构组织
Zone结构:
struct zone {// 空闲页面链表,按order组织struct free_area free_area[MAX_ORDER];// 水位标记unsigned long watermark[NR_WMARK];// 迁移类型管理unsigned long nr_migrate_reserve_block;// 统计信息atomic_long_t vm_stat[NR_VM_ZONE_STAT_ITEMS];
};
Free Area结构:
struct free_area {struct list_head free_list[MIGRATE_TYPES]; // 按迁移类型分类的空闲链表unsigned long nr_free; // 空闲块数量
};
4.1.3 迁移类型分类
MIGRATE_UNMOVABLE:不可移动页面
- 内核代码页、内核数据结构
- 页表页、内核栈等
MIGRATE_MOVABLE:可移动页面
- 用户匿名页面(malloc分配的页面属于此类)
- 页面缓存页面
- 支持内存压缩和热插拔
MIGRATE_RECLAIMABLE:可回收页面
- 文件系统缓存
- 可以通过回写到存储设备来回收
4.2 alloc_zeroed_user_highpage_movable详细解析
4.2.1 函数调用层次与职责
alloc_zeroed_user_highpage_movable(vma, address)
├── 职责:为用户VMA分配可移动的高端内存页并清零
├── 位置:include/linux/highmem.h:180
└── 实现:调用__alloc_zeroed_user_highpage(__GFP_MOVABLE, vma, vaddr)__alloc_zeroed_user_highpage(movableflags, vma, vaddr)
├── 职责:分配页面并清零,支持调用者指定可移动标志
├── 位置:include/linux/highmem.h:157
├── GFP标志组合:GFP_HIGHUSER | __GFP_MOVABLE
└── 实现:alloc_page_vma() + clear_user_highpage()alloc_page_vma(gfp_mask, vma, addr)
├── 职责:VMA感知的单页分配(宏定义)
├── 位置:include/linux/gfp.h:467
└── 展开为:alloc_pages_vma(gfp_mask, 0, vma, addr, numa_node_id(), false)alloc_pages_vma(gfp, order, vma, addr, node, hugepage)
├── 职责:应用NUMA策略的VMA页面分配
├── 位置:mm/mempolicy.c:1950
├── NUMA策略处理:MPOL_INTERLEAVE/PREFERRED/BIND/DEFAULT
└── 核心调用:__alloc_pages_nodemask()__alloc_pages_nodemask(gfp_mask, order, zonelist, nodemask)
├── 职责:Buddy分配器的核心入口点
├── 位置:mm/page_alloc.c:3225
├── 快速路径:get_page_from_freelist()
└── 慢速路径:__alloc_pages_slowpath()
4.2.2 GFP标志详细解析
GFP_HIGHUSER标志组合:
#define GFP_HIGHUSER (GFP_USER | __GFP_HIGHMEM)
#define GFP_USER (GFP_RECLAIM | __GFP_IO | __GFP_FS | __GFP_HARDWALL)
#define GFP_RECLAIM (__GFP_DIRECT_RECLAIM | __GFP_KSWAPD_RECLAIM)
标志含义解析:
- __GFP_HIGHMEM:允许从高端内存分配
- __GFP_MOVABLE:页面可移动,支持内存压缩
- __GFP_DIRECT_RECLAIM:允许直接回收内存
- __GFP_KSWAPD_RECLAIM:允许唤醒kswapd进行后台回收
- __GFP_IO:允许启动I/O操作进行回收
- __GFP_FS:允许调用文件系统进行回收
4.3 Buddy分配器核心算法
4.3.1 快速路径分配算法
get_page_from_freelist核心逻辑:
static struct page *
get_page_from_freelist(gfp_t gfp_mask, unsigned int order, int alloc_flags,const struct alloc_context *ac)
{// 1. Zone遍历策略for_each_zone_zonelist_nodemask(zone, z, zonelist, ac->high_zoneidx, ac->nodemask) {// 2. 约束检查if (!cpuset_zone_allowed(zone, gfp_mask)) continue;if (!zone_dirty_ok(zone)) continue;// 3. 水位检查mark = zone->watermark[alloc_flags & ALLOC_WMARK_MASK];if (!zone_watermark_ok(zone, order, mark, ac->classzone_idx, alloc_flags)) {// 尝试zone回收if (zone_reclaim_mode != 0) {ret = zone_reclaim(zone, gfp_mask, order);if (zone_watermark_ok(zone, order, mark, ac->classzone_idx, alloc_flags))goto try_this_zone;}continue;}try_this_zone:// 4. 实际分配page = buffered_rmqueue(ac->preferred_zone, zone, order,gfp_mask, alloc_flags, ac->migratetype);if (page) {// 5. 页面预处理if (prep_new_page(page, order, gfp_mask, alloc_flags))goto try_this_zone;return page;}}return NULL;
}
4.3.2 buffered_rmqueue分配细节
Per-CPU页面缓存机制:
static struct page *buffered_rmqueue(struct zone *preferred_zone,struct zone *zone, unsigned int order,gfp_t gfp_flags, int alloc_flags,int migratetype)
{struct page *page;// 单页分配优先使用per-CPU缓存if (likely(order == 0)) {struct per_cpu_pages *pcp;struct list_head *list;local_irq_save(flags);pcp = &this_cpu_ptr(zone->pageset)->pcp;list = &pcp->lists[migratetype];// 从per-CPU缓存获取if (!list_empty(list)) {page = list_first_entry(list, struct page, lru);list_del(&page->lru);pcp->count--;} else {// 缓存为空,从伙伴系统批量获取page = rmqueue_bulk(zone, 0, pcp->batch, list,migratetype, cold);}local_irq_restore(flags);} else {// 多页分配直接从伙伴系统获取page = __rmqueue(zone, order, migratetype);}return page;
}
4.3.3 伙伴系统核心分配算法
__rmqueue函数实现:
static struct page *__rmqueue(struct zone *zone, unsigned int order,int migratetype)
{struct page *page;// 1. 尝试从指定迁移类型分配page = __rmqueue_smallest(zone, order, migratetype);// 2. 指定类型不足,尝试备用类型if (unlikely(!page)) {page = __rmqueue_fallback(zone, order, migratetype);}return page;
}static struct page *__rmqueue_smallest(struct zone *zone, unsigned int order,int migratetype)
{unsigned int current_order;struct free_area *area;struct page *page;// 从请求的order开始向上查找for (current_order = order; current_order < MAX_ORDER; ++current_order) {area = &(zone->free_area[current_order]);// 检查对应迁移类型的空闲链表if (list_empty(&area->free_list[migratetype]))continue;// 找到空闲块,从链表中移除page = list_entry(area->free_list[migratetype].next,struct page, lru);list_del(&page->lru);rmv_page_order(page);area->nr_free--;// 分割大块为所需大小expand(zone, page, order, current_order, area, migratetype);return page;}return NULL;
}
4.4 内存映射与Buddy分配器的关联
4.4.1 完整的关联链路
用户程序malloc() → 分配VMA → 首次访问触发缺页↓
do_anonymous_page() → 需要物理页面↓
alloc_zeroed_user_highpage_movable() → 请求可移动用户页↓
Buddy分配器 → 从MIGRATE_MOVABLE类型分配物理页面↓
clear_user_highpage() → 清零页面内容↓
mk_pte() → 构造页表项↓
set_pte_at() → 建立虚拟地址到物理地址的映射↓
用户程序可以正常访问该虚拟地址
4.4.2 关键的设计决策
为什么选择MIGRATE_MOVABLE:
- 内存压缩支持:可移动页面可以被迁移到其他位置,支持内存碎片整理
- 热插拔支持:支持内存热插拔操作
- 大页面分配:有利于透明大页面分配
页面清零的必要性:
- 安全性:防止信息泄露,新分配的页面不包含之前的数据
- 一致性:确保malloc返回的内存内容为零
- 性能考虑:在分配时清零比使用时清零更高效
4.5 内存压缩与碎片整理机制
4.5.1 内存碎片问题
外部碎片的产生:
- 随着系统运行,内存分配和释放导致可用内存被分割成小块
- 虽然总的空闲内存足够,但无法满足大块连续内存的分配需求
- 特别影响高阶页面分配(order > 0)和透明大页面分配
碎片化的影响:
// 碎片化示例:总共有足够的空闲页面,但不连续
Zone状态:
[已用][空闲1页][已用][空闲1页][已用][空闲2页][已用]
// 需要分配4页连续内存时失败,尽管总空闲页面 >= 4页
4.5.2 内存压缩核心算法
压缩原理:
- 通过页面迁移将可移动页面集中到zone的一端
- 将空闲页面集中到zone的另一端
- 形成更大的连续空闲内存块
双扫描器算法:
// mm/compaction.c 核心算法
static int compact_zone(struct zone *zone, struct compact_control *cc)
{// 1. 迁移扫描器:从zone开始向前扫描,寻找可移动页面unsigned long migrate_pfn = zone->zone_start_pfn;// 2. 空闲扫描器:从zone末尾向后扫描,寻找空闲页面unsigned long free_pfn = zone_end_pfn(zone);while (migrate_pfn < free_pfn) {// 3. 隔离可移动页面isolate_migratepages_range(cc, migrate_pfn, end_pfn);// 4. 隔离空闲页面作为迁移目标isolate_freepages(cc, &free_pfn, migrate_pfn);// 5. 执行页面迁移migrate_pages(&cc->migratepages, compaction_alloc,compaction_free, (unsigned long)cc, cc->mode, MR_COMPACTION);// 6. 更新扫描位置migrate_pfn = cc->migrate_pfn;}
}
4.5.3 压缩触发条件
自动触发场景:
// 1. 高阶页面分配失败时
static struct page *__alloc_pages_slowpath(gfp_t gfp_mask, unsigned int order,struct alloc_context *ac)
{// 尝试直接压缩page = __alloc_pages_direct_compact(gfp_mask, order, alloc_flags, ac,mode, &contended_compaction,&deferred_compaction);if (page)return page;
}// 2. kswapd后台回收时
static void kswapd_try_to_sleep(pg_data_t *pgdat, int order,int classzone_idx)
{if (pgdat_needs_compaction && sc.nr_reclaimed > nr_attempted)wakeup_kcompactd(pgdat, order, classzone_idx);
}
压缩适用性判断:
static unsigned long __compaction_suitable(struct zone *zone, int order,int alloc_flags, int classzone_idx)
{// 1. 检查水位标记if (zone_watermark_ok(zone, order, watermark, classzone_idx, alloc_flags))return COMPACT_PARTIAL; // 无需压缩// 2. 检查碎片化程度fragindex = fragmentation_index(zone, order);if (fragindex >= 0 && fragindex <= sysctl_extfrag_threshold)return COMPACT_NOT_SUITABLE_ZONE; // 碎片化不严重return COMPACT_CONTINUE; // 适合压缩
}
4.5.4 压缩模式与策略
异步压缩(MIGRATE_ASYNC):
- 不会阻塞,遇到锁竞争立即放弃
- 只迁移容易移动的页面
- 适用于用户态分配请求
同步压缩(MIGRATE_SYNC):
- 可以等待锁和I/O操作
- 能够迁移更多类型的页面
- 适用于内核态和关键分配
轻量同步压缩(MIGRATE_SYNC_LIGHT):
- 介于异步和同步之间
- 平衡性能和成功率
4.5.5 压缩效果与优化
压缩成功率影响因素:
// 1. 页面类型分布
MIGRATE_UNMOVABLE // 不可移动,影响压缩效果
MIGRATE_MOVABLE // 可移动,压缩效果好
MIGRATE_RECLAIMABLE // 可回收,中等效果// 2. 内存使用模式
- 大量长期存在的不可移动页面会降低压缩效果
- 频繁的小内存分配释放有助于压缩
- 内存热点区域可能影响压缩性能
压缩优化机制:
// 1. 延迟压缩机制
void defer_compaction(struct zone *zone, int order)
{zone->compact_considered = 0;zone->compact_defer_shift++;if (order < zone->compact_order_failed)zone->compact_order_failed = order;
}// 2. 跳过不适合的页面块
static bool suitable_migration_source(struct compact_control *cc,struct page *page)
{if (pageblock_skip_persistent(page))return false;return true;
}// 3. Per-CPU页面缓存避免碎片
static struct page *buffered_rmqueue(struct zone *preferred_zone,struct zone *zone, unsigned int order,gfp_t gfp_flags, int alloc_flags,int migratetype)
{// 单页分配优先使用per-CPU缓存,减少碎片if (likely(order == 0)) {// 从per-CPU缓存分配}
}
4.5.6 压缩与MIGRATE_MOVABLE的协同
设计协同:
// 1. 分配时指定可移动类型
page = alloc_zeroed_user_highpage_movable(vma, address);
// 等价于:
page = __alloc_zeroed_user_highpage(__GFP_MOVABLE, vma, vaddr);// 2. 压缩时优先处理可移动页面
static bool suitable_migration_source(struct compact_control *cc,struct page *page)
{int migratetype = get_pageblock_migratetype(page);// 异步压缩只处理可移动页面if (cc->mode == MIGRATE_ASYNC && !migrate_async_suitable(migratetype))return false;return true;
}// 3. 迁移类型保持一致
static struct page *compaction_alloc(struct page *migratepage,unsigned long data)
{struct compact_control *cc = (struct compact_control *)data;struct page *freepage;// 从空闲页面列表获取目标页面,保持迁移类型freepage = list_entry(cc->freepages.next, struct page, lru);list_del(&freepage->lru);cc->nr_freepages--;return freepage;
}
协同效果:
- 提高压缩成功率:可移动页面更容易被迁移
- 减少压缩开销:避免尝试迁移不可移动页面
- 保持系统稳定性:关键内核页面不受压缩影响
- 支持大页分配:为透明大页面创造连续内存空间
这种精心设计的内存压缩机制与MIGRATE_MOVABLE标志的协同,使得Linux能够在长期运行中有效对抗内存碎片化,保证大块内存分配的成功率。
5. 内存分配的可移动性分析
5.1 迁移类型概述
Linux内核根据页面的使用特性将内存分为不同的迁移类型,这是内存管理的重要分类机制:
// include/linux/mmzone.h
enum {MIGRATE_UNMOVABLE, // 不可移动页面MIGRATE_MOVABLE, // 可移动页面 MIGRATE_RECLAIMABLE, // 可回收页面MIGRATE_PCPTYPES, // Per-CPU页面类型数量MIGRATE_HIGHATOMIC, // 高优先级原子分配
#ifdef CONFIG_CMAMIGRATE_CMA, // 连续内存分配器页面
#endif
#ifdef CONFIG_MEMORY_ISOLATIONMIGRATE_ISOLATE, // 隔离页面
#endifMIGRATE_TYPES
};
5.2 可移动页面(MIGRATE_MOVABLE)场景
5.2.1 用户空间匿名页面
典型场景:
// 1. malloc分配的堆内存
void *ptr = malloc(4096); // 触发匿名页分配// 2. 用户栈页面
int local_array[1024]; // 栈扩展时分配的页面// 3. mmap匿名映射
void *addr = mmap(NULL, 4096, PROT_READ|PROT_WRITE, MAP_PRIVATE|MAP_ANONYMOUS, -1, 0);// 4. brk系统调用扩展堆
sbrk(4096); // 扩展数据段
分配路径:
// do_anonymous_page() -> alloc_zeroed_user_highpage_movable()
page = alloc_zeroed_user_highpage_movable(vma, address);
// 等价于使用 GFP_HIGHUSER | __GFP_MOVABLE 标志
5.2.2 页面缓存(Page Cache)
文件映射页面:
// 1. 文件读取缓存
int fd = open("file.txt", O_RDONLY);
read(fd, buffer, 4096); // 可能触发页面缓存分配// 2. mmap文件映射
void *mapped = mmap(NULL, file_size, PROT_READ, MAP_PRIVATE, fd, 0);// 3. 写回缓存
write(fd, data, 4096); // 脏页缓存,可移动但需要写回
分配特点:
- 可以通过写回到存储设备来释放
- 支持页面迁移和内存压缩
- 在内存压力下可以被回收
5.2.3 用户空间共享内存
共享内存场景:
// 1. System V共享内存
int shmid = shmget(IPC_PRIVATE, 4096, IPC_CREAT | 0666);
void *shm_addr = shmat(shmid, NULL, 0);// 2. POSIX共享内存
int fd = shm_open("/myshm", O_CREAT | O_RDWR, 0666);
ftruncate(fd, 4096);
void *addr = mmap(NULL, 4096, PROT_READ|PROT_WRITE, MAP_SHARED, fd, 0);// 3. tmpfs文件系统
// /dev/shm下的文件实际存储在内存中,页面可移动
5.3 不可移动页面(MIGRATE_UNMOVABLE)场景
5.3.1 内核核心数据结构
关键内核对象:
// 1. 内核代码段和数据段
// 内核镜像本身,包含代码和静态数据// 2. 页表页面
pte_t *pte = pte_alloc_kernel(pmd, address); // 页表分配// 3. 内核栈
// 每个进程/线程的内核栈,通常8KB或16KB// 4. 中断处理相关结构
// 中断向量表、中断处理程序栈等// 5. 内核关键数据结构
struct task_struct *task; // 进程控制块
struct mm_struct *mm; // 内存描述符
struct vm_area_struct *vma; // VMA结构
5.3.2 DMA缓冲区
DMA内存分配:
// 1. 一致性DMA内存
void *coherent_mem = dma_alloc_coherent(dev, size, &dma_handle, GFP_KERNEL);// 2. 流式DMA映射
dma_addr_t dma_addr = dma_map_single(dev, cpu_addr, size, direction);// 3. 低端内存DMA(某些老设备)
void *dma_mem = kmalloc(size, GFP_KERNEL | GFP_DMA);
不可移动原因:
- 硬件设备直接访问物理地址
- 移动会破坏DMA传输的连续性
- 某些设备只能访问特定地址范围
5.3.3 原子分配和紧急分配
高优先级分配:
// 1. 中断上下文分配
void *ptr = kmalloc(size, GFP_ATOMIC);// 2. 自旋锁持有期间分配
spin_lock_irqsave(&lock, flags);
void *mem = kmalloc(size, GFP_ATOMIC);
spin_unlock_irqrestore(&lock, flags);// 3. 网络数据包分配
struct sk_buff *skb = alloc_skb(len, GFP_ATOMIC);
5.3.4 内核模块和驱动程序
模块内存分配:
// 1. 模块加载时的代码段
// 内核模块的代码和数据段// 2. 驱动程序私有数据
struct my_device *dev = kmalloc(sizeof(*dev), GFP_KERNEL);// 3. 硬件寄存器映射
void __iomem *regs = ioremap(phys_addr, size);
5.4 可回收页面(MIGRATE_RECLAIMABLE)场景
5.4.1 Slab/SLUB分配器缓存
内核对象缓存:
// 1. 文件系统相关缓存
struct inode *inode = kmem_cache_alloc(inode_cachep, GFP_KERNEL);
struct dentry *dentry = kmem_cache_alloc(dentry_cache, GFP_KERNEL);// 2. 网络协议栈缓存
struct sock *sk = sk_alloc(net, family, priority, prot, kern);// 3. 通用内核对象
void *obj = kmalloc(size, GFP_KERNEL); // 小对象通过slab分配
5.4.2 文件系统元数据
元数据缓存:
// 1. 目录项缓存(dcache)
// 文件路径解析缓存,可以重建// 2. 索引节点缓存(icache)
// 文件元数据缓存,可以从磁盘重新读取// 3. 缓冲区头(buffer_head)
// 块设备I/O缓存,可以释放和重建
5.5 迁移类型的动态转换
5.5.1 GFP标志到迁移类型的转换
static inline int gfpflags_to_migratetype(const gfp_t gfp_flags)
{// 如果禁用了按移动性分组,所有页面都是不可移动的if (unlikely(page_group_by_mobility_disabled))return MIGRATE_UNMOVABLE;// 根据GFP标志的移动性位确定迁移类型return (gfp_flags & GFP_MOVABLE_MASK) >> GFP_MOVABLE_SHIFT;
}
标志映射关系:
// GFP_KERNEL -> MIGRATE_UNMOVABLE
void *ptr1 = kmalloc(size, GFP_KERNEL);// GFP_KERNEL | __GFP_MOVABLE -> MIGRATE_MOVABLE
void *ptr2 = kmalloc(size, GFP_KERNEL | __GFP_MOVABLE);// GFP_KERNEL | __GFP_RECLAIMABLE -> MIGRATE_RECLAIMABLE
struct kmem_cache *cache = kmem_cache_create(..., SLAB_RECLAIM_ACCOUNT, ...);
5.5.2 运行时迁移类型调整
页面块迁移类型转换:
// 1. 内存碎片严重时,可能会"偷取"其他类型的页面块
static struct page *__rmqueue_fallback(struct zone *zone, unsigned int order,int start_migratetype)
{// 从其他迁移类型"借用"页面块// 并可能改变整个页面块的迁移类型
}// 2. CMA区域的动态转换
// MIGRATE_CMA <-> MIGRATE_MOVABLE 之间的转换
5.6 实际应用中的选择策略
5.6.1 应用程序内存分配
用户态分配:
// 所有用户态malloc/new分配都是MIGRATE_MOVABLE
char *buffer = malloc(1024); // 可移动
int *array = new int[1000]; // 可移动
void *mmap_mem = mmap(...); // 可移动(匿名映射)
5.6.2 内核驱动开发
驱动程序分配策略:
// 1. 一般内核数据结构 - 不可移动
struct my_device *dev = kzalloc(sizeof(*dev), GFP_KERNEL);// 2. 大块临时缓冲区 - 可考虑可移动
void *temp_buf = kmalloc(large_size, GFP_KERNEL | __GFP_MOVABLE);// 3. DMA缓冲区 - 必须不可移动
void *dma_buf = dma_alloc_coherent(dev, size, &dma_handle, GFP_KERNEL);// 4. 可回收的缓存对象
struct my_cache_obj *obj = kmem_cache_alloc(my_cache, GFP_KERNEL);
5.7 性能和稳定性考虑
5.7.1 可移动页面的优势
- 内存碎片整理:支持内存压缩,减少外部碎片
- 内存热插拔:支持内存模块的热插拔操作
- 大页分配:有利于透明大页面的分配
- 系统长期稳定性:防止内存碎片化导致的分配失败
5.7.2 不可移动页面的必要性
- 系统稳定性:关键内核结构不能被随意移动
- 硬件兼容性:DMA等硬件操作需要固定物理地址
- 性能考虑:避免移动开销影响关键路径
- 原子操作:中断上下文等不能进行复杂的页面迁移
通过这种精细的分类管理,Linux内核在保证系统稳定性的同时,最大化了内存使用效率和抗碎片化能力。
6. 页表映射建立过程
6.1 do_set_pte函数分析
位置:mm/memory.c:2813
void do_set_pte(struct vm_area_struct *vma, unsigned long address,struct page *page, pte_t *pte, bool write, bool anon)
{pte_t entry;// 1. 刷新指令缓存flush_icache_page(vma, page);// 2. 构造PTE项entry = mk_pte(page, vma->vm_page_prot);if (write)entry = maybe_mkwrite(pte_mkdirty(entry), vma);// 3. 处理匿名页面if (anon) {inc_mm_counter_fast(vma->vm_mm, MM_ANONPAGES);page_add_new_anon_rmap(page, vma, address);} else {inc_mm_counter_fast(vma->vm_mm, MM_FILEPAGES);page_add_file_rmap(page);}// 4. 设置页表项set_pte_at(vma->vm_mm, address, pte, entry);// 5. 更新MMU缓存update_mmu_cache(vma, address, pte);
}
6.2 关键步骤说明
- PTE构造:根据页面和VMA权限构造页表项
- 反向映射:建立物理页面到虚拟地址的反向映射
- 统计更新:更新进程内存统计信息
- MMU同步:确保MMU缓存与页表一致
7. Slab分配器角色澄清
7.1 职责边界
-
Buddy分配器:
- 分配单位:页(4KB)
- 用途:用户匿名页、内核页面分配
- 特点:支持不同order,NUMA感知
-
Slab分配器:
- 分配单位:对象(小于页面)
- 用途:内核对象缓存、kmalloc
- 特点:对象重用,减少碎片
7.2 匿名页分配中的角色
- 用户匿名页:完全由Buddy分配器处理
- 页表页:通常也由Buddy分配器分配
- Slab不参与:用户匿名页的物理内存分配
8. 性能优化要点
8.1 内存分配优化
- NUMA策略:合理配置NUMA内存策略
- 迁移类型:使用__GFP_MOVABLE支持内存压缩
- 透明大页:评估THP对性能的影响
- 预分配:对于频繁分配的场景考虑预分配
8.2 页面故障优化
- 预取机制:利用fault-around机制
- 批量处理:减少页表锁竞争
- 零页面共享:充分利用零页面优化
9. 流程图解与架构图
9.1 malloc到物理映射完整流程图
┌─────────────────┐
│ 用户程序调用 │
│ malloc() │
└─────────┬───────┘│▼
┌─────────────────┐
│ 分配VMA结构 │
│ (虚拟地址空间) │
└─────────┬───────┘│▼
┌─────────────────┐
│ 用户程序首次 │
│ 访问虚拟地址 │
└─────────┬───────┘│▼
┌─────────────────┐
│ CPU/MMU检测 │
│ 页表项不存在 │
└─────────┬───────┘│▼
┌─────────────────┐
│ 产生页面故障 │
│ 异常 │
└─────────┬───────┘│▼
┌─────────────────┐
│ 硬件保存现场并 │
│ 跳转异常处理程序│
└─────────┬───────┘│▼
┌─────────────────┐
│ do_page_fault │
│ (架构相关) │
└─────────┬───────┘│▼
┌─────────────────┐
│ handle_mm_fault │
│ (通用处理) │
└─────────┬───────┘│▼
┌─────────────────┐
│__handle_mm_fault│
│ (页表层次处理) │
└─────────┬───────┘│▼
┌─────────────────┐
│handle_pte_fault │
│ (PTE层故障分类) │
└─────────┬───────┘│▼
┌─────────────────┐
│do_anonymous_page│
│ (匿名页处理) │
└─────────┬───────┘│▼
┌─────────────────┐
│alloc_zeroed_ │
│user_highpage_ │
│movable() │
└─────────┬───────┘│▼
┌─────────────────┐
│ Buddy分配器 │
│ 分配物理页面 │
└─────────┬───────┘│▼
┌─────────────────┐
│ clear_user_ │
│ highpage() │
│ (页面清零) │
└─────────┬───────┘│▼
┌─────────────────┐
│ mk_pte() │
│ (构造页表项) │
└─────────┬───────┘│▼
┌─────────────────┐
│ set_pte_at() │
│ (建立页表映射) │
└─────────┬───────┘│▼
┌─────────────────┐
│page_add_new_ │
│anon_rmap() │
│ (建立反向映射) │
└─────────┬───────┘│▼
┌─────────────────┐
│lru_cache_add_ │
│active_or_ │
│unevictable() │
└─────────┬───────┘│▼
┌─────────────────┐
│ 用户程序可以 │
│ 正常访问该地址 │
└─────────────────┘
9.2 Buddy分配器架构图
Buddy分配器架构┌─────────────────────────────────────────────────┐│ Zone管理 ││ ┌─────────────┐ ┌─────────────┐ ┌─────────────┐││ │ZONE_DMA │ │ZONE_NORMAL │ │ZONE_HIGHMEM │││ └─────────────┘ └─────────────┘ └─────────────┘│└─────────────────────────────────────────────────┘│▼┌─────────────────────────────────────────────────┐│ Free Area数组 ││ Order 0: [UNMOVABLE][MOVABLE][RECLAIMABLE] ││ Order 1: [UNMOVABLE][MOVABLE][RECLAIMABLE] ││ Order 2: [UNMOVABLE][MOVABLE][RECLAIMABLE] ││ ... ││ Order 10:[UNMOVABLE][MOVABLE][RECLAIMABLE] │└─────────────────────────────────────────────────┘│▼┌─────────────────────────────────────────────────┐│ 分配路径选择 ││ ││ 单页分配(order=0) 多页分配(order>0) ││ │ │ ││ ▼ ▼ ││ Per-CPU缓存 直接从伙伴系统 ││ ┌─────────────┐ ┌─────────────┐ ││ │CPU0 PCP │ │__rmqueue() │ ││ │CPU1 PCP │ │分割大块 │ ││ │... │ │合并小块 │ ││ └─────────────┘ └─────────────┘ │└─────────────────────────────────────────────────┘
9.3 页表映射建立过程图
虚拟地址到物理地址映射建立过程┌─────────────────┐ ┌─────────────────┐ ┌─────────────────┐
│ 虚拟地址 │ │ 页表层次 │ │ 物理页面 │
│ 0x12345000 │ │ │ │ │
└─────────┬───────┘ │ │ │ ││ │ │ │ │▼ │ │ │ │
┌─────────────────┐ │ ┌───────────┐ │ │ ┌─────────────┐ │
│ 页表遍历 │───▶│ │ PGD │ │ │ │ 物理页框 │ │
│ [47:39]→PGD │ │ └─────┬─────┘ │ │ │ 4KB │ │
│ [38:30]→PUD │ │ │ │ │ └─────────────┘ │
│ [29:21]→PMD │ │ ▼ │ │ │
│ [20:12]→PTE │ │ ┌───────────┐ │ │ ┌─────────────┐ │
│ [11:0] →偏移 │ │ │ PUD │ │ │ │页面内容清零 │ │
└─────────────────┘ │ └─────┬─────┘ │ │ │全部为0x00 │ ││ │ │ │ └─────────────┘ ││ ▼ │ │ ││ ┌───────────┐ │ └─────────────────┘│ │ PMD │ │ ▲│ └─────┬─────┘ │ ││ │ │ ││ ▼ │ ┌─────────────────┐│ ┌───────────┐ │ │ PTE项构造 ││ │ PTE │◀─┼────│ mk_pte() ││ │ Present=1│ │ │ 物理页框号+权限 ││ │ Write=1 │ │ │ Dirty=1 ││ │ User=1 │ │ │ Young=1 ││ └───────────┘ │ └─────────────────┘└─────────────────┘
9.4 内存分配器层次关系图
Linux内存分配器层次结构┌─────────────────────────────────────────────────────────┐│ 用户空间 ││ ┌─────────────┐ ┌─────────────┐ ┌─────────────┐ ││ │ mmap │ │ brk │ │ munmap │ ││ └─────────────┘ └─────────────┘ └─────────────┘ │└─────────────────────────────────────────────────────────┘│▼┌─────────────────────────────────────────────────────────┐│ VMA管理层 ││ ┌─────────────┐ ┌─────────────┐ ┌─────────────┐ ││ │ VMA分配 │ │ VMA查找 │ │ VMA合并 │ ││ └─────────────┘ └─────────────┘ └─────────────┘ │└─────────────────────────────────────────────────────────┘│▼┌─────────────────────────────────────────────────────────┐│ 页面故障处理 ││ ┌─────────────┐ ┌─────────────┐ ┌─────────────┐ ││ │do_page_fault│ │handle_mm_ │ │do_anonymous_│ ││ │ │ │fault │ │page │ ││ └─────────────┘ └─────────────┘ └─────────────┘ │└─────────────────────────────────────────────────────────┘│▼┌─────────────────────────────────────────────────────────┐│ 物理页面分配 ││ ││ ┌─────────────────────┐ ┌─────────────────────┐ ││ │ Buddy分配器 │ │ Slab分配器 │ ││ │ (页面级分配) │ │ (对象级分配) │ ││ │ │ │ │ ││ │ • 用户匿名页 │ │ • 内核对象 │ ││ │ • 页表页 │ │ • kmalloc缓存 │ ││ │ • 内核页面 │ │ • 文件系统缓存 │ ││ │ │ │ │ ││ └─────────────────────┘ └─────────────────────┘ │└─────────────────────────────────────────────────────────┘│▼┌─────────────────────────────────────────────────────────┐│ 物理内存 ││ ┌─────────────┐ ┌─────────────┐ ┌─────────────┐ ││ │ ZONE_DMA │ │ZONE_NORMAL │ │ZONE_HIGHMEM│ ││ └─────────────┘ └─────────────┘ └─────────────┘ │└─────────────────────────────────────────────────────────┘
9.5 TLB与页表交互示意图
TLB与页表交互机制┌─────────────────┐│ CPU访问 ││ 虚拟地址 ││ 0x12345678 │└─────────┬───────┘│▼┌─────────────────┐│ 查找TLB ││ (Translation ││ Lookaside ││ Buffer) │└─────────┬───────┘│┌────┴────┐│ │TLB命中 TLB未命中│ │▼ ▼┌─────────┐ ┌─────────────────┐│直接获得 │ │ 硬件页表遍历 ││物理地址 │ │ │└─────────┘ │ 1. 读取CR3获取 ││ PGD基地址 ││ 2. 索引PGD→PUD ││ 3. 索引PUD→PMD ││ 4. 索引PMD→PTE ││ 5. 获取物理页框号 │└─────────┬───────┘│▼┌─────────────────┐│ 更新TLB ││ 缓存新的映射关系│└─────────┬───────┘│▼┌─────────────────┐│ 访问物理内存 ││ 完成读写操作 │└─────────────────┘
10. 总结
malloc虚拟内存到物理映射的完整过程体现了Linux内存管理的精妙设计:
10.1 核心机制总结
-
按需分配机制:
- malloc只分配虚拟地址空间,不立即分配物理内存
- 物理内存在首次访问时通过页面故障机制按需分配
- 这种延迟分配策略大大节省了系统内存资源
-
分层处理架构:
- 硬件层:CPU/MMU检测页表项不存在,产生页面故障异常
- 架构层:do_page_fault处理架构相关的故障细节
- 通用层:handle_mm_fault和__handle_mm_fault处理通用逻辑
- 具体层:do_anonymous_page等函数处理特定类型的页面故障
-
Buddy分配器核心作用:
- 作为物理页面分配的唯一来源,负责所有用户匿名页的分配
- 通过Zone管理、迁移类型分类和Per-CPU缓存提供高效分配
- 支持NUMA感知分配,优化多处理器系统性能
-
优化机制集成:
- 零页面共享:只读访问使用全局零页面
- COW机制:写入时才分配真实物理页面
- TLB缓存:已映射页面通过TLB实现高速地址转换
- 反向映射:支持内存回收和页面迁移
10.2 关键设计决策
-
为什么选择按需分配:
- 内存节省:避免分配未使用的物理内存
- 启动加速:程序启动时无需等待物理内存分配
- 缓存友好:按需分配提高内存局部性
-
为什么使用MIGRATE_MOVABLE:
- 支持内存压缩和碎片整理
- 支持内存热插拔操作
- 有利于透明大页面分配
-
为什么Slab不参与用户页分配:
- Slab专注于内核小对象的高效管理
- 用户页面需要整页分配,不适合对象级管理
- 职责分离使系统架构更清晰
10.3 性能特点分析
优势:
- 内存利用率高:只分配实际使用的物理内存
- 启动速度快:程序启动时无需分配所有物理内存
- 缓存友好:按需分配提高缓存局部性
- 扩展性好:支持NUMA和大内存系统
开销:
- 首次访问延迟:页面故障处理需要时间
- TLB刷新成本:页表更新需要刷新TLB
- 内存碎片:可能产生外部碎片
10.4 实践意义
这种设计为现代操作系统内存管理提供了重要参考:
- 系统设计:分层架构和职责分离的重要性
- 性能优化:缓存机制和延迟分配的有效性
- 资源管理:按需分配和自动回收的平衡
- 扩展性:NUMA感知和多核优化的必要性
通过深入理解这一机制,我们可以更好地:
- 优化应用程序的内存使用模式
- 理解系统性能瓶颈的根源
- 设计更高效的内存管理策略
- 为特定场景选择合适的内存分配方案
这种“按需映射 + 伙伴分配 + 迁移压缩”的三级协同机制,使 Linux 在用户态 malloc 的每一次缺页都能以最小代价拿到物理页,同时保持全局内存的紧凑与可伸缩性,是当代操作系统在性能、功耗与资源利用率之间取得最佳权衡的典范实现。