AI智能摘要
深入剖析AArch64架构中Linux缺页异常的处理流程,本文聚焦do_swap_page函数的定位及其在缺页处理中的关键角色。通过梳理PTE的多种状态分支,明确do_swap_page仅在PTE为非present且编码为swap entry或特殊entry时介入,对swap entry的结构、swp_entry_t的编码机制进行深度解析,阐释type与offset的打包方式及其如何映射到实际swap区。文章还系统区分了swap cache与page cache的本质差异:page cache负责文件页面管理,swap cache则专为匿名页与swap slot建立高效缓存。
此摘要由AI分析文章内容生成,仅供参考。

接上文:[linux内存管理] 第038篇 深入剖析AArch64架构下的do_page_fault缺页异常处理,我们提到了缺页异常走到最后有这几种情况,见下图

2026/01/halo_k23dk9b.png

我们在上一篇[linux内存管理] 第040篇 文件映射与匿名映射中已经聊过了匿名映射以及文件映射,所以这一篇文章咱们聊一聊这剩余的两个函数do_swap_page

do_swap_page 在缺页异常里的位置

如果把缺页异常当作一条“从硬件异常到把页装进页表”的流水线,那么 do_swap_page() 不是这条流水线的入口,也不是出口,它处在 缺页处理的“分类分流”之后:当内核确认这次 fault 对应的 PTE 不是 present 的物理页映射,而是一个 swap entry(或类似的特殊 entry) 时,才会进入 do_swap_page()

handle_pte_fault() 中,常见分支大致可以按“PTE 的形态”理解:

  1. PTE none(完全无映射)

    • 例如匿名页首次访问:走 do_anonymous_page()

    • 或者继续走文件页 fault 的路径(取决于 VMA 属性)

  2. PTE present(已映射)但 fault 发生

    • 常见是写时复制(COW)或写保护:走 do_wp_page()

    • 也可能是权限/dirty/young 等导致的处理(不同场景不同)

  3. PTE not present,但编码了“某种 entry”
    这就是 do_swap_page() 介入的地方:

    • 真正的 swap entry:页被换出到 swap,PTE 里留下 (type, offset)

    • 非 swap 的特殊 entry(non_swap_entry):migration entry / device private / hwpoison 等

///pte为空
	if (!vmf->pte) {
		if (vma_is_anonymous(vmf->vma))
			return do_anonymous_page(vmf);  ///处理匿名映射
		else
			return do_fault(vmf);           ///文件映射
	}

///pte不为空
	if (!pte_present(vmf->orig_pte))   ///pte存在,但是不在内存中,从交换分区读回页面
		return do_swap_page(vmf);

	if (pte_protnone(vmf->orig_pte) && vma_is_accessible(vmf->vma))  ///处理numa调度页面
		return do_numa_page(vmf);

#define pte_present(pte)	(!!(pte_val(pte) & (PTE_VALID | PTE_PROT_NONE)))

swap entry 是什么

上一节我们确定了:只有当 PTE 是 non-present,并且携带了 entry(尤其是 swap entry)时,缺页路径才会进入 do_swap_page()。这一节我们把 do_swap_page() 的“输入”讲透——也就是 swap entry 本身:它长什么样、表达什么含义、怎么把一个逻辑票据落到真实的交换区 slot。

从 PTE 到 swp_entry_t

我们进入 do_swap_page 看看

	entry = pte_to_swp_entry(vmf->orig_pte);  ///获取换出页标识符
	if (unlikely(non_swap_entry(entry))) {   ///非换出页标识符,处理迁移页面,复用swap机制
		if (is_migration_entry(entry)) {
			migration_entry_wait(vma->vm_mm, vmf->pmd,
					     vmf->address);
		} else if (is_device_exclusive_entry(entry)) {
			vmf->page = pfn_swap_entry_to_page(entry);
			ret = remove_device_exclusive_entry(vmf);
		} else if (is_device_private_entry(entry)) {
			vmf->page = pfn_swap_entry_to_page(entry);
			ret = vmf->page->pgmap->ops->migrate_to_ram(vmf);
		} else if (is_hwpoison_entry(entry)) {
			ret = VM_FAULT_HWPOISON;
		} else {
			print_bad_pte(vma, vmf->address, vmf->orig_pte, NULL);
			ret = VM_FAULT_SIGBUS;
		}
		goto out;
	}
  • vmf->orig_pte 解码出 swp_entry_t entry

  • 然后判断它是不是 non_swap_entry(entry)

  • 如果不是,才进入真正的 swap-in 流程

