Swap:换出去的页怎样回来
上一篇把页回收的整体机制讲清楚了:水位线决定何时回收、kswapd 与 direct reclaim 决定谁来回收。但回收路径里对匿名页有一个前提条件——必须有地方可以把数据写出去。这个"地方"就是 swap。 理解 swap 的关键判断: swap 是匿名页在内存压力下的 backing store;它不是系统失败的标志,而是把物理内存压力转成更慢的外部存储访问。 问题从哪里来 文件页天然有 backing store:磁盘上的原始文件。干净文件页可以直接丢弃,下次访问时从文件重新读入。脏文件页先写回再丢弃。无论哪种情况,数据都有归处。 匿名页没有这种归处。一块 malloc 出来的内存、一个栈帧、一段 mmap(MAP_ANONYMOUS) 区域——它们的内容只存在于物理内存中,没有对应文件。如果要回收这些页面,必须先把内容保存到某个外部存储,等将来再需要时读回来。 没有 swap 时,匿名页完全不可回收。内存压力一旦超过文件页能释放的量,系统直接到 OOM。这就是为什么纯粹"关掉 swap"在内存紧张的工作负载下会导致进程被杀—...
页回收:kswapd 和 direct reclaim
上一篇讲了内核如何用 LRU 近似和 workingset 检测来判断"谁冷谁热"。判断完之后,实际把页面释放出来的工作由回收子系统完成。回收不是一个单点事件,而是围绕水位线、后台线程和分配路径形成的一套压力响应机制。 核心问题可以压成一句话: 回收是围绕水位线的分级压力响应,不是耗尽后的单点动作。 问题从哪里来 内存分配在 Linux 里几乎无处不在:用户态 malloc 背后的匿名页、page cache 的文件页、内核自己的 slab 对象。每次分配都从 buddy allocator 拿物理页。物理页是有限的。 如果等到完全分配不出页面再回收,分配方会被阻塞很长时间——回收可能涉及写脏页、等待 I/O、遍历反向映射。这种"等到没有了才动"的策略延迟不可控。 Linux 的做法是提前开始。内核设定一组水位线(watermark),在不同压力级别触发不同强度的回收。大部分情况下由后台线程 kswapd 在压力升起时提前回收;只有来不及时才在分配路径上直接回收(direct reclaim)。 水位线模型 每个 zone 有三条基本...
LRU 和 workingset:内核如何近似"最近使用"
上一篇把反向映射讲清楚了:内核可以从物理页找回所有映射方。一旦找到,下一个问题是:在所有缓存着的页面中,选谁回收?这就是 LRU 和 workingset 机制要回答的事。 核心问题可以压成一句话: Linux VM 无法知道未来访问序列,只能用最近访问证据、链表分层和 refault 信息近似估计 working set。 问题从哪里来 教材把 LRU 画成一个链表:每次访问把页面移到头部,回收时从尾部摘掉。理论上,这对局部性良好的负载接近最优。 实际系统无法直接实现教材 LRU。原因有三: 第一,每次内存访问都移动链表节点,开销不可承受。用户态一个循环可能每秒触发亿次内存访问,不可能每次都走内核锁链表。 第二,精确 LRU 需要全局排序。两个进程分别访问各自的页面,真正的 LRU 需要知道谁先谁后,需要一个全局时间戳或全局链表锁,NUMA 架构下这是严重瓶颈。 第三,内核的信息来源有限。硬件只提供 PTE 的 Accessed 位:访问过就置 1,内核定期清除再观察是否重新置 1。这是一个"采样"而不是"时间戳"。 因此 Linu...
反向映射:从物理页找回虚拟地址
上一篇把 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...



