Linux 内核 cgroup v1 HugeTLB 控制器:缺页计费与预留计费的源码级解析
【免费下载链接】linuxLinux kernel source tree项目地址: https://gitcode.com/GitHub_Trending/li/linux
HugeTLB(巨型页)由于不支持页回收,其内存限额必须采用与常规内存控制器不同的策略。本文基于 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 cgroup
HugeTLB 控制器随 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、将当前 shell(bash)进程写入g1/tasks,从而把该进程及其后续 fork 出的子任务纳入g1的 HugeTLB 计费与限额范围内。cgroup 内的限额会随任务迁移而生效,任务移动出组后不再受该组限额约束。
三、控制文件总览
控制器为每一种已配置的 HugeTLB 页大小维护两套相互独立的资源计数:一套用于缺页(fault/usage)计费,一套用于预留(reservation,带.rsvd前缀)计费。控制文件的通式为hugetlb.<hugepagesize>.<attribute>:
hugetlb.<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读取的是计数器的watermark;failcnt直接输出计数器的失败累计值。
写路径 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"拼接尺寸前缀与模板属性名,并分别按 legacy(v1)与 default(v2)两套模板(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逻辑,只是通过rsvd=true选择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_offline(mm/hugetlb_cgroup.c)反复遍历各 hstate 的hugepage_activelist,调用 hugetlb_cgroup_move_parent 把非预留(fault)归属的 folio 从本组计数器page_counter_cancel掉并挂到父组计数器下,循环直至非预留用量归零。而预留侧从未出现在这条 reparent 路径中——这正是文档所述「预留计费留在下线 cgroup」且该组变成 zombie 的根本原因:预留计费在 charge 时持有的那一次 css 引用(见第五节)只有等到预留真正 uncharge(hugetlb_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),仅供参考