① 程序看到的内存分布:一个进程的虚拟地址空间
每个进程都以为自己独占一大片连续内存(实际是 OS 给的 illusion)。从低地址到高地址,编译器 + 运行时 + OS 把这片空间划分成若干功能区:
典型 64 位 Linux 进程虚拟地址空间示意(地址与比例仅作参考,ASLR 会打乱具体数值)
- text:机器指令,来自 ELF 的 .text,权限 r-x。
- data:已初始化全局/静态变量,来自 ELF 的 .data,权限 rw-。
- bss:未初始化全局/静态变量,ELF 里只记大小不存内容,装载时 OS 清零并映射匿名页,权限 rw-。
- heap:动态分配区,由 malloc 管理,不够时通过 brk() 或 mmap() 向 OS 申请更多虚拟地址。
- mmap:文件映射、共享库、大内存分配、线程栈等。
- stack:函数调用链、局部变量、返回地址,向下生长,大小受 rlimit 限制。
② 从“程序分布”到“物理内存”:核心映射链路
程序里的地址全是虚拟地址。CPU 每次访存都要经过 MMU(内存管理单元) 查页表,把虚拟地址翻译成物理地址,才能访问 DRAM。
虚拟地址 → 页表 → MMU → 物理页框。页表由 OS 在进程创建/装载时建立,CPU 的 MMU 在运行时翻译。
关键结论:程序里的 0x400000、0x1000000、0x7fff... 全是虚拟地址。它们通过页表映射到物理内存中离散的页框。同一个虚拟地址在不同进程里,可以映射到完全不同的物理帧;同一物理帧也可以被多个进程共享(如共享库、共享内存)。
③ 不同段落到 OS 内存的方式并不一样
虽然都叫“映射到物理内存”,但 text/data/bss/heap/stack/mmap 的来源、权限、是否共享、是否写时复制都有差别。下面这张表把细节拆开:
| 程序段 | 映射来源 | 物理页如何获得 | 权限 | 是否共享 / COW |
| text | ELF 文件 .text | 文件映射到物理页,多进程共享同一磁盘页缓存 | r-x | 共享只读,省内存 |
| data | ELF 文件 .data | 文件映射,但标记为可写;第一次写时触发 COW,复制出私有页 | rw- | 写时复制 (COW) |
| bss | ELF 只记录大小 | 匿名映射,OS 分配全零页(可能映射到零页,写时复制) | rw- | 常与零页共享,写则 COW |
| heap | 运行时 malloc | 匿名映射或 brk 扩展;按需分配/延迟分配 | rw- | 进程私有 |
| stack | OS 建立初始栈 | 匿名映射,初始只提交少量页,向下缺页时按需增长 | rw- | 进程私有 |
| mmap | 文件 / 匿名 | 文件-backed 映射到页缓存;匿名映射到交换区/物理页 | 按 mmap 参数 | 文件映射可共享;匿名通常私有 |
④ 支撑映射的四个核心机制
1) 页表与多级页表
虚拟地址被切成“页内偏移 + 若干级页索引”。x86-64 通常用 4 级或 5 级页表,每级查一个索引,最终找到 PTE(页表项),里面存着物理页框号 + 权限位。
为什么需要多级? 如果单级页表覆盖整个 64 位地址空间,页表本身就要占用天文数字的内存。多级页表把“没有映射的区域”留空,按需展开,实际只占用少量页表页。
2) MMU 与 TLB
MMU 是 CPU 内部的硬件单元,负责每次访存时查页表做地址翻译。TLB 是 MMU 里的高速缓存,缓存最近用过的“虚拟页 → 物理帧”映射。TLB 命中时翻译几乎零开销;不命中才去内存里逐级查页表。
3) 缺页异常 (Page Fault)
程序访问了一个合法但尚未映射的虚拟地址(或访问了权限不足的页),CPU 会触发缺页异常,进入内核:
- 按需分页:如果是首次访问堆/栈的匿名页,OS 分配一个物理页、清零、填页表、恢复执行。
- 请求调页:如果是文件映射区首次访问,OS 从磁盘读入对应页到页缓存,再映射。
- 非法访问:如果地址根本不在页表范围内,OS 发送 SIGSEGV(段错误),进程被杀。
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 的“延迟分配”策略——先答应你,等你真用再给钱。
⑥ 常见误区澄清
- 误区 1:“虚拟地址就是物理地址,只是换了个名字。”
错。虚拟地址通过页表映射到物理帧,多个虚拟页可映射同一帧(共享),也可映射到磁盘(换出),还可不映射(未分配)。
- 误区 2:“堆和栈是 OS 预先分配好的两块内存。”
错。OS 只给起点和扩展规则;堆内部由 malloc 切分,栈由编译器生成的指令压栈/弹栈使用。物理页都是按需分配的。
- 误区 3:“程序有多大,物理内存就占多大。”
错。text 段多进程共享;data/bss 写时才复制;堆/栈按需分页;mmap 文件区直接映射磁盘缓存。实际 RSS 往往远小于虚拟地址空间大小。
- 误区 4:“所有段都映射到物理内存。”
错。很多虚拟页只是“占坑”,并没有物理 backing,访问时才触发缺页分配。
⑦ 一句话总结
程序内存分布是虚拟地址空间里按用途贴的标签(text/data/bss/heap/stack/mmap);操作系统内存是真实物理页框 + 页表 + 交换区。两者通过页表 + MMU + 缺页异常联系起来:OS 在装载/运行时建立映射,CPU 在每次访存时翻译地址,按需分配、写时复制、换入换出让这套映射既隔离又高效。