缺页表示处理器无法依据当前映射或权限完成一次访问。它不等于存储读取,有些缺页还代表非法访问,而不是可以恢复的按需分页。Linux 的次缺页与主缺页计数,区分处理过程中是否需要页面换入输入输出。解释时应结合映射类型与工作负载,不能把任意一个计数直接当作应用全部存储流量,更不能仅凭名字中的“主”“次”判断问题严重程度。
次缺页不代表没有成本
次缺页可能为已有文件缓存页建立映射,也可能为匿名内存分配并清零物理页,或在写时复制时复制一页内容。内核可能需要分配内存、维护页表、复制数据和同步。把它说成只增加一个指针,会遗漏这些真实成本。即使完全没有存储读取,密集次缺页仍然可以消耗可观处理器时间,影响请求延迟,不能因为分类是 minor 就无条件忽略。
应用第一次访问,并不保证每个基本页都产生一次新的缺页。预先建立映射、已存在的映射、预读相关处理和大页都可能改变计数。系统页大小也不是处处都等于四千零九十六字节。示例应读取本机值,不能从循环次数直接承诺精确缺页数量;否则换一台机器或启用不同内存策略,实验就可能因为错误前提而失败。
主缺页描述另一条处理路径
主缺页意味着处理涉及页面换入输入输出,常见于文件映射或已换出的内存。背后可能是存储设备,也可能是远程文件系统,因此这个分类不是物理磁盘延迟基准。一次缺页还可能触发预读,让后续访问直接获益,计数与页数或磁盘请求数并不一一对应。反过来,普通文件读取也可能产生输入输出,却不在应用里形成对应的主缺页。
文件页已经在主机缓存中时,新进程即使从未访问过它,也可能只产生次缺页。重启进程不等于清空整台机器的页缓存。匿名内存首次写入通常通过分配零填充页面完成,不必从存储读回旧载荷,但压力和同时发生的活动仍会影响测量。“匿名”不能保证整个进程的主缺页计数在任何环境都保持不变;应以本次实际计数为准。
坏掉的样子
冷启动延迟可能与主缺页同时出现,但在查清缺少什么之前,不应一律建议预热。提前触碰不需要的页面会消耗内存,甚至挤掉有用缓存。错开启动也许能缓解已确认的共享瓶颈,另一个负载却可能需要更小映射、不同访问模式或更多容量余量。单靠缺页数无法在这些方案之间作出可靠选择,需要把变化与业务延迟联系起来。
持续交换会让被逐出的页面反复进入工作集,并拉长等待。增加工作者可能扩大内存需求,使情况更差。应关联缺页、换入换出、回收、存储等待和请求延迟,而不是一看到主缺页上升就宣布存储设备损坏,或认定所有缓存缺失都本来可以避免。对顺序扫描和随机访问,也应分别理解预读与局部性的作用。
进程级计数还包含测量代码之外的工作。解释器、启动过程和其他线程都可能贡献变化。常驻情况查询只是一个时刻的快照,紧接着访问时状态可能已经改变。不要为了得到更明显的图表就清空全局缓存,或不断扩大分配直到影响其他任务。诊断实验应该有明确资源预算,并保留失败时的原始观察,而不是只保留符合预期的数字。
快速验证
保存为 fault_probe.py,在有足够空闲内存的 Linux 上使用 Python 3.9 或更新版本执行 python3 fault_probe.py。程序创建十六 MiB 的私有匿名映射,按本机页大小每页写一个字节,再重复一轮。结束时自动关闭映射,不操作文件、不修改全局缓存设置,也不改变任何内存限制。
import mmap
import resource
import sys
import time
if not sys.platform.startswith("linux"):
raise SystemExit("This exercise requires Linux getrusage semantics")
PAGE = mmap.PAGESIZE
SIZE = 16 * 1024 * 1024
def counters():
value = resource.getrusage(resource.RUSAGE_SELF)
return value.ru_minflt, value.ru_majflt
def touch(region):
for offset in range(0, len(region), PAGE):
region[offset] = 1
with mmap.mmap(-1, SIZE, flags=mmap.MAP_PRIVATE | mmap.MAP_ANONYMOUS,
prot=mmap.PROT_READ | mmap.PROT_WRITE) as region:
before = counters()
start = time.perf_counter()
touch(region)
first_seconds = time.perf_counter() - start
middle = counters()
start = time.perf_counter()
touch(region)
second_seconds = time.perf_counter() - start
after = counters()
first = tuple(b - a for a, b in zip(before, middle))
second = tuple(b - a for a, b in zip(middle, after))
print(f"page_size={PAGE} mapping_bytes={SIZE}")
print(f"first: minor={first[0]} major={first[1]} seconds={first_seconds:.6f}")
print(f"second: minor={second[0]} major={second[1]} seconds={second_seconds:.6f}")
assert first[0] > 0, "expected demand-fault activity"
assert second[0] < first[0], "repeat pass did not show reduced minor faults"
assert first[1] == second[1] == 0, "observed major faults: this run is not a no-major example"
print("PASS: this run had fewer second-pass minor faults and no measured major faults")
本实验预期第一轮出现按需缺页,第二轮次缺页减少,并且这次运行没有测得主缺页。本轮两种语言的完整示例均通过这些断言。计数与耗时只是观察,不是固定目标。脚本没有断言第二轮一定更快,因为计时、调度和机器负载会变化。使用本机页大小,也避免把某种平台常见值误当成通用常量。
如果断言失败,说明当前运行没有建立预期对照,应检查内存条件和计数,不能悄悄删除检查。通过结果展示的是一个匿名内存案例:发生了缺页活动,却没有测得页面换入输入输出。它没有演示文件映射主缺页,没有给出通用缺页延迟,也不能替代真实应用在代表性压力下的测量。把这个局部结论保持在实验范围内,才不会从可重复现象跳到错误的生产诊断。
来源
- Linux getrusage,次缺页和主缺页计数语义,已核验 2026-10-15。
- Linux mmap,私有匿名映射与预先填充行为,已核验 2026-10-15。
- Linux memory concepts,按需分页、页缓存、回收和大页,已核验 2026-10-15。
- Linux process stat,进程缺页计数与范围,已核验 2026-10-15。