Linux 内核 cgroup v1 HugeTLB 控制器:缺页计费与预留计费的源码级解析

发布时间:2026/9/7 8:54:47
Linux 内核 cgroup v1 HugeTLB 控制器:缺页计费与预留计费的源码级解析 Linux 内核 cgroup v1 HugeTLB 控制器缺页计费与预留计费的源码级解析【免费下载链接】linuxLinux kernel source tree项目地址: https://gitcode.com/GitHub_Trending/li/linuxHugeTLB巨型页由于不支持页回收其内存限额必须采用与常规内存控制器不同的策略。本文基于 Linux 内核 cgroup v1 的 HugeTLB 控制器系统讲解其挂载方式、控制文件语义并区分「页面缺页计费Page fault accounting」与「预留计费Reservation accounting」两套独立限额体系剖析共享内存与 cgroup 下线时的特殊行为。读者将掌握 HugeTLB cgroup 的配置与排障方法并理解为何预留限额能有效避免进程收到 SIGBUS。一、控制器概览与启用前提HugeTLB 控制器是内核 cgroup 子系统之一可在内核配置中显式开启。在 init/Kconfig 中定义如下配置项CONFIG_CGROUP_HUGETLB依赖HUGETLB_PAGE自动选择PAGE_COUNTER页计数器基础设施Kconfig 帮助文本点明了该控制器的设计约束它允许对每个 cgroup 施加 HugeTLB 用量上限并在缺页page fault时强制实施由于 HugeTLB 不支持页面回收一旦进程尝试在限额之外再 fault-in HugeTLB 页就会收到SIGBUS因此应用必须预先精确知晓自身将使用多少 HugeTLB 页。另外由于控制器元数据借助 folio 的第三个 LRU 指针存放内核无法对小于 3 个页的 HugeTLB 页使用该控制器见 mm/hugetlb_cgroup.c 中 move_parent 对无 cgroup 关联页面的说明。二、挂载文件系统并创建 HugeTLB cgroupHugeTLB 控制器随 cgroup 文件系统一起暴露。传统 v1 方式下需要先挂载带hugetlb子系统的 cgroup 文件系统# mount -t cgroup -o hugetlb none /sys/fs/cgroup挂载完成后位于/sys/fs/cgroup的初始组即父级 HugeTLB 组即对用户可见。系统启动时该组包含系统中所有任务/sys/fs/cgroup/tasks列出本 cgroup 中的所有任务。在父组/sys/fs/cgroup之下可以创建新组并把当前 shell 进程移入其中# cd /sys/fs/cgroup # mkdir g1 # echo $$ g1/tasks上述三步完成了新建 cgroup 目录g1、将当前 shellbash进程写入g1/tasks从而把该进程及其后续 fork 出的子任务纳入g1的 HugeTLB 计费与限额范围内。cgroup 内的限额会随任务迁移而生效任务移动出组后不再受该组限额约束。三、控制文件总览控制器为每一种已配置的 HugeTLB 页大小维护两套相互独立的资源计数一套用于缺页fault/usage计费一套用于预留reservation带.rsvd前缀计费。控制文件的通式为hugetlb.hugepagesize.attributehugetlb.hugepagesize.rsvd.limit_in_bytes # 设置/查看 hugepagesize 巨型页预留reservation的限额 hugetlb.hugepagesize.rsvd.max_usage_in_bytes # 查看 hugepagesize 巨型页曾达到的最大预留含 no-reserve fault用量 hugetlb.hugepagesize.rsvd.usage_in_bytes # 查看 hugepagesize 巨型页当前的预留及无预留缺页用量 hugetlb.hugepagesize.rsvd.failcnt # 查看因 HugeTLB 预留限额导致的分配失败次数 hugetlb.hugepagesize.limit_in_bytes # 设置/查看 hugepagesize 巨型页缺页fault用量限额 hugetlb.hugepagesize.max_usage_in_bytes # 查看 hugepagesize 巨型页曾记录到的最大用量 hugetlb.hugepagesize.usage_in_bytes # 查看 hugepagesize 巨型页当前用量 hugetlb.hugepagesize.failcnt # 查看因 HugeTLB 用量限额导致的分配失败次数 hugetlb.hugepagesize.numa_stat # 查看本 cgroup 被计费的 HugeTLB 内存的 NUMA 分布信息其中各属性的含义如下控制文件含义可写limit_in_bytes缺页用量限额字节写入即设置root 组不可写是usage_in_bytes当前缺页计费用量字节层级累计否max_usage_in_bytes历史峰值用量watermark写入清零是写以清零failcnt因达到限额而失败的次数写入清零是写以清零rsvd.limit_in_bytes预留及无预留缺页限额字节是rsvd.usage_in_bytes当前预留计费用量字节否rsvd.max_usage_in_bytes预留用量历史峰值写入清零是写以清零rsvd.failcnt预留限额失败次数写入清零是写以清零numa_stat本 cgroup 计费的 HugeTLB 内存按 NUMA 节点的分布否这些语义与 mm/hugetlb_cgroup.c 中的枚举RES_USAGE / RES_RSVD_USAGE / RES_LIMIT / RES_MAX_USAGE / RES_FAILCNT一一对应。从 hugetlb_cgroup_read_u64 的实现可以看到usage与rsvd.usage返回page_counter_read(counter) * PAGE_SIZE字节limit返回counter-max * PAGE_SIZE其上限在初始化时被round_down(PAGE_COUNTER_MAX, pages_per_huge_page(...))对齐到整页max_usage读取的是计数器的watermarkfailcnt直接输出计数器的失败累计值。写路径 hugetlb_cgroup_write 体现了两个关键规则root cgroup 不允许设置限额直接返回-EINVAL写入的字节数会先经round_down对齐到该 HugeTLB 页大小的整数倍limit_in_bytes系列还支持写入-1表示解除限制v1 语义legacy 模板传入-1作为 max 关键字见 hugetlb_cgroup_write_legacy。3.1 多尺寸巨型页下的完整文件清单若系统同时支持 64k、32M 与 1G 三种 HugeTLB 页大小则每个 cgroup 下会看到如下 27 个控制文件缺页 5 个 预留 4 个 numa_stat 1 个再乘以 3 种页大小hugetlb.1GB.limit_in_bytes hugetlb.1GB.max_usage_in_bytes hugetlb.1GB.numa_stat hugetlb.1GB.usage_in_bytes hugetlb.1GB.failcnt hugetlb.1GB.rsvd.limit_in_bytes hugetlb.1GB.rsvd.max_usage_in_bytes hugetlb.1GB.rsvd.usage_in_bytes hugetlb.1GB.rsvd.failcnt hugetlb.64KB.limit_in_bytes hugetlb.64KB.max_usage_in_bytes hugetlb.64KB.numa_stat hugetlb.64KB.usage_in_bytes hugetlb.64KB.failcnt hugetlb.64KB.rsvd.limit_in_bytes hugetlb.64KB.rsvd.max_usage_in_bytes hugetlb.64KB.rsvd.usage_in_bytes hugetlb.64KB.rsvd.failcnt hugetlb.32MB.limit_in_bytes hugetlb.32MB.max_usage_in_bytes hugetlb.32MB.numa_stat hugetlb.32MB.usage_in_bytes hugetlb.32MB.failcnt hugetlb.32MB.rsvd.limit_in_bytes hugetlb.32MB.rsvd.max_usage_in_bytes hugetlb.32MB.rsvd.usage_in_bytes hugetlb.32MB.rsvd.failcnt这些文件由内核启动阶段统一注册生成。文件名的尺寸前缀来自 mem_fmt按页大小自动换算为KB/MB/GB字符串随后 hugetlb_cgroup_cfttypes_init 以%s.%s拼接尺寸前缀与模板属性名并分别按 legacyv1与 defaultv2两套模板hugetlb_legacy_tmpl / hugetlb_dfl_tmpl构造 cftype最终在 hugetlb_cgroup_file_init 中挂载到hugetlb_cgrp_subsys。cgroup v2 下同名控制器暴露的文件是hugetlb.size.max / rsvd.max / current / rsvd.current / events / events.local / numa_stat且events含.local文件会在用量撞上限时通过 hugetlb_event 上报max事件并逐级通知父组。四、第一套限额体系缺页计费Page fault accounting缺页计费对应的控制文件为hugetlb.hugepagesize.limit_in_bytes hugetlb.hugepagesize.max_usage_in_bytes hugetlb.hugepagesize.usage_in_bytes hugetlb.hugepagesize.failcntHugeTLB 控制器允许为每个控制组设置 HugeTLB缺页用量限额并在缺页发生时强制该限额。由于 HugeTLB 不支持页面回收在缺页时刻实施限额意味着应用若试图 fault-in 超出限额的 HugeTLB 页将收到SIGBUS。因此应用必须事先精确知道自己要用多少 HugeTLB 页系统管理员必须确保机器上有足够的 HugeTLB 页可供所有用户分配避免进程因限额被触发而收到SIGBUS。从源码看缺页计费的强制点位于 __hugetlb_cgroup_charge_cgroup该函数取出当前任务所属 cgroup 的计数器并调用page_counter_try_charge()尝试计费若返回失败则置ret -ENOMEM并调用hugetlb_event(h_cg, idx, HUGETLB_MAX)触发max事件最终由 HugeTLB 缺页路径将该失败转化为对进程的SIGBUS。成功计费后页面 cgroup 归属被记录在 folio 上_hugetlb_cgroup并在释放时经 hugetlb_cgroup_uncharge_folio 撤销。缺页计费的难点在于「超额」与「SIGBUS」之间的必然联系管理员必须对所有任务的全系统 HugeTLB 用量了如指掌并预留足量页面。在内核超售overcommit的系统中想用缺页计费完全避免进程被SIGBUS实际上几乎不可能。五、第二套限额体系预留计费Reservation accounting预留计费对应的控制文件为hugetlb.hugepagesize.rsvd.limit_in_bytes hugetlb.hugepagesize.rsvd.max_usage_in_bytes hugetlb.hugepagesize.rsvd.usage_in_bytes hugetlb.hugepagesize.rsvd.failcntHugeTLB 控制器同样允许限制每个控制组的 HugeTLB预留reservation量并在预留发生时以及对不存在预留的 HugeTLB 内存发起缺页时实施该限额。因为预留限额是在预留动作mmap或shmget执行时就强制实施的所以只要内存是预先预留的预留限额永远不会导致应用收到SIGBUS。对MAP_NORESERVE这类不建立预留的映射rsvd限额的行为则与缺页限额一致——在缺页时刻强制用量越限即向应用发送SIGBUS。预留限额相比上述缺页限额的优势在于强制时机更早在mmap/shmget阶段即拒绝超限请求而非等到真正访问页面时才发现永不引发 SIGBUS前提是内存已预留进程可以在预留失败后从容回退到替代方案例如改用普通非 HugeTLB内存运维更省心反观缺页计费为避免 SIGBUS管理员必须精确掌握系统中所有任务的 HugeTLB 用量并确保页面充足在超售系统上几乎不可行。源码侧预留计费与缺页计费共享 __hugetlb_cgroup_charge_cgroup 的同一套page_counter_try_charge逻辑只是通过rsvdtrue选择rsvd_hugepage[]计数器并经由 hugetlb_cgroup_charge_cgroup_rsvd 与hugetlb_cgroup_commit_charge_rsvd完成计费与页面归属标注folio 的_hugetlb_cgroup_rsvd字段定义见 include/linux/hugetlb_cgroup.h。一个值得注意的实现细节是预留计费会对 cgroup 的 css 额外持有引用css_tryget后不立即css_put因为预留不会被 reparent——代码注释明确写道 Reservations take a reference to the css because they do not get reparented。内核还配套了专门的回归测试 tools/testing/selftests/mm/charge_reserved_hugetlb.sh脚本中分别操作limit_in_bytes缺页限额与rsvd.limit_in_bytes / rsvd.usage_in_bytes预留限额验证两套计费路径互不混淆。六、共享 HugeTLB 内存的计费注意点对于共享 HugeTLB 内存HugeTLB 预留与缺页计费都遵循「首触计费」规则预留与缺页均计费到第一个引发该内存被预留或被缺页的任务所在 cgroup此后所有对该片已预留/已缺页内存的后续使用不再产生任何额外计费。相应地共享 HugeTLB 内存只在被解除预留unreserve或释放deallocate时才撤销计费。这一时刻通常是承载该 HugeTLB 内存的文件被删除之时而不是最初引发预留或缺页的那个任务退出之时。换言之任务退出并不会自动回收共享 HugeTLB 页的用量只要共享文件仍存在计费就一直留在发起者的 cgroup 头上。这与 HugeTLB 预留的生命周期跟随 inode/reservation map而非跟随进程是自洽的——从源码看撤销预留计费的路径 hugetlb_cgroup_uncharge_counter 正是以resv_map预留映射为操作对象在预留区间被释放如文件截断/删除时按(end - start) * pages_per_hpage成批 uncharge。七、HugeTLB cgroup 下线offline时的行为当某个 HugeTLB cgroup 下线目录被移除、任务全部迁出时若仍有计费挂在该组上行为按计费类型区分缺页计费fault charges被转移reparent到父级 HugeTLB cgroup预留计费reservation charges保留在已下线的 HugeTLB cgroup 上不会转移。因此若 HugeTLB cgroup 下线时仍有 HugeTLB 预留计费残留该 cgroup 将以zombie僵尸形态持续存在直到其上的所有预留全部被撤销计费。这种设计有意与内存控制器memory controller保持一致——后者同样允许带计费的 cgroup 以僵尸形态存在直到计费清零。此外HugeTLB 预留的跟踪本就比缺页跟踪复杂得多因此在下线时对预留做 reparent 也远比转移缺页计费困难。源码侧hugetlb_cgroup_css_offlinemm/hugetlb_cgroup.c反复遍历各 hstate 的hugepage_activelist调用 hugetlb_cgroup_move_parent 把非预留fault归属的 folio 从本组计数器page_counter_cancel掉并挂到父组计数器下循环直至非预留用量归零。而预留侧从未出现在这条 reparent 路径中——这正是文档所述「预留计费留在下线 cgroup」且该组变成 zombie 的根本原因预留计费在 charge 时持有的那一次 css 引用见第五节只有等到预留真正 unchargehugetlb_cgroup_uncharge_cgroup_rsvd 或 reserve-map 释放路径时才会css_put释放从而保证残留计费期间对应的 cgroup 结构不会被提前销毁。八、实践建议小结优先使用预留限额管理 HugeTLB 共享内存与大规模使用场景它在mmap/shmget阶段拦截超限请求进程可在分配前得到明确失败信号从容选择回退方案规避SIGBUS谨慎对待缺页限额它适合能精确核算自身 HugeTLB 用量的应用管理员需全局统筹页面供给在超售系统上不要指望它能阻止SIGBUS留意共享文件的生存期删除共享 HugeTLB 文件而非等待任务退出才是释放其 cgroup 计费的可靠手段清理容器/任务组时若见 cgroup 无法被彻底销毁多半是预留计费残留所致应定位仍持有预留的共享 HugeTLB 文件并予以释放。上述行为均可对照内核源码 mm/hugetlb_cgroup.c 与配套的预留/缺页计费回归测试 tools/testing/selftests/mm/charge_reserved_hugetlb.sh 进一步验证关于 HugeTLB 页的全局配置与预留机制可继续阅读 Documentation/admin-guide/mm/hugetlbpage.rst 与 Documentation/mm/hugetlbfs_reserv.rst。本文原始依据为内核文档 Documentation/admin-guide/cgroup-v1/hugetlb.rst该控制器默认不随内核编译default n需显式启用CONFIG_CGROUP_HUGETLB并在运行时按 cgroup v1 语义挂载后使用。【免费下载链接】linuxLinux kernel source tree项目地址: https://gitcode.com/GitHub_Trending/li/linux创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考