1.4 篇从内核侧走过页表,这篇换到用户态视角,用 /proc/self/pagemap 把"我的虚拟页此刻在物理世界的什么位置"逐位读出来;再用 smaps 看懂内核给每个进程记的内存账.中间有个意外:一个我以为十拿九稳的功能在这块板上是死的,原因值得单独说.
这篇回答几件事:
- pagemap 的 64 位每一位什么意思,能用来干什么
- 为什么"写了一页"不代表会留下软脏标记
- Rss,Pss,Shared,Private 这套账本怎么读
- 反向映射解决什么问题
pagemap 是页表的镜像:进程每 4KB 虚拟页在 /proc/self/pagemap 里占一个 64 位条目.实测一个匿名页的一生:
未触碰 entry=0000000000000000 present=0
写入后 entry=8100000000012054 present=1 excl=1 pfn=0x12054
逐位拆:bit63 是 present,没摸过的页条目干脆全零 — 按需分页在页表层的样子,和 1.4 篇从内核侧看到的"pte 全零"是同一件事的两面.bit56 是 exclusively mapped:这页没有和别人共享,独一份.bit54 到 0 是物理帧号,拿到 PFN,内存世界的对账就开始了:0x12054 在哪个 zone(7.1 篇:板上全在 DMA),在 buddyinfo 的哪个阶,甚至物理相邻的是谁,全都查得到.这就是各类内存分析工具(pagemap 采样的 smem,内存取证)的地基:虚拟世界的每个地址,都能落到物理世界的一个抽屉.
这套能力是排错利器.怀疑某进程的页被"偷"进了错误 zone?按 PFN 对 zoneinfo.做内存快照差异分析?present 位变化就是页生灭.安全研究里页表侧信道的历史考题,很多也是围绕这些位的可见性展开的.
然后是我翻车的那次.计划里要演 soft-dirty:bit55,页"被写脏"的标记,clear_refs 写 4 清零后,再写的页会重新置位 — 增量追踪页级写活动的官方机制,checkpoint/restore 和热迁移都在用.实测:写页,清零,再写,制造缺页重写,bit55 纹丝不动全程为零.一路排查到配置:这块内核根本没编 CONFIG_MEM_SOFT_DIRTY,bit55 是个永远不亮的装饰灯,QEMU 那份 defconfig 同样没编.这个功能从内核内置选项拆成独立 Kconfig 是近几年的事,默认就悄悄关了 — 教程里"写 clear_refs 就能看到"的世界,和手上这台机器的现实之间差一个配置项.顺带实验还澄清了一个语义:对已经映射好的页再写,不发生缺页,不经过内核,什么标记都不会有;想重新触发一次"内核看得见"的写,得 mprotect 权限往返逼一次缺页.两个教训打包:功能先查配置,行为先问缺页.
smaps 是另一本账,/proc/PID/smaps_rollup 是它的汇总页,systemd 的实测:
Rss: 13792 kB
Pss: 6525 kB
Shared_Clean: 9832 kB
Private_Clean: 128 kB
Private_Dirty: 3832 kB
Rss 是"我摸过的物理内存总量",但它把共享页全价计入 — libc 的代码页每个进程都算一遍,系统级一汇总就虚高.Pss(proportional set size)把每张共享页按共享进程数平摊,自己独占的全额,百人共享的算百分之一 — 所有进程的 Pss 加起来恰好等于系统总内存,这才是容量规划该用的数.账本自洽:Shared_Clean 加 Private_Clean 加 Private_Dirty 正好等于 Rss.systemd 那行还讲了个故事:它 13.8MB 的 Rss 里 9.8MB 是共享的干净文件页(可执行代码),真正的私有开销只有 3.9MB — 一个 init 进程的体面.
反向映射(rmap)是页表故事里内核自己用的那半本账.页表是"虚拟页到物理页"的正向索引,回收一张物理页时问题反着来:这页都被谁的页表映射着?fork 出去共享的页,映射它的 PTE 散落在多个进程的页表里,回收或迁移前必须挨个拆掉 — rmap 就是这套从 struct page 反查所有映射者的索引(匿名页挂 anon_vma 链,文件页挂 address_space).没有它,COW 拆共享,页迁移,KSM 合并全是空中楼阁.1.4 篇实测里 fork 后 mprotect 只读触发的写保护缺页,内核能在瞬间找到"这一页的所有主人"各发一份副本,幕后就是 rmap 在跑腿.
最后留几个容易忽略的细节:
- pagemap 的 PFN 字段对非特权用户是清零的(kptr_restrict 时代的安全收敛),root 才能拿到真帧号,容器里默认更是别想.
- excl 位在 fork 后会掉:父子共享的页两边都 excl=0,直到 COW 各自拿私页;用它能精确数出"还没分家的页",热迁移预拷贝的依据之一.
- smaps 的 Anonymous 只算匿名页,文件页即使脏了(Dirty)也不进这列;判断"纯计算型进程"内存画像,看 Anonymous 对 Rss 的占比最直接.
- smaps_rollup 是 5.x 后加的汇总接口,比逐 VMA 的 smaps 便宜几个量级,监控采集用它,别再全量解析 smaps.
- AnonHugePages 字段所有进程都有,但值非零要 THP;板上没编,恒零,QEMU 那份 defconfig 编了,下回在 QEMU 里补演.
- pagemap 读自己也要过 seqlock 一致性:条目在极端并发下可能读到正在修改的中间态,采样工具要做合理性校验.
- 回收决策不直接用这些账:内核自己的工作集估计基于页表 accessed 位和 LRU 链,但用户态工具的一切"内存画像",地基就是本篇这两个接口.