用户空间与内核空间:小内存访问背后的完整真相


支持作者新书,点击京东购买《Yocto项目实战教程:高效定制嵌入式Linux系统》


B站配套学习视频


用户空间与内核空间:小内存访问背后的完整真相


在这里插入图片描述

❓几个常见但容易误解的问题

在日常开发中,我们经常写下这样朴素的一行代码:

int *p = malloc(8);
*p = 42;

这一行简单的内存分配与写操作,在用户空间是再普通不过的行为。但你是否真正理解了背后发生了什么?

  • 这 8 字节来自哪里?物理内存是怎么分配的?
  • 是什么时候分配的?访问它需要系统调用吗?
  • glibc malloc() 能直接操作内存吗?
  • 为什么我们说“只有 Page Fault 才会进入内核”?
  • 内核空间的 kmalloc(8)malloc(8) 一样吗?
  • MMU/TLB/页表在这个过程中到底扮演了什么角色?

本文将带你全面梳理用户空间与内核空间中小内存访问的完整流程,结合代码路径、硬件机制、内核细节,为你打通表象与本质的屏障。


🧭 目录导航

  1. 用户空间内存访问流程(以 malloc(8) 为例)
  2. 内核空间小内存分配(以 kmalloc(8) 为例)
  3. glibc 分配器策略解析
  4. Page Fault、系统调用、页表建立全过程
  5. MMU 与 TLB 的硬件行为详解
  6. 对比总结与流程图展示

1️⃣ 用户空间:malloc(8) 到访问的完整路径

我们以最常见的场景为例:

int *p = malloc(8);
*p = 42;

你以为它只是用户空间的简单调用,实际背后流程如下:


✅ 分配阶段

  1. 调用 malloc(8)glibc 实现的分配器(默认是 ptmalloc2)处理
  2. 分配器会尝试从现有小块内存池中找一块合适区域
  3. 如果没有,就通过 brk()mmap() 向内核申请更多虚拟地址空间
// 示例调用栈(glibc)
__libc_malloc()
├── _int_malloc()
│   ├── sysmalloc()
│   │   ├── __mmap()
│   │   └── __sbrk()

✅ 虚拟地址空间建立

glibc 使用 syscall(SYS_brk)syscall(SYS_mmap) 进入内核:

  • 调用路径:
sys_brk()do_brk()
sys_mmap()do_mmap()
  • 内核创建对应 struct vm_area_struct(VMA)
  • 注意:此时页表尚未建立,物理页帧尚未分配!

✅ 首次访问:触发缺页异常

当你执行 *p = 42; 时:

  1. CPU 执行 store 指令
  2. MMU 查找该虚拟地址的页表项
  3. 如果页表不存在 → Page Fault 异常(缺页中断)
  4. 内核陷入 → do_page_fault()handle_mm_fault()__handle_mm_fault()do_anonymous_page()
  5. 分配物理页帧(通过 alloc_page() → buddy 分配器),更新页表
  6. 返回用户空间 → 重新执行 store

✅ 后续访问

  • 页表已经存在
  • TLB 命中 → 虚拟地址直接转为物理地址 → 不再陷入内核

2️⃣ 内核空间:kmalloc(8) 背后的流程

void *p = kmalloc(8, GFP_KERNEL);

调用路径:

kmalloc()kmalloc_slab()slab_alloc()new_slab_objects() if needed
        → alloc_pages()__alloc_pages() → buddy system
  • 内核使用 slab 分配器管理小对象(如 task_struct)
  • 内部按对象大小(如 kmalloc-8、kmalloc-32)维护缓存池
  • 每类对象使用多个页帧作为缓存池
  • slab 拆页 → 构建对象链表 → 分配对象
  • 使用的是内核虚拟地址(直接映射区)

3️⃣ glibc 分配器策略解析(用户空间)

glibc 的 malloc() 实现并不直接与物理内存交互,它依赖内核提供的虚拟内存机制:

  • 小块内存(<128KB):使用 brk() 扩展堆顶
  • 大块内存(≥128KB):使用 mmap() 申请匿名页
  • 内部维护多个 free list,支持 bin 机制、小块复用、延迟释放等

✅ glibc 分配器不关心页表/物理内存/页帧,它只管理用户空间的分配策略。


4️⃣ Page Fault 与页表建立全过程

当访问未映射地址时:

  1. CPU 捕获异常 → trap handler → do_page_fault()

  2. 根据地址所在 VMA,确认可访问性

  3. 如果是匿名页:

    • 创建 PTE
    • 调用 alloc_page() 分配物理页帧
    • 设置 PTE 权限位(RW/U)
    • 刷新 TLB(如 flush_tlb_page()
  4. 用户态恢复 → 重新执行失败的指令

页表结构示意(以 x86_64 为例):

PGD → P4D → PUD → PMD → PTE → PFN

每一级页表的索引宽度:9 bits,PTE 最终映射 4KB 页帧。


5️⃣ MMU 与 TLB 的硬件行为

🔹 MMU(Memory Management Unit)

  • 位于 CPU 内部,接管所有虚拟地址访问
  • 自动根据页表翻译虚拟地址为物理地址
  • 不执行任何代码,只是“硬件查表 + 组合地址”

🔹 TLB(Translation Lookaside Buffer)

  • 页表缓存,提升地址翻译性能
  • CPU 自动维护(替换/失效/刷新)
  • 如果 TLB 命中,虚拟地址翻译只需 1 cycle
  • TLB miss 时,硬件自动翻页表 → Page Walker

🔹 是否需要进入内核?

场景是否内核代码?是否系统调用?
TLB 命中❌ 否❌ 否
页表命中(TLB miss)❌ 否❌ 否
页表缺失(Page Fault)✅ 是✅ 是(陷入)

6️⃣ 图示总结:用户空间 malloc 与访问的完整路径

┌──────────────┐
│ malloc(8)    │
└──────┬───────┘
       ↓
┌──────────────────────┐
│ glibc: 找空闲小块或扩展 │
└──────┬────────────────┘
       ↓
┌────────────────────────────────────────┐
│ syscall: brk() 或 mmap()               │
│  → 内核建立 VMA(尚无页表/PTE)       │
└──────┬─────────────────────────────────┘
       ↓
┌────────────────────────────┐
│ 首次访问 p → MMU 查 TLB    │
└──────┬────────────┬────────┘
       │            │
       ↓            ↓
   TLB miss      TLB hit
       │            │
       ↓            ↓
  查页表(无)   完成访问(无需内核)
       ↓
Page Fault → handle_mm_fault() → 建页表 + 分配页帧
       ↓
  返回用户态 → 重新执行 → 访问成功

✅ 最终总结:核心差异对比

项目用户空间 malloc(8)内核空间 kmalloc(8)
使用 APImalloc() (glibc)kmalloc() (内核)
是否进入内核?首次访问时可能会 Page Faultkmalloc 直接在内核
分配策略glibc 分配器 + brk/mmapslab 分配器 + buddy
是否直接访问页表?❌ 否(硬件 MMU 自动查)✅ 内核直接操作页表
物理页帧由谁分配handle_mm_fault()alloc_page()slab 向 alloc_pages() 请求页
是否使用 TLB✅ 是✅ 是

📌 建议延伸阅读



支持作者新书,点击京东购买《Yocto项目实战教程:高效定制嵌入式Linux系统》


B站配套学习视频

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值