可以把它理解成:

do_swap_page() 的第一步不是“读盘”,而是“把 PTE 里那种特殊编码,翻译成 swap 子系统认识的逻辑地址(swp_entry_t)”。

这里的关键点:PTE 里存的不是磁盘块地址,而是一个逻辑票据

swp_entry_t 的语义

typedef struct {
    unsigned long val;
} swp_entry_t;

这看起来不像我们常说的 “(type, offset)” 二元组——因为它确实不是两个字段

关键点在于:swp_entry_t 是一个不透明编码类型(opaque encoding)。swap entry 的语义仍然是:

  • type:第几个 swap area(swap 分区或 swapfile)

  • offset:该 swap area 内的第几个 slot(页粒度索引)

swap area 逻辑上可以理解成这样:

offset:   0        1        2        3        4  ...
        [HDR]   [PAGE]   [PAGE]   [PAGE]   [PAGE] ...

其中 offset 0 通常是 header,因此 swap readahead 会显式跳过它。读者只要记住:

swp_offset(entry) 是“页粒度索引”,而不是磁盘扇区/LBA。
从 offset 映射到块 IO,是 swap 子系统(swapfile 映射/块设备映射)完成的。

type 与 offset 被打包进 entry.val 的不同 bit 位域中。内核通过一组宏/内联函数来编码与解码:

  • swp_type(entry):从 entry.val 解出 type

  • swp_offset(entry):从 entry.val 解出 offset

  • swp_entry(type, offset):把 type/offset 打包成 entry.val

pte_to_swp_entry() 在干什么?

static inline swp_entry_t pte_to_swp_entry(pte_t pte)
{
	swp_entry_t arch_entry;

	pte = pte_swp_clear_flags(pte);
	arch_entry = __pte_to_swp_entry(pte);
	return swp_entry(__swp_type(arch_entry), __swp_offset(arch_entry));
}

当匿名页被换出(swap out)后,PTE 不再指向 PFN,而是变成 non-present,并把 swap entry 的编码塞进 PTE 的“特殊编码空间”里。缺页时,do_swap_page() 会从 vmf->orig_pte 开始:

  • 先调用 pte_to_swp_entry(orig_pte) 把 PTE 中的编码解码成 swp_entry_t entry

  • 后续再通过 swp_type(entry) / swp_offset(entry) 拆出 type/offset 去找 swap area 与 slot

non_swap_entry(entry) 是什么?

这里要着重声明的一点就是,non-present entry 并不等于 swap。Linux 会把一些“临时/特殊状态”也编码进同一套 entry 空间里,例如:

  • migration entry:页正在迁移

  • device private / device exclusive:设备内存页,需要迁回 RAM

  • hwpoison:坏页

这些 entry 与 swap entry 共享同一个“打包编码容器”(entry.val),所以必须先用 non_swap_entry(entry) 把它们分流处理掉,否则你会把它们当 swap 去读盘,逻辑就完全错了。

swap cache VS page cache

page cache:文件页缓存

