进程地图
点一个框看它放什么、在哪篇讲过;选一条路径,一步一步看变量落在哪、一块内存怎么来的。蓝字可以直接点开对应的文章,“硬件地图”会打开硬件地图上接着走的那条路。
硬件地图
速查页
静态图
变量放在哪
new 一块内存
访问一个地址
起一个线程
程序启动
看原尺寸
进程地图:一个 C++ 程序的虚拟地址空间
左边是一个进程的虚拟地址空间,高地址在上(x86-64、4 级页表、开了 PIE;不按比例)。右边是这些地址怎么落到物理内存上,以及出了错落在哪一段。
蓝字 = 在哪里讲过(#N = 交易系统笔记 #N,速查 = 速查页的小节,硬件地图 = 打开硬件地图上对应的路径)。数字都是量级。
红线 = 缺页时内核往页表里填一项。
高地址 ↑
低地址 ↓
0xffff ffff ffff ffff
0xffff 8000 0000 0000
0x0000 7fff ffff ffff
≈ 0x7ffc …
≈ 0x7f…
≈ 0x55… / 0x56…
0x0000 0000 0001 0000
0x0
用户空间(128 TiB):每个进程一份
mmap 区 ↓:从高往低分配,下面四类都在这里
没映射的空地:堆往上长,mmap 区往下长,中间留得最大
内核空间(128 TiB):所有进程共用同一份
内核代码和数据、每个进程的页表、每个线程的内核栈(16 KiB)
系统调用、缺页、中断时,CPU 切到这里执行
还有全部物理内存的线性映射、页缓存(读写文件时缓存的文件内容)
用户态的代码读写这段地址 → SIGSEGV
#19 调度策略与内核调优
非规范地址空洞(约 16 M TiB)
地址只有 48 位有效:bit 47 往上必须全和 bit 47 相同,中间这一大段不能用
主线程的栈 ↓
局部变量、函数参数、返回地址;每调用一层,往低地址长一段
上限默认 8 MiB(ulimit -s),用超了碰到保护页 → SIGSEGV
#11 返回值优化:返回的对象直接构造在调用者的栈上
[vvar] [vdso]:内核映射进来的一小段代码和数据
clock_gettime、gettimeofday 在这里直接读 TSC 算时间,不进内核
#20 1.2 clock_gettime 走 vDSO
共享库(.so)
libstdc++、libc、ld.so
每个库也分代码段、数据段
代码页所有进程共用一份
从源码到进程:.so 怎么加载
大块 malloc(≥ 128 KiB)
glibc 直接 mmap 一块新映射
free 时立刻 munmap 还给内核
门槛会随使用自动调高
#3 2.1 mmap 陷阱
其他线程的栈 + TLS
每个线程一块,默认 8 MiB
最低处留一页保护页
thread_local 在栈顶那头
文件映射、共享内存
mmap 文件:读写的就是页缓存
/dev/shm:几个进程映射同一批页
大页(hugetlbfs)也映射在这里
#5 2 放进共享内存
堆 ↑(brk)
new、malloc 的小块从这里切:先查本线程的缓存(tcache),不够再找 arena
arena 不够了,brk 把堆顶往上推;推出来的只是虚拟地址
#3 内存池与分配器
.bss:没有初值的全局变量和 static 变量
初值都是 0,文件里只记大小,启动时映射成全 0 的页
.data:有初值的全局变量和 static 变量
初值存在可执行文件里,启动时映射进来,可读可写
.rodata:只读数据
字符串字面量、有常量初值的 const 全局变量、编译期算好的查找表
vtable 在 .data.rel.ro:启动时填好函数地址,再改成只读
虚函数 1.2 vptr 和 vtable
.text:机器码(只读 + 可执行)
函数编译出来的指令;.plt 是调 .so 里函数的跳板
PIE 每次加载到随机地址;gdb 默认关掉随机化,基址常见 0x5555 5555 4000
#6 2.6 函数分组:代码也走缓存
0 – 64 KiB:不映射
空指针解引用落在这里 → SIGSEGV(vm.mmap_min_addr = 65536)
C++ 里写的东西放在哪
int g = 1; // .data
int z; // .bss
const char* s = "hi"; // s:.data,"hi":.rodata
thread_local int t; // 每个线程一份(TLS)
void f() { // 指令:.text
int x = 0; // 栈
int* p = new int(2); // p:栈,*p:堆
char* b = new char[1 << 20];
// 1 MiB:mmap 区
}
线程:共享什么,各有什么
共享:整个地址空间,同一份页表
(代码、全局变量、堆、mmap 区、文件描述符)
各一份:栈、TLS、寄存器、内核栈
所以全局和堆上的数据要同步;
栈上的变量地址不传出去,就只有自己用
#2 内存序与缓存行
#18 线程安全队列
页表:虚拟地址 → 物理地址
每个进程一份;4 级,每级用 9 位,页内偏移 12 位
TLB 命中不额外花时间;没命中查页表约 15–30 ns
2 MiB 大页少查一级,一项 TLB 管 512 倍的地址
硬件地图:dTLB → 一次 load 走到内存
#6 2 MiB 大页
速查 2.1 TLB 覆盖范围
物理内存(内存条)
物理页零散地分在内存条上
连续的虚拟地址,背后的物理页不一定连续
同一个 .so 的代码页、/dev/shm 的页:几个进程共用
硬件地图:内存控制器 → 通道
每格 = 一页物理内存(4 KiB),颜色 = 属于左边哪一段;
白格 = 空闲或属于别的进程。同一种颜色的格子并不挨着。
缺页:第一次碰到才给物理页
new、mmap、brk 只拿到虚拟地址,页表里还没有
第一次写:陷入内核,分一页、清零、填进页表
约 0.5–2 µs;要读磁盘的(major)几十 µs 到 ms
地址不在任何映射里,或权限不对 → SIGSEGV
#3 3.2 预先缺页 + mlock
出了错,落在图上哪里
空指针解引用 → 最低 64 KiB 的不映射区:SIGSEGV
递归太深、局部数组太大 → 越过栈底的保护页:SIGSEGV
改字符串字面量 → 写只读的 .rodata:SIGSEGV
越界、用已经释放的内存 → 多半还落在堆的映射里,不一定立刻崩,要用 ASan 查
内存泄漏 → 堆或 mmap 区一直涨,RSS 跟着涨;double free → glibc 检查到后 abort
在自己机器上看
cat /proc/<pid>/maps # 每一段的地址范围、权限、来源文件(r-xp 就是 .text)
pmap -x <pid> # 每一段实际占了多少物理内存(RSS)
size a.out # .text、.data、.bss 各多大
perf stat -e page-faults,dTLB-load-misses ./a.out # 缺页、TLB 未命中各几次
ulimit -s # 栈上限(KiB)
几个常见说法,哪里不对
“堆比栈慢”:访问一样快,都是普通内存;慢在 malloc 耗时不固定,以及对象散开以后局部性差
“用户空间 3 GB、内核 1 GB”:那是 32 位。64 位上用户空间是低 128 TiB,内核是高 128 TiB
“全局区空间大”:.data、.bss 的大小链接时就定死了,要能变大的数据放堆上
“new 了就占内存”:第一次写之前不占物理页,RSS 不涨
“栈上的变量别的线程碰不到”:地址传过去就能读写,栈也在同一个地址空间里
虚拟
地址
物理地址
内核填一项
地址范围按 x86-64 Linux 的默认布局;开了 ASLR,栈、mmap 区、堆和程序本身的起点每次运行都随机挪一点。
图随文章更新:讲到 fork、动态链接、IO 时再往上补。