K06 —— mmap 申请内存的全流程:demand paging 和物理页分配

金句:你申请 1GB 内存,系统不会真的先给你 1GB——而是在你真正使用时才分配。


mmap 的延迟分配(Lazy Allocation)

mmap 只创建虚拟地址映射,不立即分配物理页。物理页在实际访问时按需分配(Demand Paging)。

Bash
mmap 的工作方式:

mmap(NULL, 1GB, PROT_READ|PROT_WRITE, MAP_PRIVATE|MAP_ANONYMOUS, -1, 0);
  → 创建 VMA:vm_start ~ vm_end = 1GB
  → VMA 标记为匿名私有映射
  → 不分配任何物理页
  → 返回起始地址(程序以为有了 1GB)

第一次访问(假设读 0x1000 字节):
  → 访问的页面不在内存(Page Fault)
  → 内核分配物理页
  → 填充全零
  → 建立页表映射
  → 继续执行

结果:
  - 如果程序只访问 4KB,实际只用了 4KB 物理页
  - 如果程序访问 1GB,全部 1GB 物理页才会被分配

Page Fault 的处理流程

Bash
当访问 mmap 区域时发生 Page Fault:

do_page_fault() → __do_page_fault():

Step 1:查找 VMA
  vma = find_vma(mm, addr);
  if (!vma || addr < vma->vm_start)
      → 非法地址,发送 SIGSEGV

Step 2:检查访问权限
  if (vma->vm_flags & VM_WRITE && !pte_writable)
      → COW 或写保护错误

Step 3:分配物理页(handle_pte_fault)

  if (!pte_present(pte)) {
      // 页不在内存,按类型分配

      if (vma->vm_file) {
          // 文件映射
          do_file_fault();
          // 从磁盘读取文件内容到页面
      } else {
          // 匿名映射(MAP_ANON)
          do_anonymous_page();
          // 分配全零页面
      }
  }

Bash
do_anonymous_page() 的详细流程:

1. 从 buddy system 分配物理页
   page = alloc_page(GFP_KERNEL | __GFP_ZERO);
   // __GFP_ZERO:页面清零

2. 设置 PTE
   set_pte(ptel, mk_pte(page, vma->vm_page_prot));
   // 设置读写权限

3. 更新 LRU
   lru_cache_add(page);
   // 把页面加入 LRU(可被回收)

4. 刷新 TLB
   flush_tlb_page(vma, addr);

5. 返回,继续执行指令

MAP_ANONYMOUS vs MAP_FILE

Bash
匿名映射(MAP_ANON)的处理:

mmap(NULL, size, PROT, MAP_ANONYMOUS|MAP_PRIVATE, -1, 0);

Page Fault:
  → do_anonymous_page()
  → 分配全零页面
  → PTE 指向该页面

特点:
  - 内容是全零(不来自文件)
  - 页面可以独立存在(不在 page cache 中)
  - fork 时走 COW(共享但只读)

---

文件映射(MAP_FILE)的处理:

mmap(NULL, size, PROT, MAP_PRIVATE, fd, 0);

Page Fault:
  → do_file_fault()
  → 从 inode 读取文件内容
  → 分配页面,从磁盘读取数据
  → PTE 指向该页面(file-backed)

特点:
  - 内容来自磁盘文件
  - 页面在 inode 的 address_space 中(radix tree)
  - 写时 COW(私有映射)

/proc/meminfo 中的 Page Cache 统计

Bash
$ cat /proc/meminfo

MemTotal:       32768176 kB     // 总物理内存
MemFree:        12345678 kB     // 完全空闲
MemAvailable:   28901234 kB     // 空闲 + 可回收(可用的)
Buffers:           123456 kB     // 块设备缓冲(Page Cache)
Cached:           5678901 kB    // Page Cache + SwapCache
SwapCached:        234567 kB     // 已读入内存的 swap 页(不需要再读)

具体含义:
  Buffers:原始块设备的缓存(metadata)
  Cached:文件系统 Page Cache(文件数据)
  SwapCached:已换入内存的 swap 页面(在 LRU 上,不需要重新读 swap)

Active(page)/Inactive(page):File-backed 页
Active(anon)/Inactive(anon):Anonymous 页

$ cat /proc/meminfo | grep -E "^(Anon|File|Cached|Swap):"
AnonPages:       12345678 kB     // 匿名页(堆/栈)
Mapped:           1234567 kB     // 文件映射的页面(mmap 的文件)
Shmem:            234567 kB      // 共享内存(tmpfs)

总结

  • mmap 延迟分配:mmap 只创建 VMA,不分配物理页,物理页在 page fault 时按需分配
  • do_page_fault:查找 VMA → 检查权限 → 按类型分配(anonymous 或 file)
  • do_anonymous_page:从 buddy 分配全零页 → 设置 PTE → 加入 LRU → flush TLB
  • MAP_ANON:全零页面,不需要磁盘 I/O
  • MAP_FILE:从磁盘读取文件内容到页面
  • /proc/meminfo:Active/Inactive 分 anonymous 和 file-backed,分别统计

下篇预告(K07):当用户调用 malloc 时,内核是怎么参与分配的?brk、mmap、以及 glibc 的 ptmalloc 分配器——为什么频繁 malloc/free 会变慢。


关注公众号「AI不着急」,回复”资料”获取内存学习路线图。

最后修改: 2024年7月7日

作者

评论

发表评论

您的邮箱地址不会被公开。