[{"content":"物理内存是什么 学习 Linux 内核内存管理，第一个问题看起来很简单：从内核的角度来看，物理内存到底是什么？\n你的内存条是一大片连续的字节。内核不会把它当成一整块来用——它把内存切成固定大小的块，每一块叫做一个页（page）。在 x86_64 上，默认页大小是 4KB，一台 8GB 内存的机器大约有 200 万个页。\n4KB 是谁定的？为什么是 4KB？ 这是硬件决定的，不是内核决定的。每颗现代 CPU 里都有一个专门负责虚拟地址到物理地址翻译的硬件单元，叫做 MMU（内存管理单元）。MMU 在芯片设计阶段就固定了它支持的页大小，内核只能用 MMU 支持的那几种，没有别的选择。\nx86_64 的 MMU 支持三种页大小：\n大小 名称 典型用途 4KB 普通页 通用 2MB 大页（Huge Page） 数据库、大内存映射 1GB 巨页（Gigantic Page） 特殊高性能场景 4KB 成为默认值是因为它在两个极端之间取得了平衡：足够小，不会在稀疏分配时浪费太多内存；足够大，不会让页表变得过于庞大。内核可以通过 THP（透明大页） 或显式的 mmap 标志使用 2MB 或 1GB 的大页，但 4KB 是一切的基础。\nstruct page — 每个页的\u0026quot;档案卡\u0026quot; 内核需要追踪每一个页的状态：它在被使用吗？被谁用？能不能被回收？有没有被写脏？\n这些信息存在 struct page 里，定义在 include/linux/mm_types.h。可以把它想象成停车场里每个停车位上贴的一张档案卡——停车场是你的内存，每个停车位是一个页，档案卡记录了谁停在这里、现在是什么状态。\n1 2 3 4 5 6 7 8 9 10 struct page { memdesc_flags_t flags; /* 状态标志位 — 脏页、锁定、回写中等 */ union { struct list_head lru; /* LRU 链表指针，用于内存回收 */ ... }; atomic_t _mapcount; /* 有多少个页表项映射了这个页 */ atomic_t _refcount; /* 引用计数 — 降到 0 时页才能被释放 */ ... }; 逐个字段来看。\nflags 一个位掩码，记录页的各种状态。常见的标志位：\nPG_dirty — 页已被写入但尚未刷回磁盘 PG_locked — 有人持有页锁（比如正在做 I/O） PG_writeback — 页正在被写回存储 PG_uptodate — 页的数据是有效且最新的 PG_lru — 页在某个 LRU 链表上 这些标志位的操作都是原子的，因为多个 CPU 可能同时访问同一个页。\n_mapcount 记录有多少个进程的页表项（PTE）指向这个页。当一个页被多个进程共享时（比如共享库），_mapcount 就会大于 0。它的初始值是 -1，表示\u0026quot;没有任何映射\u0026quot;，每增加一个映射就加一。\nlru 一个 list_head，把这个页链入内核的某个 LRU（最近最少使用）链表。内存回收子系统（kswapd）通过这些链表找到可以驱逐的页。这个字段是一个 union 的一部分——根据页当前的用途，它会被复用来存储其他信息。\n现代抽象：folio 在较新的内核版本（5.16+）中，内核引入了 struct folio 作为 struct page 的上层抽象。一个 folio 代表一组物理连续、大小为 2 的幂次方的页，作为一个整体来操作。引入它的动机是减少处理大页时的开销——与其对一个 2MB 大页的 512 个 struct page 逐一操作，不如用一个 folio 来统一处理。\n大多数新内核代码都操作 folio 而不是原始的 struct page。底层的 struct page 还在，但 folio API（folio_get()、folio_put()、folio_ref_count()）是推荐的接口。\n引用计数：_refcount _refcount 是页的引用计数——一个 atomic_t，记录当前有多少个内核子系统持有对这个页的引用。规则很简单：_refcount 降到零时，页才能被释放。\n1 2 3 4 5 6 7 8 9 10 11 12 // include/linux/mm.h static inline void folio_get(struct folio *folio) { atomic_inc(\u0026amp;folio-\u0026gt;_refcount); } void __folio_put(struct folio *folio) { if (folio_is_zone_device(folio)) { free_zone_device_folio(folio); return; } /* ... 最终把页还给伙伴分配器 */ } 谁会持有引用？\n进程的页表映射了这个页 → +1 页缓存（page cache） 持有一个文件页 → +1 驱动程序正在用这个页做 DMA → +1 内核临时 pin 住一个页做 I/O → +1 当所有这些引用都释放后，_refcount 归零，__folio_put() 被调用，页被归还给伙伴分配器（后续文章会详细讲）。\n_refcount 和 _mapcount 容易混淆。_mapcount 只统计页表映射。_refcount 统计所有引用，包括非映射类型的引用（比如页缓存的 pin）。一个页可以 _refcount \u0026gt; 0 同时 _mapcount == -1——它被内核持有，但没有映射到任何进程的地址空间。\n脏页与写回 当进程写入一个页，而这个修改还没有刷回后端存储（磁盘或网络文件系统）时，这个页就变成了\u0026quot;脏页\u0026quot;。内核不会立即写回每一个脏页——那样会慢得无法接受。它用三道机制来保证脏页最终会被写回：\n第一道：定时器，每 5 秒触发一次\n1 2 // mm/page-writeback.c unsigned int dirty_writeback_interval = 5 * 100; /* 单位：百分之一秒 */ 内核线程每 5 秒唤醒一次，把脏了太久的页刷回去（默认超过 30 秒的脏页会被强制写回）。\n第二道：脏页比例阈值\n1 2 int dirty_ratio = 20; /* 进程触发同步写回的阈值 */ int dirty_background_ratio = 10; /* 后台写回线程启动的阈值 */ 当脏页占总内存的比例超过 dirty_background_ratio（默认 10%），后台写回线程就会启动。超过 dirty_ratio（默认 20%），写入进程本身会被阻塞，强制同步写回。\n第三道：内存回收触发写回 当系统内存紧张，kswapd 开始回收页时，遇到脏页会先触发写回，写完再回收。\n这三道防线保证了即使没有显式的 fsync()，脏页也不会永远留在内存里。\nDMA 一致性：内存管理的边界 学到这里自然会想到 DMA。DMA 操作的内存必须是物理连续的，所以驱动会调用 mm/ 里的接口来分配页（比如 alloc_pages(GFP_DMA)）。但 dma_alloc_coherent() 这类 API 管的是另一件事——CPU cache 和内存之间的一致性。\n这一层是纯软件概念，跟具体设备无关。它的实现在：\nkernel/dma/coherent.c — dma_alloc_coherent() 的核心逻辑 arch/x86/mm/、arch/arm64/mm/ — 各架构的 cache flush 实现 所以 DMA coherency 不属于 mm/ 子系统，而是 kernel/dma/ + 各架构 mm/ 的交叉地带。mm/ 负责分配物理内存，DMA 层负责保证访问这块内存时 cache 是一致的。\n总结 本文建立了 Linux 物理内存管理的基础认知：内核如何将 RAM 切分为固定大小的页、通过 struct page 追踪每个页的元数据、以及 _refcount 引用计数如何管理页的生命周期。我们还分析了脏页通过三道独立机制保证最终写回的原理，以及 DMA coherency 与 mm/ 子系统的边界关系。struct folio 作为现代抽象层叠加在这一切之上，为内核其他部分提供了更简洁的操作接口。\n","permalink":"https://blog.troy-y.org/zh/posts/linux-mm-0-physical-memory-and-struct-page/","summary":"Linux 内核如何将 RAM 划分为页，通过 struct page 跟踪每页元数据，并通过引用计数和脏页回写机制管理页的生命周期。","title":"linux-mm[0]: 物理内存与 struct page"},{"content":"这是我的第一篇 Hugo 博客文章。\n从 Hexo + Butterfly 迁移到 Hugo + PaperMod，追求简洁高效。\n","permalink":"https://blog.troy-y.org/zh/posts/hello-world/","summary":"\u003cp\u003e这是我的第一篇 Hugo 博客文章。\u003c/p\u003e\n\u003cp\u003e从 Hexo + Butterfly 迁移到 Hugo + PaperMod，追求简洁高效。\u003c/p\u003e","title":"你好世界"}]