Arm32 Memory Model:内核空间与用户空间的真相解析
📘 支持作者新书:《Yocto项目实战教程:高效定制嵌入式Linux系统》
👉 京东购买链接:https://item.jd.com/15020438.html
从
malloc()
一次 8 字节的小分配,到 CPU 如何找到对应物理页帧,本篇带你用 ARM32 架构梳理一遍完整流程与背后机制,并回答一个长期困惑嵌入式工程师的问题:为什么每个进程要有自己的页表?为什么内核页表却能共享?
第一部分:基本概念清晰化
我们首先厘清四个常被混淆的概念:
名称 | 本质 | 所属 | 常见场景 |
---|---|---|---|
用户态(User Mode) | CPU 特权等级(PL0) | CPU 运行态 | 执行用户程序代码 |
内核态(Kernel Mode) | CPU 特权等级(PL1) | CPU 运行态 | 执行系统调用、中断服务 |
用户空间(User Space) | 虚拟地址范围(如 0x0000_0000 ~ 0xBFFF_FFFF) | 进程虚拟地址空间 | malloc、栈、共享库等 |
内核空间(Kernel Space) | 虚拟地址范围(如 0xC000_0000 ~ 0xFFFF_FFFF) | 全局虚拟地址空间 | 内核代码、内核数据结构、内核模块 |
✔ 注意:用户态/内核态 = CPU 运行权限;用户空间/内核空间 = 虚拟内存区域。
第二部分:虚拟地址空间划分(ARM32)
在经典的 ARMv7 架构(32-bit)中,Linux 默认使用 3GB/1GB 分割:
┌────────────────────────────┐ 0xFFFF_FFFF
│ 内核空间(1GB) │
│ 内核映射、内核代码段等 │
├────────────────────────────┤ 0xC000_0000 = PAGE_OFFSET
│ 用户空间(3GB) │
│ 用户程序代码/数据/堆栈 │
└────────────────────────────┘ 0x0000_0000
- 用户空间每个进程独立:只有自己能访问其 0x00000000 - 0xBFFFFFFF 的内容。
- 内核空间由所有进程共享映射,内容一致(只在内核态访问)。
第三部分:为什么需要两套页表?
页表是虚拟地址 → 物理地址 的映射结构。ARMv7 Linux 使用二级页表(PGD → PTE)。
❶ 用户空间的页表(每进程唯一)
- 用于映射用户态下的虚拟地址。
- 每个进程在创建时
copy_mm()
或dup_mm()
时创建独立页表。 - 有利于隔离:每个进程不能访问别的进程空间。
❷ 内核空间的页表(所有进程共享)
- 映射高地址(如 0xC000_0000 起),用于访问内核代码、数据结构。
- 创建于内核启动早期(paging_init()),写入所有进程页表高位部分。
✅ Linux 的“共享 + 隔离”策略是节省内存与保护隔离的折中产物。
第四部分:页表结构与寄存器关系
ARM 页表结构(以 1MB section 映射为例)
- PGD(页全局目录)为一级表,1KB 大小,共有 4096 项,每项映射 1MB,共 4GB 空间。
// ARM Linux 页表结构(经典情况)
pgd_t *pgd; // 每个进程独立分配
pgd[0] ~ pgd[3071] // 映射用户空间(0~3GB)
pgd[3072] ~ pgd[4095] // 映射内核空间(3GB~4GB)→ 所有进程共享部分
TTBR0 / TTBR1(页表基地址寄存器)
- ARM 使用
TTBR0
保存用户空间页表基地址。 TTBR1
通常被 Linux 保留为内核映射。- 上下文切换时,Linux 内核只需要更新 TTBR0 即可实现进程切换。
write_ttbr0(pgd_base); // 切换页表,进程调度时执行
第五部分:谁初始化、谁负责
时间点 | 行为 | 负责函数 |
---|---|---|
内核早期启动 | 建立内核页表、固定映射 | paging_init() / map_kernel() |
第一个进程 | 创建独立用户空间页表 | mm_init() / copy_mm() |
fork 新进程 | 拷贝父页表或写时复制 | dup_mm() / copy_page_range() |
进程切换 | 替换页表基地址 | context_switch() → switch_mm() → cpu_switch_mm() |
第六部分:页表占用的空间与优化
-
每个进程页表占用(粗略估算):
- 一级页表(PGD)大小:4KB
- 二级页表(PTE)按页分配,按使用情况动态增长
- 共享部分(内核空间)不用重复分配
-
优化:Linux 使用
page_table_cache
回收页表页
小结:这一切是为了什么?
目的 | 措施 |
---|---|
内存隔离 | 每进程独立用户页表 + 用户态访问限制 |
资源节省 | 内核页表共享 + 按需创建页表项(延迟分配) |
安全性保障 | CPU 特权等级控制 + 页表权限保护 |
下一篇预告
将在《第二篇:从 malloc 到页表:一次用户态内存访问背后的全流程》中,完整展示:
- malloc(8) 的 glibc 分配过程
- mmap/brk 系统调用建立 VMA
- 首次访问触发 Page Fault
- do_page_fault → handle_mm_fault → do_anonymous_page
- 最终到 alloc_page() → 页帧分配
敬请期待!
📘 支持作者新书:《Yocto项目实战教程:高效定制嵌入式Linux系统》
👉 京东购买链接:https://item.jd.com/15020438.html