news 2026/9/7 8:54:35

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

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
Linux 内核 cgroup v1 HugeTLB 控制器:缺页计费与预留计费的源码级解析

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 的实现可以看到:

  • usagersvd.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>.failcnt

HugeTLB 控制器允许为每个控制组设置 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.failcnt

HugeTLB 控制器同样允许限制每个控制组的 HugeTLB预留(reservation)量,并在预留发生时以及对不存在预留的 HugeTLB 内存发起缺页时实施该限额。因为预留限额是在预留动作(mmapshmget)执行时就强制实施的,所以只要内存是预先预留的,预留限额永远不会导致应用收到SIGBUS。对MAP_NORESERVE这类不建立预留的映射,rsvd限额的行为则与缺页限额一致——在缺页时刻强制用量,越限即向应用发送SIGBUS

预留限额相比上述缺页限额的优势在于:

  1. 强制时机更早:在mmap/shmget阶段即拒绝超限请求,而非等到真正访问页面时才发现;
  2. 永不引发 SIGBUS(前提是内存已预留):进程可以在预留失败后从容回退到替代方案,例如改用普通(非 HugeTLB)内存;
  3. 运维更省心:反观缺页计费,为避免 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),仅供参考

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/9/7 8:50:33

YOLO+多模态LLM:智慧交通监控预警系统实战

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

作者头像 李华
网站建设 2026/9/7 8:49:54

MSP430与LMP90100 SPI驱动开发实战:多通道ADC采集与移植经验

简介&#xff1a;面向嵌入式开发者&#xff0c;这是TI官方的MSP430与LMP90100传感器AFE接口代码库及说明文档&#xff0c;用于解决高精度传感器信号链中原型搭建、初始化配置与数据读取等常见问题&#xff0c;尤其适合需要快速评估LMP90100性能的工程师。资源包共37个文件&…

作者头像 李华
网站建设 2026/9/7 8:49:42

Linux用户空间驱动DS1302 RTC实战:GPIO模拟时序与系统时间同步

简介&#xff1a;面向Linux驱动开发者&#xff0c;资源提供DS1302实时时钟芯片的完整驱动源码与测试程序。驱动覆盖设备树配置、I2C/SPI接口适配、BCD时间格式转换、内核timekeeper同步、掉电保护处理&#xff0c;以及用户空间/dev/rtc*设备节点访问等关键环节&#xff1b;配套…

作者头像 李华
网站建设 2026/9/7 8:44:08

如何有效判断与排查Java GC问题

目录 一、GC的重要性与对性能的影响 (一)GC对性能的影响简要分析 1.GC暂停与应用停顿 2.GC吞吐量与资源利用率 3.GC对内存管理的作用:资源回收 4.GC策略与优化的选择 (二)GC的双刃剑 二、GC性能评价标准 (一)GC性能评价标准:延迟(Latency)与吞吐量(Through…

作者头像 李华