程序内存分布是怎么映射到操作系统内存的?

程序看到的 text / data / bss / heap / stack / mmap 只是虚拟地址空间里的标签。真正坐在 DRAM 上的是物理页框。本页用图把“程序内存分布 → 页表 → 物理内存”这条映射链路拆开。

① 程序看到的内存分布:一个进程的虚拟地址空间

每个进程都以为自己独占一大片连续内存(实际是 OS 给的 illusion)。从低地址到高地址,编译器 + 运行时 + OS 把这片空间划分成若干功能区:

进程 A 虚拟地址空间 0x0000_0000_0000 代码段 (Text) 只读 | 可执行 0x400000 数据段 (Data) 已初始化全局变量 0x600000 BSS 段 未初始化变量 (zero) 堆 (Heap) ↑ 动态分配 (malloc/new) brk 指针向上增长 brk: 0x1000000 内存映射区 (mmap) 文件映射 / 匿名映射 0x7f00_0000_0000 栈 (Stack) ↓ 局部变量 / 函数调用 向下增长 rsp: 0x7fff_ffff_ffff 内核空间(高地址)

典型 64 位 Linux 进程虚拟地址空间示意(地址与比例仅作参考,ASLR 会打乱具体数值)

② 从“程序分布”到“物理内存”:核心映射链路

程序里的地址全是虚拟地址。CPU 每次访存都要经过 MMU(内存管理单元) 查页表,把虚拟地址翻译成物理地址,才能访问 DRAM。

进程 A 虚拟地址空间 text / data / bss heap(向上生长)0x1000000 mmap 区0x7f00.. stack(向下生长)0x7fff.. 页表 (Page Table) VPN → PFN PTE: 0x00401 → 0x3A PTE: 0x01000 → 0x7F PTE: 0x7FFFF → 0x12 PTE: 0x7FFFE → 0x55 多级页表 / PTE存在位 P | 权限位 R/W/X MMU TLB 缓存 物理内存 (DRAM) 帧 0x3A 帧 0x7F 帧 0x12 帧 0x55 空闲帧 空闲帧 CPU

虚拟地址 → 页表 → MMU → 物理页框。页表由 OS 在进程创建/装载时建立,CPU 的 MMU 在运行时翻译。

关键结论:程序里的 0x400000、0x1000000、0x7fff... 全是虚拟地址。它们通过页表映射到物理内存中离散的页框。同一个虚拟地址在不同进程里,可以映射到完全不同的物理帧;同一物理帧也可以被多个进程共享(如共享库、共享内存)。

③ 不同段落到 OS 内存的方式并不一样

虽然都叫“映射到物理内存”,但 text/data/bss/heap/stack/mmap 的来源、权限、是否共享、是否写时复制都有差别。下面这张表把细节拆开:

程序段映射来源物理页如何获得权限是否共享 / COW
textELF 文件 .text文件映射到物理页,多进程共享同一磁盘页缓存r-x共享只读,省内存
dataELF 文件 .data文件映射,但标记为可写;第一次写时触发 COW,复制出私有页rw-写时复制 (COW)
bssELF 只记录大小匿名映射,OS 分配全零页(可能映射到零页,写时复制)rw-常与零页共享,写则 COW
heap运行时 malloc匿名映射或 brk 扩展;按需分配/延迟分配rw-进程私有
stackOS 建立初始栈匿名映射,初始只提交少量页,向下缺页时按需增长rw-进程私有
mmap文件 / 匿名文件-backed 映射到页缓存;匿名映射到交换区/物理页按 mmap 参数文件映射可共享;匿名通常私有

COW = Copy-On-Write,写时复制。OS 利用它让多个进程先共享同一份只读物理页,谁写谁复制,既省内存又保证隔离。

④ 支撑映射的四个核心机制

1) 页表与多级页表

虚拟地址被切成“页内偏移 + 若干级页索引”。x86-64 通常用 4 级或 5 级页表,每级查一个索引,最终找到 PTE(页表项),里面存着物理页框号 + 权限位。

为什么需要多级? 如果单级页表覆盖整个 64 位地址空间,页表本身就要占用天文数字的内存。多级页表把“没有映射的区域”留空,按需展开,实际只占用少量页表页。

2) MMU 与 TLB

MMU 是 CPU 内部的硬件单元,负责每次访存时查页表做地址翻译。TLB 是 MMU 里的高速缓存,缓存最近用过的“虚拟页 → 物理帧”映射。TLB 命中时翻译几乎零开销;不命中才去内存里逐级查页表。

3) 缺页异常 (Page Fault)

程序访问了一个合法但尚未映射的虚拟地址(或访问了权限不足的页),CPU 会触发缺页异常,进入内核:

4) 交换 (Swap) 与按需分配

物理内存不够时,OS 可以把不活跃的物理页换出到磁盘交换区,腾出帧给其他进程。所以虚拟地址空间可以比物理内存大——不是真的同时装下所有内容,而是按需加载 + 换入换出。

⑤ 一次 malloc(1024) 的完整映射旅程

用一个小例子把“程序分布 → OS 内存映射”串起来。假设进程执行 char *p = malloc(1024); *p = 42;:

1
用户态:malloc 在堆区找空闲块
libc 的 malloc 先在自己的堆里翻一翻,看有没有合适的空闲 chunk。如果有,直接返回一个虚拟地址,完全不惊动 OS。
2
不够了:malloc 向 OS 要虚拟地址空间
如果堆顶空间不够,malloc 调用 brk() 抬高堆顶,或 mmap() 在 mmap 区拿一块。此时 OS 只是在页表里标记这片虚拟地址可用,但可能还没有分配物理页。
3
首次写入触发缺页异常
当执行 *p = 42 时,CPU 发现该虚拟页没有映射到物理帧,触发 Page Fault,陷入内核。
4
OS 分配物理页并建立映射
内核从伙伴系统拿一个空闲物理页框,清零(匿名页默认),在页表里填一条 PTE,设置权限为 rw-,然后恢复进程执行。
5
MMU 完成写入
恢复后 CPU 重新执行 *p = 42,这次 MMU 查到 PTE,把虚拟地址翻译成物理地址,写入 DRAM。
体会一下:malloc 返回的地址很早就有,但真正占用物理内存发生在第一次写入时。这是 Linux 的“延迟分配”策略——先答应你,等你真用再给钱。

⑥ 常见误区澄清

⑦ 一句话总结

程序内存分布是虚拟地址空间里按用途贴的标签(text/data/bss/heap/stack/mmap);操作系统内存是真实物理页框 + 页表 + 交换区。两者通过页表 + MMU + 缺页异常联系起来:OS 在装载/运行时建立映射,CPU 在每次访存时翻译地址,按需分配、写时复制、换入换出让这套映射既隔离又高效。