一文几下​Linux内核透明巨型页支持

描述

==目标 == 

处理大内存的性能关键计算应用程序工作集已经运行在libhugetlbfs之上,然后依次运行hugetlbfs。透明的巨型页面支持是另一种使用大页为虚拟内存提供大页支持的方法, 该支持自动提升和降低页面大小和没有hugetlbfs的缺点。

目前它只适用于匿名内存映射和tmpfs/shmem。但是将来它可以扩展到其他文件系统。实际上,已经支持了只读的文件映射

应用程序运行更快的原因有两个的因素。第一个因素几乎完全无关紧要,事实并非如此,这很重要,因为它也有缺点在页错误中需要更大的清除页拷贝有潜在的负面影响。第一个因素是采取每个2M的虚拟区域都有一个页面错误(将内核的进入/退出频率减少512倍)。这的生命周期中,一个内存映射只有第一次访问内存。

第二个更持久,也更重要因子将会影响应用程序的运行时整个内存的所有后续访问。第二个因素有两个组件: 1)TLB miss将运行更快(特别是使用嵌套分页的虚拟化,但几乎总是在没有虚拟化的裸系统上。2)单个TLB条目将是映射更大数量的虚拟内存,从而减少TLB miss次数。使用虚拟化和嵌套分页只有KVM和Linux客户端同时支持映射更大的TLB正在使用大页面,但显著的速度已经发生了,如果其中一个使用大页面只是因为TLB miss会跑得更快。

== 设计 ==

“优雅回退”:内存组件没有透明的巨型页面知识可以回退到将巨型的PMD映射分解成ptes表,如果有必要,分裂一个透明的大页面。因此这些组件可以继续在常规页面或常规pte映射上工作。

 如果由于内存碎片而导致大页面分配失败,常规页面应该优雅地分配和混合在相同的vma中,没有任何故障或重大延迟,没有用户感知。

如果某个任务退出了,并且出现了更多可用的大页面(要么立即在buddy中或者通过VM),由常规页面支持的guest物理内存应该重新自动的安放在大页面上(通过khugepaged线程)。

它不需要内存预留,并且尽可能地使用大页(这里唯一可能的预留是kernelcore=,以避免不可移动的页面碎片化所有内存,但这样的调整不是针对透明大页支持的,它是通用的适用于内核中所有动态高阶分配的特性)

 透明大页支持最大限度地利用空闲内存,如果与hugetlbfs的保留方法相比,允许所有未使用的内存用作缓存或其他可移动(甚至不可移动的对象)。它不需要预留来防止从用户空间发现大页面分配失败。它允许分页和所有其他高级vm功能在大页上。应用程序不需要修改就可以利用它。

然而,应用程序可以进一步优化以利用这个功能,就像他们之前优化过避免每个malloc(4k)都需要大量的mmap系统调用。优化用户空间到目前为止不是强制性的,khugepaged已经可以照顾长生命周期的页面分配,即使对于处理大量内存的不知道大页的应用程序也是如此。

在某些情况下,当启用大页面时,系统范围内,应用程序可能最终会分配更多的内存资源。一个应用程序可以映射一个大的区域,但只触及其中1字节,在这种情况下,一个2M的页面可能被分配而不是分配一个4k页面是没有好处的。这就是为什么可以在系统范围内禁用大页面,并且只在内部使用它们MADV_HUGEPAGE的madvise的区域。

嵌入式系统应该只在madvise区域内启用大页面为了消除浪费宝贵内存字节的风险,并且只会跑得更快。

应用程序可以从大页中获得很多好处,而不可以冒着丢失内存的风险使用大页,应该使用madvise(MADV_HUGEPAGE)在他们关键映射区域。

== sysfs ==

透明大页支持匿名内存能被完全的禁用(主要是为了调试)或仅在MADV_HUGEPAGE区域内启用(避免占用更多内存资源的风险)或者系统范围内启用。这可以通过以下方式实现:

 

echo never >/sys/kernel/mm/transparent_hugepage/enabled
echo always >/sys/kernel/mm/transparent_hugepage/enabled 
echo madvise >/sys/kernel/mm/transparent_hugepage/enabled 

 

还可以限制VM中的碎片整理工作,以生成匿名的巨型页面,以防它们不能立即自由地使用madvise区域,或者永远不要尝试对内存进行碎片整理,而只是回退到常规页面,除非巨型页面立即可用。显然,如果我们花费CPU时间对内存进行碎片整理,那么我们将期望获得更多的好处,因为我们稍后使用了大页面而不是普通页面。这不是总能保证的,更可能的情况是分配给一个MADV_HUGEPAGE区域。

 