page cache 的定位很明确:缓存文件内容页。在内核里,它的索引键是:

  • address_space *mapping:通常来自 inode(struct inode->i_mapping

  • pgoff_t index:文件内页号(offset / PAGE_SIZE)

也就是说:

page cache key = (mapping, index)
mapping = “这个文件对象”
index   = “文件内第几页”

page cache 的典型使用场景:

  • read() 文件:先查 page cache,miss 再发 IO

  • mmap 文件映射缺页:走 filemap_fault(),优先命中 page cache

  • 写回、回写一致性、脏页管理:都围绕 page cache 展开

对 page cache 来说,“页面属于谁”很清楚:属于某个文件对象(mapping 指向 inode 的 address_space)。

swap cache:swap entry 的缓存

swap cache 的定位也很明确:缓存匿名页与 swap slot 的对应关系,核心价值是:

  1. 并发合流:多个线程同时 fault 同一个 swap entry,只做一次 IO

  2. 预读落脚点:swap readahead 把邻居页读回来后,必须有地方存(否则读回就白读)

最容易误解的地方是:swap cache 也使用 address_space + XArray(i_pages)作为实现外壳,并且也会用 find_get_page() 之类通用接口去查页。

但 swap cache 的 key 并不是 “文件 mapping + 文件页号”,而是:

swap cache key = (swap_mapping(type), swp_offset)

其中 swap_mapping(type) 不是 inode 的 mapping,而是内核为 swap 伪造出来的一组 mapping——通常叫 swapper_space / swapper_spaces[](对每个 swap type 一组)。

它的含义不是“假的不重要”,而是:“它不是文件系统意义上的 mapping,只是为了复用 page cache 的组织结构”。

两者的差异

对比维度

Page Cache

Swap Cache

缓存内容

文件的数据块

被换出的匿名内存页

host 指针

指向文件的 struct inode

NULL

索引键

文件内的偏移量 (page index)

交换条目中的偏移量 (swp_offset(entry))

操作方法集 (a_ops)

ext4_aops, xfs_aops 等文件系统特定操作

swap_aops (只处理交换区回写)

主要目的

加速文件读写,减少磁盘I/O

避免重复换入写时复制时数据错乱

swap 预读机制

到目前为止,我们已经把 do_swap_page() 的输入(swap entry)和它依赖的缓存体系(swap cache)讲清楚了。下一步就是解释 do_swap_page() 里最“像魔法”的一段:swap 预读(swap readahead / swapin readahead)

很多人第一次读到 swapin_readahead() 会疑惑:

  • “我只是缺一页,为什么内核要读一堆?”

  • “它怎么决定读多少?读哪些 offset?”

  • “预读到底有没有命中?怎么证明它真的有效?”

这一节我们用下面俩方面简单聊聊:

  1. 算法直觉:为什么 swap-in 值得预读

  2. 实现机制:cluster 对齐 + 动态窗口

为什么 swap-in 预读很重要

swap-in fault 的痛点在于:它通常属于 major fault,意味着要等 IO 才能把页装回 PTE。线程会在缺页路径上阻塞,表现为:

  • 进程卡顿(用户态直接感知)

  • 系统 latency 上升

  • 大量进程同时 swap-in 时容易抖动甚至出现 hung task 报警

而很多 workload 访问具有局部性:
当你访问 swap entry offset = N 的页时,紧接着访问 offset = N±1 的概率不低(尤其是栈、堆的顺序触碰、或相邻对象分配)。

于是 swap readahead 的设计目标就很清晰:

把“后续可能发生的多个同步缺页”提前变成“一次(或少数几次)批量读回”,提高后续缺页命中 swap cache 的概率,减少 major fault 次数。

读回来的邻居页先进入 swap cache

预读不是把页“直接映射进进程页表”。它做的是:

  • 根据当前 entry 选择一个 offset 范围(邻居页)

  • 把这些页异步读回到内存

  • 插入 swap cache

  • 等真正访问发生 fault 时,lookup_swap_cache(entry) 命中,从 swap cache 拿到 page,回填 PTE

所以 swap readahead 与 swap cache 是一个闭环:

fault entry N
   |
   v
swapin_readahead 读入 [start..end] 多个 offset 到 swap cache
   |
   v
后续 fault entry N+1/N+2 ...
   |
   v
lookup_swap_cache 命中 -> 不用再发 IO -> 直接回填 PTE

cluster 对齐

swap readahead 的核心思想之一是 cluster(页簇)。直觉上:

  • swap 空间是按页排列的 slot 数组

  • 预读最好读连续的一段(更利于 block IO 合并、更符合局部性)

  • 于是把若干页(2 的幂)当作一个窗口,按边界对齐

在代码里常见的套路是:

  • nr_pages = swapin_nr_pages(...) 决定本次窗口大小(通常是 2 的幂)

  • mask = nr_pages - 1

  • start = offset & ~mask(向下对齐窗口起点)

  • end = offset | mask(向上扩展窗口终点)

以 offset=13、窗口 8 页为例的对齐

窗口大小 = 8 页 -> mask = 0b111
offset = 13 (0b1101)

start = offset & ~mask = 0b1101 & 0b1000 = 0b1000 = 8
end   = offset |  mask = 0b1101 | 0b0111 = 0b1111 = 15

=> 预读范围 [8 .. 15](一整个对齐窗口)

do_swap_page

到这一节为止,我们已经准备好了读 do_swap_page() 所需的全部前置知识:

  • swap entry:语义是 (type, offset),实现是 swp_entry_t.val 位域打包

  • swap cache:用虚构 mapping(swapper_space[type])+ swp_offset 索引,服务并发合流与预读

  • swap readahead:按 cluster 对齐批量读入 swap cache

现在我们进入主菜: mm/memory.c: do_swap_page() ,演示代码以linux-5.15内核

为了方便读者跟随源码,我们把它拆成 8 个阶段:

  1. 入口并发复核:PTE 还没变吗?

  2. 解码 entry,先处理 non_swap_entry

  3. pin 住 swap device:防 swapoff

  4. 查 swap cache:命中就合流,miss 则触发读盘/预读

  5. 锁页并等待 IO:得到稳定且 uptodate 的 page

  6. 重新锁 PTE 并二次复核:还没被别人装好吗?

  7. set_pte + rmap/LRU + 计数:把页“真正装回去”

  8. swap slot 生命周期:swap_free +(可选)try_to_free_swap,最后更新 MMU cache/处理 WP

pte_unmap_same

进入 do_swap_page() 时,pte 可能已被映射(mapped)但尚未加锁。缺页异常是高并发场景:两个线程可能同时 fault 同一个地址,或一个线程 fault 时另一个线程已经把 PTE 修复掉了。

所以第一件事必须是:

  • “我手里这份 orig_pte 快照还有效吗?”

  • 如果无效,立刻退出,让上层重新处理

这就是 pte_unmap_same() 的意义:它用来验证“当前 PTE 仍等于 orig_pte”,否则说明现场已变化,继续 swap-in 会造成覆盖/计数错乱。

static inline int pte_unmap_same(struct mm_struct *mm, pmd_t *pmd,
				pte_t *page_table, pte_t orig_pte)
{
	int same = 1;
#if defined(CONFIG_SMP) || defined(CONFIG_PREEMPTION)
	if (sizeof(pte_t) > sizeof(unsigned long)) {
		spinlock_t *ptl = pte_lockptr(mm, pmd);
		spin_lock(ptl);
		same = pte_same(*page_table, orig_pte);
		spin_unlock(ptl);
	}
#endif
	pte_unmap(page_table);
	return same;
}

pte_to_swp_entry+non_swap_entry

	entry = pte_to_swp_entry(vmf->orig_pte);  ///获取换出页标识符
	if (unlikely(non_swap_entry(entry))) {   ///非换出页标识符,处理迁移页面,复用swap机制
		if (is_migration_entry(entry)) {
			migration_entry_wait(vma->vm_mm, vmf->pmd,
					     vmf->address);
		} else if (is_device_exclusive_entry(entry)) {
			vmf->page = pfn_swap_entry_to_page(entry);
			ret = remove_device_exclusive_entry(vmf);
		} else if (is_device_private_entry(entry)) {
			vmf->page = pfn_swap_entry_to_page(entry);
			ret = vmf->page->pgmap->ops->migrate_to_ram(vmf);
		} else if (is_hwpoison_entry(entry)) {
			ret = VM_FAULT_HWPOISON;
		} else {
			print_bad_pte(vma, vmf->address, vmf->orig_pte, NULL);
			ret = VM_FAULT_SIGBUS;
		}
		goto out;
    }

entry = pte_to_swp_entry(orig_pte):把 PTE 的特殊编码解码成 swp_entry_t(仅一个 val,type/offset 是位域)。
然后 non_swap_entry(entry) 分流:

  • migration entry:页正在迁移,需要等待或走迁移路径

  • device private/exclusive:需要迁移回 RAM

  • hwpoison:坏页,返回 HWPOISON/SIGBUS 语义

为什么必须在这里处理?因为这些 entry 长得像 swap:它们都占用 “non-present entry 编码空间”。如果你把它们当 swap 去读盘,逻辑就彻底错了。

is_migration_entry场景

迁移条目是一种特殊的 swap entry,它被内核用来临时表示一个正在被迁移的页面。

static inline int is_migration_entry(swp_entry_t entry)
{
	return unlikely(swp_type(entry) == SWP_MIGRATION_READ ||
			swp_type(entry) == SWP_MIGRATION_WRITE);
}
  • SWP_MIGRATION_READ:表示页面正在被读取(即源页面正在被拷贝)

  • SWP_MIGRATION_WRITE:表示页面即将被写入(即目标位置准备接收数据)

它们与真正的 swap 条目共享 swp_entry_t 类型,但通过 swp_type() 值区分(值大于等于 MAX_SWAPFILES)。

migration_entry_wait() 的工作流程

void __migration_entry_wait(struct mm_struct *mm, pte_t *ptep,
				spinlock_t *ptl)
{
	pte_t pte;
	swp_entry_t entry;
	struct page *page;

	spin_lock(ptl);
	pte = *ptep;
	if (!is_swap_pte(pte))
		goto out;
    // pte提取entry
	entry = pte_to_swp_entry(pte);
    // 再次判断是否是迁移条目
	if (!is_migration_entry(entry))
		goto out;
    // 从迁移条目提取页面
	page = pfn_swap_entry_to_page(entry);
	page = compound_head(page);

	/*
	 * Once page cache replacement of page migration started, page_count
	 * is zero; but we must not call put_and_wait_on_page_locked() without
	 * a ref. Use get_page_unless_zero(), and just fault again if it fails.
	 */
	if (!get_page_unless_zero(page))
		goto out;
	pte_unmap_unlock(ptep, ptl);
    // 等待迁移完成
	put_and_wait_on_page_locked(page, TASK_UNINTERRUPTIBLE);
	return;
out:
	pte_unmap_unlock(ptep, ptl);
}

void migration_entry_wait(struct mm_struct *mm, pmd_t *pmd,
				unsigned long address)
{
	spinlock_t *ptl = pte_lockptr(mm, pmd);
	pte_t *ptep = pte_offset_map(pmd, address);
	__migration_entry_wait(mm, ptep, ptl);
}

is_device_exclusive_entry和is_device_private_entry场景

除了传统的swap条目和迁移条目,还有两类重要的设备内存条目需要特殊处理:设备私有条目设备独占条目。

static inline bool is_device_exclusive_entry(swp_entry_t entry)
{
	return swp_type(entry) == SWP_DEVICE_EXCLUSIVE_READ ||
		swp_type(entry) == SWP_DEVICE_EXCLUSIVE_WRITE;
}

static inline bool is_device_private_entry(swp_entry_t entry)
{
	int type = swp_type(entry);
	return type == SWP_DEVICE_READ || type == SWP_DEVICE_WRITE;
}

get_swap_device

/*
 * Check whether swap entry is valid in the swap device.  If so,
 * return pointer to swap_info_struct, and keep the swap entry valid
 * via preventing the swap device from being swapoff, until
 * put_swap_device() is called.  Otherwise return NULL.
 *
 * Notice that swapoff or swapoff+swapon can still happen before the
 * percpu_ref_tryget_live() in get_swap_device() or after the
 * percpu_ref_put() in put_swap_device() if there isn't any other way
 * to prevent swapoff, such as page lock, page table lock, etc.  The
 * caller must be prepared for that.  For example, the following
 * situation is possible.
 *
 *   CPU1				CPU2
 *   do_swap_page()
 *     ...				swapoff+swapon
 *     __read_swap_cache_async()
 *       swapcache_prepare()
 *         __swap_duplicate()
 *           // check swap_map
 *     // verify PTE not changed
 *
 * In __swap_duplicate(), the swap_map need to be checked before
 * changing partly because the specified swap entry may be for another
 * swap device which has been swapoff.  And in do_swap_page(), after
 * the page is read from the swap device, the PTE is verified not
 * changed with the page table locked to check whether the swap device
 * has been swapoff or swapoff+swapon.
 */
struct swap_info_struct *get_swap_device(swp_entry_t entry)
{
	struct swap_info_struct *si;
	unsigned long offset;
    // 排除空条目
	if (!entry.val)
		goto out;
    //从swap_entry中获取到swap_info_struct
	si = swp_swap_info(entry);
	if (!si)
		goto bad_nofile;
	if (!percpu_ref_tryget_live(&si->users))
		goto out;
	/*
	 * Guarantee the si->users are checked before accessing other
	 * fields of swap_info_struct.
	 *
	 * Paired with the spin_unlock() after setup_swap_info() in
	 * enable_swap_info().
	 */
    // 内存屏障,保证内存访问的顺序性
	smp_rmb();
    // 偏移量边界检查
	offset = swp_offset(entry);
	if (offset >= si->max)
		goto put_out;

	return si;
bad_nofile:
	pr_err("%s: %s%08lx\n", __func__, Bad_file, entry.val);
out:
	return NULL;
put_out:
	percpu_ref_put(&si->users);
	return NULL;
}

si = get_swap_device(entry) 并不是随手取个指针,它的语义是:

在整个 swap-in 过程中,防止 swapoff 把 entry 所属 swap area 撤销。

如果 swapoff 在等待 IO 的期间发生,而还继续使用这个 entry 的 type/offset 去映射 IO 或更新计数,很容易产生悬空引用与不可预期错误。

因此这一步必须早,而且必须在退出路径里配对 put_swap_device(si)

查 swap cache

lookup_swap_cache(entry, ...)(swapper_space[type], swp_offset) 为 key 查页:

  • hit:说明别的线程已经读回(或 readahead 已读回),你复用它

  • miss:需要发起 IO(或触发 readahead)

这一行是“避免重复 IO”的关键。

未命中后也有两种情况:一种是同步 IO,另一种是常规路径swapin_readahead

同步 IO 分支

		if (data_race(si->flags & SWP_SYNCHRONOUS_IO) &&
		    __swap_count(entry) == 1) { ///需要启动慢速IO操作,此时根据局部性原理,还可做预取动作来优化性能
			/* skip swapcache */
			page = alloc_page_vma(GFP_HIGHUSER_MOVABLE, vma,    ///分配page
							vmf->address);
			if (page) {
				__SetPageLocked(page);
				__SetPageSwapBacked(page);

				if (mem_cgroup_swapin_charge_page(page,
					vma->vm_mm, GFP_KERNEL, entry)) {
					ret = VM_FAULT_OOM;
					goto out_page;
				}
				mem_cgroup_swapin_uncharge_swap(entry);

				shadow = get_shadow_from_swap_cache(entry);
				if (shadow)
					workingset_refault(page, shadow);

				///page加入swap_cache
				lru_cache_add(page);

				/* To provide entry to swap_readpage() */
				set_page_private(page, entry.val);

				///从swap文件读取数据到page
				swap_readpage(page, true);
				set_page_private(page, 0);
			}
		}

当交换设备支持同步IO且页面只有一个引用时,内核会绕过Swap Cache,直接分配页面并从交换设备读取数据。这种优化减少了锁竞争和内存开销,特别适用于高速存储设备。

当 swap 设备是同步 IO 且 swapcount==1 时:

  • 分配 page

  • 锁页

  • memcg charge

  • 临时把 entry.val 塞进 page_private(为了让 swap_readpage 知道读哪个 entry)

  • swap_readpage(...do_poll=true) 直接读

  • 再把 page_private 清回 0

常规分支

			page = swapin_readahead(entry, GFP_HIGHUSER_MOVABLE,  ///从swap文件读取数据到page
						vmf);
			swapcache = page;

这条路更常见:

  • 以当前 entry 为中心按 cluster 对齐

  • 把一段范围内的页读入 swap cache

  • 返回目标页(通常来自 swap cache)

这就把单页缺页升级成“簇级预读”的机会,后续 fault 命中 swap cache 的概率更高。

VM_FAULT_MAJOR

		/* Had to read the page from swap area: Major fault */
		ret = VM_FAULT_MAJOR;  ///需要启动慢速IO操作,标记为主缺页

只要发生了真正的 IO,do_swap_page() 就会把返回值带上 VM_FAULT_MAJOR

锁页与等待 IO

拿到 page 之后不能马上 set_pte,因为:

  • IO 可能还没完成(PageUptodate 未置位)

  • 并发路径可能在操作同一页

  • 你必须在稳定状态下检查 page 是否仍对应 entry、是否 uptodate

所以要锁页。

	locked = lock_page_or_retry(page, vma->vm_mm, vmf->flags);

lock_page_or_retry 的 “retry” 分支则处理了“当前不适合阻塞/持锁”的情况:释放 mmap_lock,稍后重试 fault,从而避免某些死锁/长等待。

static inline int lock_page_or_retry(struct page *page, struct mm_struct *mm,
				     unsigned int flags)
{
	might_sleep();
	return trylock_page(page) || __lock_page_or_retry(page, mm, flags);
}

int __lock_page_or_retry(struct page *page, struct mm_struct *mm,
			 unsigned int flags)
{
	if (fault_flag_allow_retry_first(flags)) {
		/*
		 * CAUTION! In this case, mmap_lock is not released
		 * even though return 0.
		 */
		if (flags & FAULT_FLAG_RETRY_NOWAIT)
			return 0;

		mmap_read_unlock(mm);
		if (flags & FAULT_FLAG_KILLABLE)
			wait_on_page_locked_killable(page);
		else
			wait_on_page_locked(page);
		return 0;
	}
	if (flags & FAULT_FLAG_KILLABLE) {
		int ret;

		ret = __lock_page_killable(page);
		if (ret) {
			mmap_read_unlock(mm);
			return 0;
		}
	} else {
		__lock_page(page);
	}
	return 1;

}

二次复核

在你等待/锁页期间,以下事情可能发生:

  • try_to_free_swap 把页从 swap cache 踢掉

  • swapoff 相关路径改变了 entry/slot 的状态

  • 拿到的 page 已经不再对应原 entry

因此必须确认:

  • 这个 page 仍在 swap cache

  • 它仍对应当初看到的 entry(page_private(page) == entry.val

否则必须退出走清理路径,不能继续 set_pte。

	/*
	 * Make sure try_to_free_swap or reuse_swap_page or swapoff did not
	 * release the swapcache from under us.  The page pin, and pte_same
	 * test below, are not enough to exclude that.  Even if it is still
	 * swapcache, we need to check that the page's swap has not changed.
	 */
	if (unlikely((!PageSwapCache(page) ||
			page_private(page) != entry.val)) && swapcache)
		goto out_page;

这里原文的英文注释中也标明了。

	vmf->pte = pte_offset_map_lock(vma->vm_mm, vmf->pmd, vmf->address,
			&vmf->ptl);
	if (unlikely(!pte_same(*vmf->pte, vmf->orig_pte)))
		goto out_nomap;
	if (unlikely(!PageUptodate(page))) {
		ret = VM_FAULT_SIGBUS;
		goto out_nomap;
	}

在前面做了 IO、等待、锁页,期间 PTE 锁并不一直被持有。可能发生:

  • 另一个线程更快完成 swap-in,并已经 set_pte

  • 页被迁移/回收路径改变了 PTE

因此必须:

  1. pte_offset_map_lock():重新拿到 PTE 锁

  2. pte_same(*pte, orig_pte):确认 PTE 仍然是你最初看到的 swap entry

如果不相同,说明别人已经处理过,你只要释放当前 page 引用即可,不能覆盖。

然后还要检查 PageUptodate(page):IO 是否成功。失败就走 VM_FAULT_SIGBUS/错误语义。

set_pte + rmap/LRU + 计数

这是 do_swap_page() 的“装页”核心区域,也是顺序最不能乱的地方。

更新计数:MM_ANONPAGES / MM_SWAPENTS

    inc_mm_counter_fast(vma->vm_mm, MM_ANONPAGES);   ///匿页也计数增加
	dec_mm_counter_fast(vma->vm_mm, MM_SWAPENTS);    ///swap页面技术减少

swap-in 本质是:

  • 匿名页回到内存:匿名页计数 +1

  • swap entry 作为“在 swap 中的占位”减少:swapents -1

这类计数影响 vmstat、回收策略与系统观察。

构造 present PTE

pte = mk_pte(page, vma->vm_page_prot);           ///拼接页表项

struct page 转成 PTE,并带上 VMA 的属性(可执行/可写等由后续进一步调整)。

reuse_swap_page

	///优化,reuse_swap_page,只被当前vma使用,直接改为可写,不做写时复制
	if ((vmf->flags & FAULT_FLAG_WRITE) && reuse_swap_page(page, NULL)) {
		pte = maybe_mkwrite(pte_mkdirty(pte), vma);
		vmf->flags &= ~FAULT_FLAG_WRITE;
		ret |= VM_FAULT_WRITE;
		exclusive = RMAP_EXCLUSIVE;
	}

如果 fault 原因包含写(FAULT_FLAG_WRITE),内核会尝试:

  • 如果这页可以独占(swapcount/mapcount 合适),则直接把 PTE 做成可写 dirty

  • 同时清掉 FAULT_FLAG_WRITE,表示这次 fault 已经满足了写权限

  • ret |= VM_FAULT_WRITE 并设置 exclusive = RMAP_EXCLUSIVE

这样可以避免之后再次触发 do_wp_page(),减少一次 fault。

set_pte_at + arch_do_swap_page

	set_pte_at(vma->vm_mm, vmf->address, vmf->pte, pte);   ///填充页表
	arch_do_swap_page(vma->vm_mm, vma, vmf->address, pte, vmf->orig_pte);
	vmf->orig_pte = pte;

set_pte_at() 把 swap PTE 替换成 present PTE。随后 arch_do_swap_page() 处理架构相关的收尾(如 cache/TLB 相关细节),把通用逻辑与架构逻辑分隔开。

rmap/LRU

	/* ksm created a completely new copy */
	if (unlikely(page != swapcache && swapcache)) {
		page_add_new_anon_rmap(page, vma, vmf->address, false);
		lru_cache_add_inactive_or_unevictable(page, vma);
	} else {
		do_page_add_anon_rmap(page, vma, vmf->address, exclusive);  ///加入rmap
	}

页表装好了还不够:匿名页必须建立反向映射(rmap),并进入 LRU,才能被回收系统正确管理。

  • 如果 page != swapcache(KSM copy),要用“新页”路径:page_add_new_anon_rmap + lru_cache_add

  • 否则走常规:do_page_add_anon_rmap

这一步决定回收系统能否正确定位“这页属于哪个 VMA/进程”。

swap_free + try_to_free_swap

	swap_free(entry); ///递减交换页槽的引用计数

///mem_cgroup_swap_full:交换页槽使用超过总数的1/2,或者vma被锁内存,尝试释放swap页面
	if (mem_cgroup_swap_full(page) ||
	    (vma->vm_flags & VM_LOCKED) || PageMlocked(page))
		try_to_free_swap(page);   ///引用计数为0,尝试释放swap cache

swap_free(entry) 释放 swap slot 的引用计数。
之后(在一些条件满足时)try_to_free_swap(page) 尝试把该页从 swap cache 移除,进一步释放 swapcache 关联。

顺序必须是:先 swap_free,再 try_to_free_swap。
否则 slot 引用计数没放下来,try_to_free_swap 往往不会成功。

写保护二次处理

	if (vmf->flags & FAULT_FLAG_WRITE) {   
		ret |= do_wp_page(vmf);            ///写时复制
		if (ret & VM_FAULT_ERROR)
			ret &= VM_FAULT_ERROR;
		goto out;
	}

如果一开始是写 fault,但 reuse_swap_page() 没抢到独占,FAULT_FLAG_WRITE 仍然存在。此时流程是:

  • 先把页以合适权限装回(通常只读)

  • 然后调用 do_wp_page() 处理写保护(COW/权限升级)

否则(写 fault 已被满足),直接 update_mmu_cache() 收尾。

这一篇感觉写的有点乱,但是因为一些知识点如果不说清楚,聊do_swap_page就又说不清楚,可能等后面聊到swap的时候,这一篇还会重新写一篇来补充!