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

我们在上一篇[linux内存管理] 第040篇 文件映射与匿名映射中已经聊过了匿名映射以及文件映射,所以这一篇文章咱们聊一聊这剩余的两个函数do_swap_page
do_swap_page 在缺页异常里的位置
如果把缺页异常当作一条“从硬件异常到把页装进页表”的流水线,那么 do_swap_page() 不是这条流水线的入口,也不是出口,它处在 缺页处理的“分类分流”之后:当内核确认这次 fault 对应的 PTE 不是 present 的物理页映射,而是一个 swap entry(或类似的特殊 entry) 时,才会进入 do_swap_page()。
在 handle_pte_fault() 中,常见分支大致可以按“PTE 的形态”理解:
PTE none(完全无映射)
例如匿名页首次访问:走
do_anonymous_page()或者继续走文件页 fault 的路径(取决于 VMA 属性)
PTE present(已映射)但 fault 发生
常见是写时复制(COW)或写保护:走
do_wp_page()也可能是权限/dirty/young 等导致的处理(不同场景不同)
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解出 typeswp_offset(entry):从entry.val解出 offsetswp_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 再发 IOmmap文件映射缺页:走filemap_fault(),优先命中 page cache写回、回写一致性、脏页管理:都围绕 page cache 展开
对 page cache 来说,“页面属于谁”很清楚:属于某个文件对象(mapping 指向 inode 的 address_space)。
swap cache:swap entry 的缓存
swap cache 的定位也很明确:缓存匿名页与 swap slot 的对应关系,核心价值是:
并发合流:多个线程同时 fault 同一个 swap entry,只做一次 IO
预读落脚点: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 的组织结构”。
两者的差异
swap 预读机制
到目前为止,我们已经把 do_swap_page() 的输入(swap entry)和它依赖的缓存体系(swap cache)讲清楚了。下一步就是解释 do_swap_page() 里最“像魔法”的一段:swap 预读(swap readahead / swapin readahead)。
很多人第一次读到 swapin_readahead() 会疑惑:
“我只是缺一页,为什么内核要读一堆?”
“它怎么决定读多少?读哪些 offset?”
“预读到底有没有命中?怎么证明它真的有效?”
这一节我们用下面俩方面简单聊聊:
算法直觉:为什么 swap-in 值得预读
实现机制: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 - 1start = 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 个阶段:
入口并发复核:PTE 还没变吗?
解码 entry,先处理 non_swap_entry
pin 住 swap device:防 swapoff
查 swap cache:命中就合流,miss 则触发读盘/预读
锁页并等待 IO:得到稳定且 uptodate 的 page
重新锁 PTE 并二次复核:还没被别人装好吗?
set_pte + rmap/LRU + 计数:把页“真正装回去”
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
因此必须:
pte_offset_map_lock():重新拿到 PTE 锁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 cacheswap_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的时候,这一篇还会重新写一篇来补充!