echo always >/sys/kernel/mm/transparent_hugepage/defrag 
echo defer >/sys/kernel/mm/transparent_hugepage/defrag 
echo defer+madvise >/sys/kernel/mm/transparent_hugepage/defrag 
echo madvise >/sys/kernel/mm/transparent_hugepage/defrag
echo never >/sys/kernel/mm/transparent_hugepage/defrag

 

“always”意味着请求THP的应用程序将在分配失败时暂停,并直接回收页面和规整内存,以便立即分配THP。对于那些从THP使用中受益颇多并愿意延迟虚拟机开始使用它们的虚拟机来说,这可能是可取的。

“defer”意味着应用程序将在后台唤醒kswapd来回收页面,并唤醒kcompactd来规整内存,以便在不久的将来THP可用。khugepage负责随后安装THP页面。

"defer+madvise"只对已经使用madvise(MADV_HUGEPAGE)的区域,后台唤醒kswapd以回收页面,并唤醒kcompactd以规整内存,以便THP在不久的将来可用。

"madvise"将进入直接回收,像"always",但只对madvise(MADV_HUGEPAGE)的区域。这是默认行为。

“never”应该是不言自明的,它不采取任何措施。

默认情况下,内核尝试在读取页面错误时使用巨型零页来进行匿名映射。可以通过写入0来禁用巨型0页,也可以通过写入1来启用巨型0页:

 

echo 0 >/sys/kernel/mm/transparent_hugepage/use_zero_page 
echo 1 >/sys/kernel/mm/transparent_hugepage/use_zero_page

 

一些用户空间(比如一个测试程序,或者一个优化的内存分配库)可能想知道一个透明大页的大小(以字节为单位):

 

cat /sys/kernel/mm/transparent_hugepage/hpage_pmd_size

 

 当transparent_hugepage/enabled设置为“always”或“madvise”时,khugepaged将自动启动,如果设置为“never”,它将自动关闭。

 khugepaged的运行频率通常较低,因此,虽然人们可能不希望在缺页异常期间同步调用碎片整理算法,但至少在khugepaged中调用碎片整理是值得的。但是,也可以通过写0来禁用khugepaged中的碎片整理,或者通过写1来启用khugepaged中的碎片整理:

 

echo 0 >/sys/kernel/mm/transparent_hugepage/khugepaged/defrag 
echo 1 >/sys/kernel/mm/transparent_hugepage/khugepaged/defrag

 

你也可以控制khugepaged每次通过时应该扫描多少页面:

 

/sys/kernel/mm/transparent_hugepage/khugepaged/pages_to_scan

 

以及每次通过之间在khugepaged中等待毫秒数(你可以设置为0来运行khugepaged,在一个核的100%利用率):

 

/sys/kernel/mm/transparent_hugepage/khugepaged/scan_sleep_millisecs

 

以及在khugepage中等待多少毫秒,如果有一个巨大的页面分配失败,以阻止下一次分配尝试。

 

/sys/kernel/mm/transparent_hugepage/khugepaged/alloc_sleep_millisecs

 

 khugepaged的进度可以从坍缩的页面数中看到:

 

/sys/kernel/mm/transparent_hugepage/khugepaged/pages_collapsed

 

每次通过:

 

/sys/kernel/mm/transparent_hugepage/khugepaged/full_scans

 

max_ptes_none指定有多少额外的小页面(即尚未映射的)可以在踏缩一组小页到大页中被分配(查询到相应的页表项为空)。

 

/sys/kernel/mm/transparent_hugepage/khugepaged/max_ptes_none

 

 较高的值会导致程序使用额外的内存。数值越低,获得的thp性能越低。max_ptes_none值只会浪费很少的cpu时间,你可以忽略它。

max_ptes_swap指定当将一组页面坍缩(collapse)成一个透明的大页面时,可以从交换区换入多少页面(查询到相应的页表项为换出页标识符)。。

 

/sys/kernel/mm/transparent_hugepage/khugepaged/max_ptes_swap

 

较高的值会导致过多的交换IO并浪费内存。较低的值可以防止thp被坍缩,从而导致更少的页面坍缩进thp,内存访问性能较低。

== 启动参数 == 

