THP阅读分析[WIP]
本文由于工作优先级原因暂时搁置,专心转向zswap
// TODO
- [x] 配置源码阅读环境 第三部分-Linux源码阅读环境
- [ ] 理清如下几个先决问题
- [x] 什么是THP?THP要做什么?
- [ ] 为什么在虚拟化和嵌套页表的情况下,THP对TLB和TLB miss的优化更明显
- [x] THP和HP的相关性与区别
- [ ] THP和HugeTLB的相关性与区别
- [ ] 梳理THP相关核心流程
- [x] khugepaged守护进程如何工作?
- [x] khugepaged与huge_memory关系如何?
- [ ] huge_memory如何管理大页面,更一般的,理清下面两个问题
- [ ] 一次内存访问中发生了什么
- [ ] 一次页错误中发生了什么
- [ ] 梳理THP相关核心数据结构
- [ ] 理清如下几个逻辑问题
- [ ] 一个mm是管理一个进程内存空间的数据结构?
核心代码:
huge_memory.c
khugepaged.c
官方文档:
Transparent Hugepage Support
概述 (暂时性的,不一定正确)
- Hugepage:现代操作系统使用的页式内存管理通常以4K为一个标准页面。在内存越来越大的今天,4K大小的页面对于使用大量内存的应用会产生一些问题,即大量产生的缺页中断。
大量的缺页中断会带来两个问题
1. 进入内核的次数更多,带来更多的切换上下文开销。
2. 带来更多的TLB Miss,这也是Hugepage优化的主要问题。
对于问题a,大页带来的性能优化可能较低。一方面更大的页面要求在一次页错误中复制更多的内容。另一方面,进出内核的频率降低只在内存映射的生命周期内第一次访存时才重要。
对于问题b,因为TLB的容量是有限的,更大的页面也代表着更少的页面,这会带来两点好处
1. TLB miss会变快
2. 一个TLB表项可以映射更大的内存,来减少TLB miss
- THP:THP和Hugepage的区别是:Hugepage是一个操作系统管理的、用户显式请求的机制,THP是操作系统自动的,对用户透明的行为。THP无需用户修改代码,具有更高的普适性。
- khugepaged负责合并,huge_memory.c中的函数负责管理
- 透过现象看本质,这是一种用单页面大小和扫描时间换系统整体效率的机制
部分概念/缩写介绍[WIP]
页表条目
现在的Linux内核采用四级页表目录的方式,分别为
- PGD :Page Global Directory
- PUD :Page Upper Directory
- PMD :Page Middle Directory
- PTE :Page Table Entry
- 来复习下基础:每个进程都有自己独立的PGD,虚拟地址的从高到底的k位(典型的是9,最低12位是页框内地址,共48位的地址)分别用来索引PGD(在PGD中找到这个地址对应的PUD)、索引PUD、索引PMD、索引PTE进而找到自己对应的物理页框,最低几位在该页框中索引具体的内存地址。PUD、PMD和PTE的最低位是存储是否有效的标志位。其中PTE指示页框的是48...12这36位。
THP运转流程-合并
概述
在系统启动时,内核会启动khugepaged守护进程,该进程启动一个核心循环,扫描系统中的页面,检测是否有多个连续的页面可以合并为一个透明大页(事实上,我认为不是任意连续2M页面,而必须是已经存在的PMD表项所对应的2M页面,待确认)。当它发现了可以合并的页面后进行大量的检查操作,然后进入合并函数。先进行申请和double-check,移除CPU中的TLB对应表项,复制巨页内容,更新相关参数。
khugepaged如何初始化
在hugememory.c中,有一个被subsys_initcall注册的初始化函数:hugepage_init, 该函数调用了两个khugepaged相关的函数:khugepaged_init 和 start_stop_khugepaged,在此处,我们暂时先不关心其他的初始化流程,把它们放到管理中去考虑,先只看khugepaged相关的部分
khugepaged_init
申请一段slab内存,初始化四个参数
- 常量 HPAGE_PMD_NR 根据它的定义,我推测它在数量上和一个HPAGE所含的一般页面数量相等(512)
- khugepaged_pages_to_scan : 每次扫描页面的数量,默认为 512 * 8
- khugepaged_max_ptes_none:最多有多少个页表项是空页面(存疑)
- khugepaged_max_ptes_swap:最多有多少个页面在swap中未被映射,默认是512/8
- khugepaged_max_ptes_shared:最多有多少页面是被共享的,默认是512/2
start_stop_khugepaged
- 获取互斥锁,检测hugepage是否开启了,若开启了则检查static变量khugepaged_thread,存在则关闭该线程
- 启动一个内核线程,目标函数为khugepaged函数,该函数为khugepaged扫描的核心循环
- 若有错误,清理并释放锁
khugepaged如何扫描
khugepaged
- 设置一些状态,把当前线程优先级变低,进入循环
如果收到KTHREAD_SHOULD_STOP,则退出
- 调用khugepaged_do_scan函数
调用khugepaged_wait_work函数,进入等待
- 根据khugepaged_scan_sleep_millisecs,转换成中断次数,进入等待
- 获取自旋锁,进行一些清理工作,释放锁
khugepaged_do_scan
- 该函数有一个参数:khugepaged_collapse_control,只透传给下层
初始化一些变量
- progress:记录THP扫描进度
- pages:读取init中初始化的khugepaged_pages_to_scan
- wait:布尔量,用于下面过程的重试
- result:记录扫描结果的状态
- 调用lru_add_drain_all函数,把脏页从LRU列表干掉(加到写回缓冲区)
进入一个无限循环,降低自己的优先级
- 检查是否要退出
- 获取一个自旋锁
- 如果当前kugepaged_scan的mm_slot字段为空,累加“pass_through_head”计数器
- 如果kugepaged_scan的mm_head不为空,且pass_through_head < 2(我理解就是第一次进入,记录一个TODO,为什么要把0的情况算进去,即什么情况下存在刚进入do_scan函数但是mm_slot不为空)调用kugepaged_scan_mm_slot函数,并把结果累加给progress变量,更新result变量
kugepaged_scan_mm_slot
static unsigned int khugepaged_scan_mm_slot(unsigned int pages, int *result,
struct collapse_control *cc)
__releases(&khugepaged_mm_lock)
__acquires(&khugepaged_mm_lock)
该函数是scan步骤的关键函数,它接受的参数包括:
- pages:要扫描的页数
- result:结果指针
- cc:透传来的 collapse_control结构
该函数首先进行一些初始化与验证操作,包括对参数与锁的断言,对 result 的初始化
检测是否已经有正在进行的扫描,如果有,获取相应的 mm_slot和 slot(扫描当前头部未扫描完的进程),如果没有,初始化一组新的 slot 和 mm_slot(扫描下一个进程)
khugepaged如何合并
kugepaged_collapse_pte_mapped_thps
进行一些验证操作与锁的获取
便利当前 slot 的所有 Hugepage 的页表项,调用collapse_pte_mapped_thp函数对这些页表项进行合并操作。
collapse_pte_mapped_thp
int collapse_pte_mapped_thp(struct mm_struct *mm, unsigned long addr,
bool install_pmd)
该函数有三个参数
● mm:将要被聚合的进程地址空间
● addr:要发生聚合的地址
● install_pmd:布尔,决定是否应当设置一个 PMD 表项
该函数检测是否一个 PMD 中的所有 PTE 表项都指向了正确的 THP,如果是的话,撤销相关的页表项,这样 THP 可以触发一个新的页错误,使得使用一个 huge PMD 映射这个 THP
把地址按掩码对齐,获取当前地址对应的 vma,获取锁。
调用find_pmd_or_thp_or_none 查找页表中对应当前地址的 PMD 或是 THP 或是空指针。如果当前页面已经被 PMD 映射,返回。如果所有检测都没有命中,返回 SCAN_SUCCEED。
// TODO:这些检测都是什么
检查获取的 vma 是否正确,如果不正确,返回 VMA_CHECK,表示需要检查虚拟内存区域
到此为止,我门已经成功的把页缓存中的本地页面替换成了单个的 hugepage,如果 mm 触发缺页错误,当前 hugepage 就会被一个 PMD 映射,而不考虑 sysfs 中对 THP 的设置是什么样的。
检查是否存在使用 userfaultfd_wp的情况,如果是,返回
通过 find_lock_page 尝试获取页面,且验证页面是一个 Hugepage,并且检测页面是否为一个复合页面。如果不是 Hugepage 或者页面复合阶数和 HPAGE_PMD_ORDER不相等,则 drop_hpage。
接下来操作页表,首先获取当前 vma 的锁、当前文件映射的锁、当前虚拟地址对应 PTE 的锁。
STEP1:检测是否所有的已映射页表项指向了期望的 Hugepage
● 遍历当前 PMD 对应的所有 PTE
● 如果当前 PTE 是空,跳过
● 如果当前 PTE 没有物理页映射,发生了非法情况,终止(swapped out)
● 获取 PTE 指向的物理页面
● 检测是否是设备内存,如果是,WARN,并设置页面指针为 NULL
● 检测 PTE 指向的页面和期望的巨页对应的小页面是否一致,不一致则终止
● 累加计数器
STEP2:对小页面的反向映射进行调整
● 遍历当前 HugePage 的页表项
● 如果当前 PTE 是空的,跳过
● 获取当前物理页,检测是否是设备内存,若是 WARN 并 abort
● 调用 page_remove_rmap,从页的反向映射表中移除页面与 VMA 的映射关系
● 解锁 PTE 的映射
STEP3、4:设置正确的引用计数、移除 PTE 条目
● 如果 count 不是 0,说明签名的步骤中找到了一些映射到 hpage 的 PTE 条目,折叠后会删除 PTE,所以更新 hpage 的引用计数、把 vma 的 mm 计数器也减掉相应的数量
● 如果有匿名 vma,获取锁,然后调用 collapse_and_free_pmd折叠并释放 haddr 处的 PMD 条目
STEP5:
● 根据 install_pmd 参数的情况,决定是否要安装 PMD,若 PMD 安装成功或者不需要,则返回 SCAN_SUCCEED
collapse_and_free_pmd
hugepage_vma_check
khugepaged的重要数据结构
khugepagd_scan
struct khugepaged_scan {
struct list_head mm_head;
struct khugepaged_mm_slot *mm_slot;
unsigned long address;
};
这个数据结构是一个指引扫描的“光标”,跟踪khugepaged扫描时的状态。该数据结构只会有一个全局实例。
- mm_head:要扫描的mm的头部,维护需要扫描内存区的链表
- mm_slot:指向当前正在扫描的kuhugepaged_mm_slot
- address:是mm_slot内下一个即将被扫描的地址
khugepaged_mm_slot
struct khugepaged_mm_slot {
struct mm_slot slot;
/* pte-mapped THP in this mm */
int nr_pte_mapped_thp;
unsigned long pte_mapped_thp[MAX_PTE_MAPPED_THP];
};
这个数据结构用来跟踪正在扫描的每个内存区域
- slot:依赖的一个外部数据结构,实现了从mm到mm_slot的哈希查找
- nr_pte_mapped_thp:表示在该mm中已经被映射的THP的数量
- pte_mapped_thp:存放该mm中已映射的THP的地址
在我的理解中,khugepaged_scan指导scan工作,而其中的mm_slot指向当前正在被扫描的那个进程的mm_slot,(一个进程通常只有一个kugepaged_mm_slot, 暂不确定什么情况下会有多个)
评论 (0)