反向映射:从物理页找回虚拟地址
上一篇把 folio 作为缓存与回收的管理单位。一旦讨论“回收”,立刻冒出一个问题:内核拿到一个准备回收的 folio,怎么知道哪些进程的页表还指着它?正向页表只能从虚拟地址走到物理页,反过来走不通。反向映射就是为这件事存在的。 核心问题可以压成一句话: 正向页表回答“这个地址翻译到哪一页”,反向映射回答“这一页被哪些地址空间映射”。 问题从哪里来 页表只解决一个方向:拿到一个虚拟地址,按 PGD→P4D→PUD→PMD→PTE 走下去,最终得到 PFN。这是 MMU 在用户态访问路径上需要的。 但内核在很多场景需要反方向:拿到一个物理页(或 folio),找到所有当前映射它的虚拟页和对应 PTE。典型场景包括: 回收一个 folio。释放前必须撤销所有指向它的 PTE,否则用户态访问会拿到已释放的物理页。 迁移一个 folio。把内容搬到新物理页之后,所有原指针都要改写到新 PFN。 COW 写入触发时。即使是 do_wp_page 的判断,也要确认有没有其他映射方共享同一物理页。 在大页拆分、NUMA balancing、kernel same-page mergin...
Folio:为什么内核重新组织 page 抽象
上一篇看完 page cache 如何把文件内容组织成一批页面。中间已经出现过 folio 这个词。这一篇正面回答它:folio 是什么、为什么出现、读源码时该怎样处理这个新名词。 一句话定位 folio 的角色: folio 把”缓存和回收实际处理的一组页面”显式化,去掉 struct page 里 head/tail 二义性带来的语义负担。 问题从哪里来 struct page 在 Linux 内核里承担过太多角色:单页物理元数据、page cache 的索引单元、回收链表节点、复合页(compound page)的 head/tail、THP 的子页、SLUB 的 slab 元数据、设备直通的 page、ZONE_DEVICE 页等等。这个结构体在不同子系统里的字段复用方式不同,对同一个指针的解释也不同。 一个具体的痛点是复合页的 head/tail 二义性。复合页由若干物理页组成,其中第一页叫 head,其余叫 tail。许多接口要求传 head 不传 tail,或者反之;不少接口接受任意一个,但行为有微妙差异。LWN 在 folio 提案的报道里把这种混乱压成一句...
文件映射和 page cache:文件内容怎样变成页面
上一篇看了匿名页和 COW,它们处理的是没有文件 backing 的内存。文件 backing 是另一条主线:文件内容如何进入内核管理的页面池,又如何被 read() 复制到用户缓冲区或被 mmap() 映射进进程地址空间。这一切的共同枢纽是 page cache。 核心问题可以压成一句话: page cache 让文件 I/O 和虚拟内存共享同一批页面对象;read() 和 mmap() 只是进入 page cache 的两种路径。 问题从哪里来 教材常把文件 I/O 和虚拟内存分开讲。读文件用 read,写文件用 write,文件系统维护自己的“缓冲缓存”;进程内存用 mmap、malloc、fork,由 VM 子系统管理。两套体系,互不相干。 Linux 不是这样实现的。当前官方文档对 page cache 给出的定位非常直接: The page cache is the primary way that the user and the rest of the kernel interact with filesystems. 也就是说,绝大多数 read/wr...
匿名页和 COW:fork 为什么便宜,写入为什么变贵
上一篇把缺页异常拆成了 fault 分派的几条路径。do_anonymous_page 处理首次访问匿名页,do_wp_page 处理写保护异常。这两条路径是 Linux VM 里两种延迟行为的核心:匿名页延迟到首次访问才分配物理页,COW 延迟到首次写入才复制。 核心问题可以压成一句话: 匿名页是没有文件 backing 的内存承诺;COW 把页面复制成本推迟到第一次写入。 问题从哪里来 malloc(1GB) 在多数 Linux 系统上不会失败,也几乎不消耗物理内存。fork() 一个驻留几 GB 的进程,能在毫秒级返回,不会发生几 GB 的复制。这两件事在朴素的“内存就是数组、复制就是按字节抄”的模型里都讲不通。 把它们拆开后: malloc 返回的是用户态地址,背后通常是 brk 或匿名 mmap。这一步只是建立一段匿名 VMA,没有分配任何物理页面。第一次访问这段区间的某一页时,缺页异常走到 do_anonymous_page,从空闲页池里取一页、清零、填进 PTE。1GB 全分配但只访问 4KB 的程序,物理内存增长就只有 4KB。 fork 复制的是 mm_s...
缺页异常:一次访问如何进入内核
上一篇区分了 VMA 与页表:VMA 是地址区间策略,页表是 CPU 当前可消费的翻译记录。当 VMA 合法、PTE 却不满足这次访问时,硬件会把异常抛回内核,由内核的缺页处理路径接管。这一篇把这条路径走一遍。 核心问题可以压成一句话: page fault 不是错误的同义词,它是 Linux VM 延迟兑现承诺的主要入口。 问题从哪里来 “缺页异常”这个翻译容易引误解。它在英文里是 page fault,词义中性,本意只是“缺一次翻译”。大量正常程序每秒会产生上百次甚至数千次 page fault,进程跑得很好,没有任何错误。 mmap 完成后第一次访问、fork 后子进程写共享只读页、读一个尚未在 page cache 的文件、被回收过的匿名页再次访问、栈在合法范围内增长,都会触发 page fault。这些 fault 走完之后,程序继续执行下一条指令,用户态察觉不到任何异常。 只有少数情况会让 page fault 变成可见错误:访问完全不属于任何 VMA 的地址、对只读 VMA 写入、对不可执行区段取指令、内核回收阶段无法找到合适页面来满足需求。这些情况要么导致信...
页表:CPU 能读懂的翻译结构
上一篇把地址空间组织成一组 VMA,回答了“这个地址原则上是否属于进程,应该按什么规则处理”。这一篇切到另一层结构:页表。VMA 是内核策略的元数据,页表是 CPU 的 MMU 实际查询的硬件数据结构。两层一致时访问能继续,不一致时进入缺页异常。 核心问题可以压成一句话: VMA 决定一个地址原则上是否合法,页表决定一个虚拟页此刻能否被 CPU 翻译。 问题从哪里来 很多教材把页表画成“虚拟页号到物理页号的一张大表”。这张表足够回答“地址能不能翻译”,但解释不了几件实际发生的事。 一次 mmap 成功后,VMA 已经登记,可是第一次访问还会触发缺页异常,进程并没有出错。一段映射可能在 maps 里看得到、长期不被访问、/proc/<pid>/pagemap 报 present=0,进程也没有出错。一个共享文件页可以同时被多个进程访问,每个进程的页表里都有一份 PTE,但物理页只有一份。一个匿名页可能此刻在内存里、PTE present;过一会儿被 swap 出去,PTE 变成 swap 类型;再被访问时通过缺页恢复,PTE 又重新指向 PFN。 把这些现象统一起...
地址空间不是数组:mm_struct 和 vm_area_struct
上一篇把 Linux VM 的主线压成“先承诺,再兑现”。这一篇先看承诺本身:一个进程说“这段地址可以读写”“这段地址来自某个文件”“这段地址不能访问”,这些信息在内核里不是按字节存成数组,而是按区间组织成一组 VMA。 用户态看见的是地址。内核看见的是地址空间、区间、权限和来源。 1virtual address -> mm_struct -> vm_area_struct -> policy mm_struct 描述一个用户态地址空间,vm_area_struct 描述其中一段连续虚拟地址区间。页表回答“这个虚拟页当前能不能翻译到物理页”,VMA 先回答另一个问题:“这个地址原则上是不是属于进程,应该按什么规则处理”。 从 /proc/self/maps 看地址空间 先从最容易观察的地方开始。运行一个普通进程,查看 /proc/<pid>/maps,会看到类似这样的行: 123455f2b6f3a000-55f2b6f3b000 r--p 00000000 08:01 131104 /usr/bin/cat55f2b6f3b000-55f2b6...
密码学签名全景:从 HMAC 到数字签名的完整图谱
从一个困惑开始 你可能在不同场景见过这些词:HMAC-SHA256、RSA 签名、ECDSA、HS256、RS256、X.509 证书签名……它们都叫"签名",但到底有什么区别?什么时候该用哪个? 这篇文章一次性理清整个图谱。 签名家族的完整分类 先看全貌——所有"签名"机制可以按照是否使用密钥和密钥类型分为三层: 核心区别一句话: 维度 Hash MAC (HMAC) Digital Signature 密钥 无 对称(共享密钥) 非对称(私钥签/公钥验) 证明了什么 数据未被篡改 数据未被篡改 + 来自持有密钥的人 数据未被篡改 + 来自私钥持有者 不可否认性 无 无(双方都有密钥,无法区分谁生成的) 有(只有私钥持有者能生成) 性能 最快 快 慢(10-100x) 典型用途 文件校验、去重 API 签名、Cookie 防篡改 证书、代码签名、法律效力 Layer 0:Hash——没有秘密的指纹 Hash 函数是一切的基础。它把任意长度的输入压缩成固定长度的"指纹"(digest...
重读 Linux VM:给系统研究生的虚拟内存导读
Linux VM 容易读散。它表面上在讲 mmap、页表、缺页异常、匿名页、page cache、folio、LRU、swap、NUMA、huge page、slab、cgroup memory 和 OOM killer,读起来像一串彼此相邻但没有主线的内核机制;主线可以压成一句话: Linux VM 是一套把虚拟地址、物理页、文件、进程、CPU MMU 和 I/O 统一到同一个状态转移系统里的内核机制。 这句话一旦立住,虚拟内存就不再只是“地址翻译”或“把内存变大”。地址空间描述进程能够访问什么;页表描述 CPU 当前怎样翻译地址;物理页描述内核手里真实可调度的资源;page cache 把文件内容放进同一套页管理体系;缺页异常负责把尚未兑现的映射变成真实页面;页回收和 swap 则在内存压力下重新分配这些页面。 本系列以 Linux 6.x(主要参照 6.1–6.8)为基准。涉及版本敏感的机制时(如 maple tree 6.1 引入、per-VMA lock 6.4 稳定、Multi-Gen LRU 6.1 合入、folio 迁移 5.16–6.0 持续进行),各篇会标...
Self-Improving Agent 的边界:外部记忆能不能 shadow 模型权重?
2026 年讨论 self-improving agent 时,最容易混在一起的有三件事:模型权重会不会自己变强,agent 会不会从运行经验里积累结构,harness 会不会自动改造自己。 这三件事都可以被叫作"自我改进",但边界完全不同。第一种发生在 L1,也就是模型权重和训练管线里;第二种发生在 L3/L4,也就是外部记忆、skill、session search、user model 和可检索的外部世界里;第三种发生在 harness 本身,也就是工具、middleware、调度、回滚和验证结构里。 如果不先把层级分开,“Hermes 是不是 self-improving agent”、“OpenClaw 是不是第一个自进化 agent”、"外部记忆能不能替代动态权重"这几个问题都会变成口号战。 一、权重没有变,行为会变 一个冻结权重的模型不会因为昨天完成了一次任务,今天的参数就多出一个新神经回路。对主流商用 API 和开源推理栈来说,部署后的模型权重大体是静态的。所谓 self-improving agent,绝大多数不是在改...