你可以更改透明大页sysfs启动时的默认值,通过传递参数"transparent_hugepage=always" 或"transparent_hugepage=madvise" 或 "transparent_hugepage=never"到内核命令行。

 == tmpfs/shmem 中的大页面 ==

您可以使用挂载选项控制tmpfs中的大页分配策略"huge="。它可以有以下值:

"always": 每次需要新页面时,尝试分配大页面;

"never": 不要分配大页面;

"within_size": 只有它将完全在i_size内时才分配大页。也尊重fadvise()/madvise()提示;

"advise":  只有在fadvise()/madvise()请求时才分配大页面;

默认策略为“never”。

“mount -o remount,huge= /mountpoint”在挂载后工作良好:重新挂载huge=never根本不会分解大页面,只是停止更多的分配。

还有一个sysfs接口可以控制内部shmem挂载的大页分配策略:

 

/sys/kernel/mm/transparent_hugepage/shmem_enabled。

 

挂载用于SysV SHM, memfds,共享匿名映射(/dev/zero或MAP_ANONYMOUS)GPU驱动的DRM对象,Ashmem。

除了上面列出的策略之外,shmem_enabled还允许另外两个值:

"deny": 用于在紧急情况下使用,以强制关闭所有挂载的大页选项;

"force": 为所有人强制提供大页的选项——这对测试非常有用;  

==需要重新启动应用程序==

transparent_hugepage/enabled值和tmpfs挂载选项只影响未来的行为。因此,为了使它们有效,您需要重新启动任何可能使用大页面的应用程序。这也适用于在khugepaged中注册的区域。

==监控使用情况==

当前使用的匿名透明大页面的数量系统可以通过读取/proc/meminfo中的AnonHugePages字段来访问。为了识别哪些应用程序正在使用匿名透明的大页面,读取/proc/PID/smaps并统计为每个映射的AnonHugePages字段是必要的。

映射到用户空间的文件透明大页面数量可用通过读取/proc/meminfo中的ShmemPmdMapped和ShmemHugePages字段。为了确定哪些应用程序正在映射文件透明的巨大页面,它读取/proc/PID/smaps并统计为每个映射FileHugeMapped字段是必要的。

注意,读取smaps文件时昂贵的,且经常会产生开销。

在/proc/vmstat中有许多计数器可以用于监视系统提供大页面的成功程度。

thp_fault_alloc : 每当处理缺页异常时,一个大页面被成功分配,thp_fault_alloc就会增加。这适用于第一次出现缺页异常和COW错误。

thp_collapse_alloc:当它发现一个范围的页面坍缩成一个大页,并有成功分配一个新的巨大页来存储数据,thp_collapse_alloc会被khugepaged增加。

thp_fault_fallback: 如果缺页异常失败的分配一个大页,则thp_fault_fallback被增加,而回退使用小页面。

thp_collapse_alloc_failed: 当它发现一个范围的页面应该被坍缩成一个大页,但是分配大页失败,thp_collapse_alloc_failed会被khugepaged增加。 thp_file_alloc: 在文件大页成功分配时递增。

thp_file_mapped: 每映射到一个文件大页到用户地址空间,thp_file_mapped就增加一次。

thp_split_page:在每次将一个巨大的页面分裂为普通页时递增。发生这种情况的原因有很多,但都很常见原因是一个巨大的页面是旧的,正在被回收。这个操作意味着分裂页面映射的所有PMD。

thp_split_page_failed:如果内核无法分裂大页,则增加thp_split_page_failed计数。如果页面被人pin住了,就会发生这种情况。

thp_deferred_split_page:当大页被放到分裂队列时,thp_deferred_split_page计数被增加。当一个巨大的页面部分被unmap且分裂它将释放一些内存就会发生这种情况。分裂队列上的页将在内存压力下分裂。

thp_split_pmd: 每当pmd分裂成pte表时,thp_split_pmd就会递增。例如,当应用程序调用mprotect()或unmap()在大页面的一部分。它不会分割大页面,只是页表条目。

 thp_zero_page_alloc: thp_zero_page_alloc在每出现一个巨型零页被成功地分配时递增。它包括分配,放弃了与其他分配的竞争。注意,这不算每次巨型零页的映射,只有它的分配。

thp_zero_page_alloc_failed: 如果内核分配巨型零页失败并回退到使用小页,则thp_zero_page_alloc_failed会增加。

随着系统老化,分配大页的开销可能会很大,因为系统会使用内存规整在内存周围来复制数据,以释放大页供使用。在/proc/vmstat中有一些计数器可以帮助监视这种开销。

