从 malloc 到页表:一次用户态内存访问背后的全流程
📘 支持作者新书:《Yocto项目实战教程:高效定制嵌入式Linux系统》
本文以 32 位 ARMv7 架构为例,拆解一次最简单的
malloc(8)
动作在 Linux 内核下引发的虚拟内存分配与页表构建全过程。
1. 用户空间 malloc 背后的流程总览
以用户代码为例:
char *p = malloc(8);
*p = 'A';
这个过程涉及多个阶段:
- 用户调用
malloc()
,进入 glibc 分配器 - 若无可用缓存,则通过
brk()
或mmap()
系统调用请求内存 - 内核分配虚拟地址空间(创建 VMA)
- 用户首次访问
*p
触发缺页异常 - 内核页异常处理分配物理页并建立页表
2. 第一步:glibc 调用 brk 或 mmap
malloc(8)
触发ptmalloc2
,若已有堆缓存则直接分配,否则通过brk()
扩展堆;- 若分配较大(一般 >128KB),glibc 选择
mmap()
分配独立区域;
brk(0x0804a000); // 小内存扩展堆顶
3. 第二步:内核创建 VMA 区域
系统调用 sys_brk()
或 sys_mmap()
后,进入内核态:
- 找到
current->mm
(即进程的内存描述符) - 为虚拟地址区域创建
vm_area_struct
- 插入到 VMA 链表或红黑树中
// 进程虚拟内存结构
mm_struct
├── mmap → vm_area_struct → vm_area_struct → ...
├── pgd → 页目录
此时还 没有分配物理页帧,页表也尚未建立!
4. 第三步:首次访问触发 Page Fault
程序执行 *p = 'A'
时:
- CPU 发现虚拟地址未建立映射 → 触发异常中断
- 内核进入
do_page_fault()
进行缺页处理
流程如下:
do_page_fault()
→ find_vma() // 查找是否是合法地址
→ handle_mm_fault()
→ do_anonymous_page()
→ alloc_page()
5. 第四步:内核分配页帧,建立页表
do_anonymous_page()
中:
- 分配物理页帧(来自伙伴系统)
- 使用
set_pte()
更新页表项,建立虚拟 → 物理映射 - 同时更新 TLB
页表层次回顾(ARM32)
ARMv7 使用二级页表:
- PGD(页目录):4096 项,每项映射 1MB;共 4KB
- PTE(页表项):每项映射 4KB;按需分配
// 示例地址 0x0804a000(用户态地址)
pgd_index = 0x0804a000 >> 20; // PGD index
pte_index = (0x0804a000 >> 12) & 0xFF; // PTE index
内核会为缺失页表页分配内存,并建立对应页表项。
6. 最终效果:完成映射
至此:
- 0x0804a000 映射到物理页帧(如 0x1f305000)
- 对
*p = 'A'
的访问命中 TLB,内存访问成功
用户空间:0x0804a000 → 页表 → 页帧 0x1f305000 → DRAM
此后访问同一页均不再触发异常。
7. 总结流程图
malloc()
→ brk()/mmap()
→ 内核创建 VMA
→ 首次访问触发 Page Fault
→ 分配物理页 → 更新页表 → 恢复执行
8. 小结与内核视角
步骤 | 内核结构/函数 | 说明 |
---|---|---|
系统调用 | sys_brk / sys_mmap | 分配虚拟地址 |
虚拟地址注册 | insert_vm_struct | 添加到 mm_struct 中 |
缺页处理 | do_page_fault() | 异常处理入口 |
分配页帧 | alloc_page() | 来自伙伴系统 |
映射页表 | set_pte() | 更新页表结构 |
📘 推荐阅读:
- 第一篇:《Arm32 Memory Model:内核空间与用户空间的真相解析》
- 支持作者新书:《Yocto项目实战教程》
👉 京东购买链接:https://item.jd.com/15020438.html
📌 作者:嵌入式 Jerry | 视频教程请关注 B 站:“嵌入式 Jerry”