程序里的地址是真实内存地址吗?从虚拟内存到地址转换

📅 发布时间:2026/10/14 3:06:02
程序里的地址是真实内存地址吗?从虚拟内存到地址转换
写一行printf(%p, x)打出来一个像0x7ffd2c4a3b4c的值。它看着像个内存地址但它不是内存条上那个物理地址。程序里所有的指针、所有mov指令里带的地址都是虚拟地址(也叫逻辑地址)。CPU 把虚拟地址发出来之后并不是直接拿去访问内存中间还要过一道翻译翻成物理地址才真的去读写。这道翻译就是虚拟内存这篇文章讲它为什么存在、怎么翻。为什么要虚拟内存先想一下如果不用虚拟内存会怎样也就是程序里直接写物理地址两个程序想同时跑得提前商量好各自占哪块物理内存谁也别碰谁的。程序一多这种排座位根本排不过来。程序编译的时候就得知道自己会被加载到哪个物理地址。可物理内存上哪里有空洞是运行的时候才知道的。用一个共享库每个进程都得在自己的物理内存里放一份拷贝浪费。虚拟内存把程序看到的地址和真实的物理地址解绑换来四样东西好处怎么来的隔离每个进程一张自己的页表看到的是自己独立的地址空间不用关心物理布局连续的一段虚拟地址可以映射到任意、离散的物理页地址空间能超过物理内存用不到的页放磁盘(swap)要用了再换进来共享同一块物理页可以同时映射进多个进程(共享库、fork的 COW)地址怎么翻译翻译的粒度是页不是每个字节。常见的页大小是 4KB也就是 2^12 字节。于是一个虚拟地址被切成两段虚拟地址 [ 虚拟页号 VPN ] [ 页内偏移 offset ] 高位 低 12 位翻译只改前面那段高位把虚拟页号换成物理页号(也叫物理页帧号 PFN)低 12 位的偏移原样搬过去。这样拼出来的就是物理地址。物理地址 [ 物理页帧号 PFN ] [ 页内偏移 offset ] ← 偏移和上面完全一样干这件事的硬件叫MMU(Memory Management Unit)它在 CPU 里每次访存都会走一遍。维护虚拟页号 → 物理页号这张对照表的是页表(Page Table)由内核负责填。页表长什么样页表本质就是一个数组用虚拟页号当下标取出来的表项叫PTE(Page Table Entry)里面存物理页帧号再加上几个标志位位名字管什么Present / Valid有效位这个页现在在不在物理内存里。在1不在(被换出去了)0Read/Write读写位这个页允许写吗不允许写就是只读页(比如代码段)User/Supervisor权限位用户态能不能访问内核页这里是 0Accessed访问位有没有被读过给页面置换算法用Dirty脏位有没有被写过决定换出时要不要写回磁盘NX执行位这个页能不能当代码执行防缓冲区溢出有效位和脏位这两个是缺页和页面置换的基础。单级页表为什么不够先算一笔账。48 位虚拟地址、4KB 页大小虚拟页的数量是2^48 / 2^12 2^36 个页每个 PTE 8 字节如果真用一个平铺的数组来存一个进程就是2^36 × 8 字节 512 GB光页表就要 512GB显然不现实。而且绝大部分虚拟地址根本不会被用到平铺等于给一片空地全铺上瓦。解法是多级页表本质是只给用到的部分分配。x86-64 用四级虚拟地址(x86-64实际只用了低 48 位) 47 39 38 30 29 21 20 12 11 0 --------------------------------------------------- | PML4 | PDPT | PD | PT | 页内偏移 | | 9 位 | 9 位 | 9 位 | 9 位 | 12 位 | ---------------------------------------------------翻译时一层层往下走内核寄存器CR3存着第一级表(PML4)的物理地址用最高的 9 位做下标取一项那一项指向第二级表再用接下来 9 位下标如此往下走四级最后一级拿出来的是物理页帧号拼上 12 位偏移。好处在于如果某个 9 位区段整个没用到上一层对应的表项直接置空就行下级的表根本不用分配。一个进程真正用到的虚拟地址是一小片多级页表就只为这片地址铺路。TLB多级页表解决了空间却把时间搞坏了四级就是四次访存只为了算出物理地址然后再访存一次拿数据——一次读内存变成五次。所以 MMU 里加了个缓存专存最近用过的虚拟页号 → 物理页帧号映射叫TLB(Translation Lookaside Buffer)。TLB 命中就一两个周期直接出结果miss 了才老老实实走那四级。程序的访问有很强的局部性同一页会被反复访问所以 TLB 命中率通常很高。这也能回头解释上上一篇讲进程切换时说的 TLB 抖动进程一换页表换了TLB 里按旧页表存的映射批量作废接下来一段时间全是 TLB miss每次访存都要多走几次页表。缺页访问一个虚拟地址时MMU 发现它的 PTE有效位是 0说明这个页不在物理内存里硬件就抛一个缺页异常(Page Fault)把控制权交给内核。内核对两种情况区别对待地址是合法的它在进程的地址空间里(比如mmap了一段文件、或者这个页被换到 swap 去了)目前只是没加载进来。内核去磁盘把它读进一个物理页填好 PTE然后回到出问题的那条指令重新执行一遍。这回 MMU 就能翻译了程序完全不知道中间发生过这个插曲。地址是非法的野指针指向进程根本没有的地址。内核给进程发SIGSEGV就是我们熟悉的那句Segmentation fault。所以每次访问内存都可能触发一次磁盘 IO这件事是真的只是靠局部性和预读把它摊薄了。malloc一块很大的内存后不马上崩、第一次写才开始变慢也是因为物理页是访问到才分配的(这叫按需分页demand paging)。这里能顺带解释一个现象注释掉的代码不改指针、只改数据只要不真的去访问这个地址就可能一直不报错——因为缺页和权限检查都发生在访问的那一刻页表项没被碰到就没事。