compact_stall: 每当进程停滞去允许内存规整时,compact_stall就会增加,以便一个巨大的页面被释放供使用。

compact_success: 如果系统规整内存和释放一个大页面供使用,则compact_success会增加(成功规整的次数)。

 compact_fail: 如果系统试图规整内存但是失败了,则compact_fail会增加(失败规整的次数)。

compact_pages_moved: 每次移动页面时,compact_pages_moved会增加。如果这个值是迅速增加的,说明该系统就是复制大量的数据来满足大页面分配。复制的成本可能超过任何减少TLB misse的节省。

compact_pagemigrate_failed: 在底层机制递增移动页面失败,compact_pagemigrate_failed会增加(规整时,迁移页面失败次数) 。

compact_blocks_moved: 每次内存规整检查时一个大页面对齐的页面范围,compact_blocks_moved会增加。

可以使用函数跟踪器来记录在__alloc_pages_nodemask中花费了多长时间,并使用mm_page_alloc跟踪点来确定哪些分配用于巨大的页面。

== get_user_pages and follow_page ==

get_user_pages和follow_page如果在一个巨型的页面上运行,将返回往常一样的头页或尾页(就像他们在hugetlbfs上做的一样)。大多数gup用户只关心实际的物理属性页的地址和它的临时固定在I/O之后释放是完整的,所以他们不会注意到页面是巨型的。但如果有任何驱动程序会在尾部的页面结构上损坏 page(用于检查page->mapping或其他相关的位对于头页而不是尾页),应该更新为跳转改为检查头页。在任何头/尾页上引用都可以防止页面被任何人分裂。

注意:这些不是GUP API的新约束,它们与hugetlbfs上的约束相同,所以任何能够在hugetlbfs上处理GUP的驱动程序也可以很好地处理透明的大页面支持映射。

如果您不能处理由follow_page返回的复合页面,那么可以将FOLL_SPLIT位指定为follow_page的参数,这样它将在返回大页面之前分裂它们。例如,迁移将FOLL_SPLIT作为参数传递给follow_page,因为它不知道巨型页面,事实上它根本不能在hugetlbfs上工作(但由于FOLL_SPLIT,它在透明的巨型页面上工作得很好)。迁移根本无法处理返回的大页面(因为它不仅检查页面的PFN并在复制期间pin住它,而且带有常规的pte/pmd映射)。

==优化应用程序==

为了保证内核将立即在任何内存区域映射2M页,mmap区域必须自然对齐。posix_memalign()可以提供这种保证。

== Hugetlbfs ==

您可以在内核中使用hugetlbfs,并且始终很好地启用了透明的超大页支持。hugetlbfs中除了整体碎片更少之外,没有什么不同。所有属于hugetlbfs的常见特性都被保留且不受影响。libhugetlbfs也会像往常一样正常工作。

==优雅回退==

代码遍历页表但不能感知巨型的pmds,可以简单地调用split_huge_pmd(vma, pmd, addr),其中pmd是pmd_offset返回的那个。通过查询“pmd_offset”并在pmd_offset返回pmd后丢失的地方添加split_huge_pmd,使代码透明地感知大页是很简单的。多亏了优雅的回退设计,只需一行代码的更改,就可以避免编写数百行(如果不是数千行的话)的复杂代码,从而使代码具有超大页面的感知能力。

 如果您没有遍历页表,但是遇到了一个物理的大页,但是您不能在代码中原生地处理它,您可以通过调用split_huge_page(page)来分裂它。这就是Linux VM在尝试切换大页面之前所做的。如果页面被pin住,那么split_huge_page()可能会失败,您必须正确处理这个问题。

 让mremap.c透明感知hugepage的例子,只需要一行代码的改变:

 

