SLUB:小对象为什么不直接按页分配
上一篇讲了 buddy allocator 如何按阶管理物理页的分配与释放。但 buddy 的最小粒度是一页(4KB)。内核中绝大多数对象远小于 4KB——一个 struct dentry 是 192 字节,一个 struct inode 是 600 字节左右。如果每个小对象都分配一整页,内部碎片率将高达 95% 以上。 核心问题可以压成一句话: 内核对象通常比一页小得多,SLUB 用 slab cache 把页切成同类型对象池,减少碎片和初始化成本。 问题从哪里来 内核不断创建和销毁各种数据结构:每次 open() 分配一个 struct file,每次路径查找分配一个 struct dentry,每次网络包到达分配一个 struct sk_buff。这些对象有两个共同特点: 第一,大小远小于 4KB。如果用 buddy allocator 直接分配,一个 192 字节的 dentry 占用 4096 字节物理页,利用率不到 5%。 第二,同类型对象的分配/释放极为频繁。dentry 在路径解析密集的工作负载中每秒创建/销毁数十万次。每次都调用 buddy allocat...
Buddy system:物理页如何按阶分配
上一篇讲了 huge page 如何通过增大映射粒度降低 TLB 和页表开销。但分配 2MB 连续物理内存本身就不是简单的事——物理页分配器必须能快速找到指定大小的连续空闲块。Buddy system 就是 Linux 用来解决这个问题的核心机制。 核心问题可以压成一句话: buddy allocator 用按阶(order)拆分和合并管理连续物理页,解决的是页级分配与碎片控制问题。 问题从哪里来 内核需要分配连续物理页的场景很多:DMA buffer 要求物理连续、huge page 需要 order-9(2MB)的连续块(1GB 页通常在启动时通过 HugeTLB 机制预留,不走常规 buddy 路径)、设备驱动需要特定对齐的物理内存。 最简单的方案是维护一个空闲页链表。但这有两个问题:第一,找"连续 N 页"需要遍历链表检查物理地址连续性,时间复杂度不可控;第二,反复分配释放后内存碎片化,即使总空闲页足够也可能凑不出连续块。 Buddy system 用 power-of-two 分块解决这两个问题:只管理 2^n 大小的块(称为 order-n ...
Huge Page:TLB 压力和页表开销
上一篇讲了 NUMA 如何让内存访问不再均匀。但即使在单 node 系统上,当工作集足够大时,另一类开销浮现:TLB miss 和页表自身的内存消耗。Huge page 通过增大映射粒度来同时缓解这两个问题。 核心问题可以压成一句话: huge page 用更大的映射粒度减少 TLB 和页表开销,但引入碎片、分配时机和回收复杂度作为代价。 问题从哪里来 标准页大小 4KB。一个 64GB 内存的机器有 1600 万个物理页。如果一个进程映射了 8GB 工作集,对应 200 万个 PTE。 TLB(Translation Lookaside Buffer)是 MMU 的地址翻译缓存。现代 CPU 的 L1 dTLB 通常只有 64-128 个条目,L2 sTLB 有 1024-2048 个条目。200 万个活跃页面对 2048 个 TLB 条目意味着 TLB 覆盖率不足 0.1%——绝大多数访问要走 page table walk。 page table walk 不是免费的。它需要 4-5 次内存访问(每级页表一次),即使有 page walk cache 辅助,在 TLB...
NUMA:内存为什么有远近
上一篇讲了 swap 如何为匿名页提供外部 backing store。讨论中一直隐含一个假设:物理内存是一块均匀的资源,任何 CPU 访问任何物理页的代价相同。在 NUMA 架构下这个假设不成立。 这篇要回答的核心矛盾: NUMA 让"物理内存"不再是均匀资源;分配位置、CPU 位置和迁移策略共同决定访问成本。 问题从哪里来 在 UMA(Uniform Memory Access)系统中,所有 CPU 共享一条总线访问同一组内存控制器。每个 CPU 访问任何物理地址的延迟相同。早期单路和低端双路机器大多是这种架构。 当 CPU 数量增加,单一总线成为瓶颈。NUMA(Non-Uniform Memory Access)把系统拆成多个节点(node),每个节点包含若干 CPU 核心和一组本地内存。节点内部通过本地总线通信(快),节点之间通过互联(QPI、UPI、Infinity Fabric 等)通信(慢)。 1234567891011UMA: CPU0 CPU1 CPU2 CPU3 \ | | / shared bus ...
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...