diff --git a/mm/mremap.c b/mm/mremap.c
--- a/mm/mremap.c
+++ b/mm/mremap.c
@@ -41,6 +41,7 @@ static pmd_t *get_old_pmd(struct mm_stru
    return NULL;


  pmd = pmd_offset(pud, addr);
+  split_huge_pmd(vma, pmd, addr);
  if (pmd_none_or_clear_bad(pmd))
    return NULL;

 

== 锁定大页面感知代码 ==

我们希望尽可能多的代码能够感知大页,因为调用 split_huge_page()或split_huge_pmd()是有代价的。

要使页表遍历感知巨型pmd,您所需要做的就是调用pmd_trans_huge()在由pmd_offset返回的PMD上。你必须持有mmap_sem处于读(或写)模式,以确保不能出现巨型PMD由khugepaged创建 (khugepaged坍缩巨型页collapse_huge_page除anon_vma锁外,还以写模式持有mmap_sem)。

如果pmd_trans_huge返回false,您只需返回到旧代码路径。如果pmd_trans_huge返回true,则必须持有页表锁(pmd_lock()),然后重新运行pmd_trans_huge。持有页表锁将防止巨型的PMD被转换成一个常规的PMD(split_huge_pmd可以与页表遍历并行)。如果第二个pmd_trans_huge返回false,则应该释放页表锁并回退到之前的旧代码中。否则,您可以继续处理巨型的pmd和hugepage本身。一旦完成,您可以释放页表锁。

 == 引用计数和透明大页 ==

THP上的引用计数和其他复合页的引用计数基本一致:

 get_page()/put_page() and GUP 在首页的->_refcount中操作。

尾页的->_refcoun总是0:get_page_unless_zero()从来不会在尾页上成功。

map/unmap具有带有PTE条目的页面,增加/减小复合页相关子页上的->_mapcount。

map/unmap 整个复合页的被记账在compound_mapcount(存储在第一个尾页)。对于文件巨型页,我们也增加所有子页面的->_mapcount,以便无竞争检测子页面的最后一次unmap。

 PageDoubleMap()表示页面可能映射了pte。

对于匿名页面,PageDoubleMap()还表示->_mapcount在所有子页面中被抵消了一个。此附加引用是必需的,当子页面同时被映射到PMDs和 PTEs时,获得对其子页面unmap的无竞争检测。

这是降低每个子页面的mapcount跟踪开销所需的优化。另一种方法是在整个复合页面的每个map/unmap上的所有子页面中添加 ->_mapcount。

 对于匿名页面,当页面的PMD被分裂时,但仍有PMD映射,我们设置PG_double_map。额外的引用去掉最后一个compound_mapcount。

文件页面在带有PTE和的页面的第一个映射上设置PG_double_map ,当页面从页面缓存中被驱逐时,该页面就会消失。

 split_huge_page内部必须在从头页到尾页分配refcount,然后清除页面结构中所有的PG_head/尾位。它可以很容易地实现页表条目的引用计数。但我们没有足够的信息来分发额外的pins(即get_user_pages)。split_huge_page()请求去分裂pin住的大页面是失败的: 它期望页面计数等于所有子页面的mapcount之和加上1 (split_huge_page调用者必须有头页引用)。

split_huge_page使用迁移条目来稳定匿名页面的page->_refcount和page->_mapcount。文件页面被取消映射。

我们和物理内存扫描器(页面回收的扫描器)竞争也是安全的:扫描器来获取对页面的引用唯一合法的方式是get_page_unless_zero()。

在atomic_add()之前,所有尾页的->_refcount都为0。这可以防止扫描器获取到尾页的引用。在atomic_add()之后,我们不关心->_refcount值。我们已经从头页上知道有多少引用是取消记账的。

对于头页,get_page_unless_zero()会成功,我们不介意。它是明确拆分后引用应该去哪里:它将停留在首页。

注意split_huge_pmd()对refcount没有任何限制:PMD可以在任何点被拆分并且永不失败。

 == 部分 unmap and deferred_split_huge_page() ==

解除THP部分映射(使用munmap()或其他方式)不会立即释放内存。相反,我们在page_remove_rmap()中检测到THP的一个子页面没有被使用,并在内存压力时,将THP排队以进行拆分。分裂将释放未使用的子页面。

由于将上下文锁住在我们可以检测到部分unmap的地方,所以不能立即拆分页面。这也可能会适得其反,因为在许多情况下,如果THP跨越VMA边界,在exit(2)期间会发生部分unmap。

用于对页面进行排队以进行拆分。当我们通过shrinker收缩器接口获得内存压力时,分裂本身就会发生。






审核编辑:刘清

打开APP阅读更多精彩内容
声明:本文内容及配图由入驻作者撰写或者入驻合作网站授权转载。文章观点仅代表作者本人,不代表电子发烧友网立场。文章及其配图仅供工程师学习之用,如有内容侵权或者其他违规问题,请联系本站处理。 举报投诉

全部0条评论

快来发表一下你的评论吧 !

×
20
完善资料,
赚取积分