内存资源控制器¶
注意
本文档已严重过时,需要完全重写。它仍然包含一些有用的信息,因此我们将其保留在此处,但如果您需要更深入的理解,请务必查看当前代码。
注意
本文档中,内存资源控制器通常被称为内存控制器。请勿将此处使用的内存控制器与硬件中使用的内存控制器混淆。
提示
当我们提及带内存控制器的 cgroup (cgroupfs 的目录) 时,我们称之为“内存 cgroup”。当您查看 git-log 和源代码时,会发现补丁标题和函数名称倾向于使用“memcg”。在本文档中,我们避免使用它。
内存控制器的优点和目的¶
内存控制器将一组任务的内存行为与系统的其余部分隔离开来。LWN 上的一篇文章 [12] 提到了内存控制器的一些可能用途。内存控制器可用于:
隔离一个应用程序或一组应用程序。内存密集型应用程序可以被隔离并限制在较小的内存量内。
创建一个具有有限内存量的 cgroup;这可以作为使用 mem=XXXX 启动的良好替代方案。
虚拟化解决方案可以控制分配给虚拟机实例的内存量。
CD/DVD 刻录机可以控制系统其余部分使用的内存量,以确保刻录不会因可用内存不足而失败。
还有其他几种用例;您可以自行发现,或者仅仅为了好玩(学习和修改 VM 子系统)而使用该控制器。
当前状态:linux-2.6.34-mmotm(2010 年 4 月的开发版本)
特性
匿名页、文件缓存、交换缓存的使用统计与限制。
页面专门链接到每个 memcg 的 LRU,不存在全局 LRU。
可选地,可以对内存+交换使用进行统计和限制。
分层统计
软限制
移动任务时,(重新)计费是可选的。
使用阈值通知器
内存压力通知器
OOM-killer 禁用旋钮和 OOM 通知器
根 cgroup 没有限制控制。
内核内存支持正在开发中,当前版本基本提供了功能。(参见 第 2.7 节)
控制文件简要摘要。
任务 |
附加任务(线程)并显示线程列表 |
cgroup.procs |
显示进程列表 |
cgroup.event_control |
一个用于 event_fd() 的接口。此旋钮在 CONFIG_PREEMPT_RT 系统上不可用。 |
memory.usage_in_bytes |
显示当前内存使用量(详见 5.5) |
memory.memsw.usage_in_bytes |
显示当前内存+交换使用量(详见 5.5) |
memory.limit_in_bytes |
设置/显示内存使用限制 |
memory.memsw.limit_in_bytes |
设置/显示内存+交换使用限制 |
memory.failcnt |
显示内存使用量达到限制的次数 |
memory.memsw.failcnt |
显示内存+交换达到限制的次数 |
memory.max_usage_in_bytes |
显示记录的最大内存使用量 |
memory.memsw.max_usage_in_bytes |
显示记录的最大内存+交换使用量 |
memory.soft_limit_in_bytes |
设置/显示内存使用软限制。此旋钮在 CONFIG_PREEMPT_RT 系统上不可用。此旋钮已弃用,不应使用。 |
memory.stat |
显示各种统计信息 |
memory.use_hierarchy |
设置/显示分层账户是否启用。此旋钮已弃用,不应使用。 |
memory.force_empty |
触发强制页面回收 |
memory.pressure_level |
设置内存压力通知。此旋钮已弃用,不应使用。 |
memory.swappiness |
设置/显示 vmscan 的 swappiness 参数(参见 sysctl 的 vm.swappiness)。在 cgroup v2 中不存在每个 memcg 的旋钮。 |
memory.move_charge_at_immigrate |
此旋钮已弃用。 |
memory.oom_control |
设置/显示 OOM 控制。此旋钮已弃用,不应使用。 |
memory.numa_stat |
显示每个 NUMA 节点的内存使用量 |
memory.kmem.limit_in_bytes |
已弃用的旋钮,用于设置和读取内核内存硬限制。自 5.16 版本以来不再支持内核硬限制。向该文件写入任何值都不会产生任何效果,就像指定了 nokmem 内核参数一样。内核内存仍会通过 memory.kmem.usage_in_bytes 进行计费和报告。 |
memory.kmem.usage_in_bytes |
显示当前内核内存分配 |
memory.kmem.failcnt |
显示内核内存使用量达到限制的次数 |
memory.kmem.max_usage_in_bytes |
显示记录的最大内核内存使用量 |
memory.kmem.tcp.limit_in_bytes |
设置/显示 TCP 缓冲区内存的硬限制。此旋钮已弃用,不应使用。 |
memory.kmem.tcp.usage_in_bytes |
显示当前 TCP 缓冲区内存分配。此旋钮已弃用,不应使用。 |
memory.kmem.tcp.failcnt |
显示 TCP 缓冲区内存使用量达到限制的次数。此旋钮已弃用,不应使用。 |
memory.kmem.tcp.max_usage_in_bytes |
显示记录的最大 TCP 缓冲区内存使用量。此旋钮已弃用,不应使用。 |
1. 历史¶
内存控制器历史悠久。Balbir Singh [1] 发布了内存控制器的评论请求(RFC)。当时发布 RFC 时,已经存在几种内存控制的实现。RFC 的目标是就内存控制所需的最小功能达成共识和一致意见。Balbir Singh [2] 于 2007 年 2 月发布了第一个 RSS 控制器。此后,Pavel Emelianov [3] [4] [5] 发布了三个版本的 RSS 控制器。在 OLS 的资源管理 BoF 上,每个人都建议我们同时处理页面缓存和 RSS。还提出了允许用户空间处理 OOM 的请求。当前的内存控制器是版本 6;它结合了映射(RSS)和未映射页面缓存控制 [11]。
2. 内存控制¶
内存是一种独特的资源,因为它以有限的数量存在。如果一个任务需要大量的 CPU 处理,该任务可以将其处理分散到数小时、数天、数月或数年,但对于内存,需要重复使用相同的物理内存来完成任务。
内存控制器的实现分为几个阶段。它们是:
内存控制器
mlock(2) 控制器
内核用户内存记账和 slab 控制
用户映射长度控制器
内存控制器是第一个开发的控制器。
2.1. 设计¶
设计的核心是一个名为 page_counter 的计数器。page_counter 跟踪与控制器关联的进程组的当前内存使用量和限制。每个 cgroup 都有一个与其关联的内存控制器特定数据结构 (mem_cgroup)。
2.2. 记账¶
+--------------------+
| mem_cgroup |
| (page_counter) |
+--------------------+
/ ^ \
/ | \
+---------------+ | +---------------+
| mm_struct | |.... | mm_struct |
| | | | |
+---------------+ | +---------------+
|
+ --------------+
|
+---------------+ +------+--------+
| page +----------> page_cgroup|
| | | |
+---------------+ +---------------+
图 1 展示了控制器重要的几个方面:
记账按 cgroup 进行
每个 mm_struct 都知道它属于哪个 cgroup
每个页面都有一个指向 page_cgroup 的指针,而 page_cgroup 又知道它属于哪个 cgroup
记账过程如下:调用 mem_cgroup_charge_common() 来设置必要的数据结构,并检查正在记账的 cgroup 是否超过其限制。如果超过,则对 cgroup 调用回收操作。更多细节可以在本文档的回收部分找到。如果一切顺利,一个名为 page_cgroup 的页面元数据结构将被更新。page_cgroup 在 cgroup 上有自己的 LRU。(*) page_cgroup 结构在启动/内存热插拔时分配。
2.2.1 记账详情¶
所有映射的匿名页 (RSS) 和缓存页 (Page Cache) 都被记账。一些不可回收且不会在 LRU 上的页面不被记账。我们只记账在常规 VM 管理下的页面。
除非 RSS 页面之前已被记账,否则会在 page_fault 时进行记账。文件页面在插入 inode (xarray) 时将作为页面缓存进行记账。当它映射到进程的页表时,会仔细避免重复记账。
当 RSS 页面完全解除映射时,将取消记账。当 PageCache 页面从 xarray 中移除时,将取消记账。即使 RSS 页面被完全解除映射(通过 kswapd),它们也可能作为 SwapCache 存在于系统中,直到它们真正被释放。此类 SwapCache 也被记账。换入的页面在添加到 swapcache 后进行记账。
注意:内核会进行 swapin-readahead 并一次读取多个交换。由于页面的 memcg 无论 memsw 是否启用都会记录到交换区,因此页面将在换入后被记账。
在页面迁移时,记账信息会保留。
注意:我们只记账 LRU 上的页面,因为我们的目的是控制已用页面的数量;不在 LRU 上的页面在 VM 看来往往是失控的。
2.4 交换扩展¶
交换使用量始终为每个 cgroup 记录。交换扩展允许您读取和限制它。
当 CONFIG_SWAP 启用时,将添加以下文件。
memory.memsw.usage_in_bytes。
memory.memsw.limit_in_bytes。
memsw 表示内存+交换。内存+交换的使用受到 memsw.limit_in_bytes 的限制。
示例:假设一个系统有 4G 交换空间。一个在 2G 内存限制下分配 6G 内存(错误地)的任务将使用所有交换空间。在这种情况下,设置 memsw.limit_in_bytes=3G 将阻止对交换空间的错误使用。通过使用 memsw 限制,您可以避免因交换空间不足而导致的系统 OOM。
2.4.1 为什么是“内存+交换”而不是“交换”¶
全局 LRU(kswapd) 可以随意换出页面。换出意味着将记账从内存移动到交换...内存+交换的使用量没有变化。换句话说,当我们要限制交换使用量而不影响全局 LRU 时,从操作系统的角度来看,内存+交换限制优于仅仅限制交换。
2.4.2. 当 cgroup 达到 memory.memsw.limit_in_bytes 时会发生什么¶
当 cgroup 达到 memory.memsw.limit_in_bytes 时,在该 cgroup 中进行换出操作是无用的。此时,cgroup 例程将不再执行换出,并且文件缓存将被丢弃。但如上所述,全局 LRU 可以从其中换出内存,以保持系统内存管理状态的健全。您无法通过 cgroup 阻止它。
2.5 回收¶
每个 cgroup 都维护一个与全局 VM 具有相同结构的 LRU。当 cgroup 超出其限制时,我们首先尝试从该 cgroup 回收内存,以便为 cgroup 新触及的页面腾出空间。如果回收不成功,则会调用 OOM 例程来选择并终止 cgroup 中占用内存最多的任务。(参见下文 10. OOM 控制。)
除了选择用于回收的页面来自每个 cgroup 的 LRU 列表之外,回收算法没有针对 cgroup 进行修改。
注意
回收功能不适用于根 cgroup,因为我们无法对根 cgroup 设置任何限制。
注意
当 panic_on_oom 设置为“2”时,整个系统将崩溃。
当注册 OOM 事件通知器时,事件将被传递。(参见 oom_control 节)
2.6 锁定¶
锁定顺序如下:
folio_lock
mm->page_table_lock or split pte_lock
folio_memcg_lock (memcg->move_lock)
mapping->i_pages lock
lruvec->lru_lock.
每个节点每个 memcgroup 的 LRU(cgroup 的私有 LRU)由 lruvec->lru_lock 保护;在 lruvec->lru_lock 下将页面从其 LRU 隔离之前,会清除 folio LRU 标志。
2.7 内核内存扩展¶
通过内核内存扩展,内存控制器能够限制系统使用的内核内存量。内核内存与用户内存根本不同,因为它不能被交换出去,这使得通过消耗过多这种宝贵的资源来发起 DoS 攻击成为可能。
默认情况下,所有内存 cgroup 都启用内核内存记账。但可以通过在启动时向内核传递 cgroup.memory=nokmem 来禁用系统范围的记账。在这种情况下,将完全不记账内核内存。
根 cgroup 不施加内核内存限制。根 cgroup 的使用量可能计费也可能不计费。使用的内存会累积到 memory.kmem.usage_in_bytes 中,或者在有意义的情况下累积到单独的计数器中(目前仅适用于 TCP)。
主“kmem”计数器的数据会输入主计数器,因此 kmem 计费也会在用户计数器中可见。
目前尚未对内核内存实现软限制。当达到这些限制时触发 slab 回收是未来的工作。
2.7.1 当前记账的内核内存资源¶
- 栈页
每个进程都会消耗一些栈页。通过记账到内核内存中,我们可以在内核内存使用过高时阻止新进程的创建。
- Slab 页
由 SLAB 或 SLUB 分配器分配的页面会被跟踪。每次缓存第一次从 memcg 内部被触及时,都会创建一个 kmem_cache 的副本。创建是惰性完成的,因此在缓存创建期间,某些对象仍然可能被跳过。slab 页中的所有对象都应属于同一个 memcg。这仅在任务在缓存分配页面期间迁移到不同的 memcg 时才会失败。
- 套接字内存压力
一些套接字协议具有内存压力阈值。内存控制器允许它们按每个 cgroup 单独控制,而不是全局控制。
- TCP 内存压力
TCP 协议的套接字内存压力。
2.7.2 常见用例¶
由于“kmem”计数器会输入到主用户计数器,因此内核内存永远不能完全独立于用户内存进行限制。假设“U”是用户限制,“K”是内核限制。有三种可能的限制设置方式:
- U != 0, K = 无限
这是在 kmem 记账之前就已存在的标准 memcg 限制机制。内核内存被完全忽略。
- U != 0, K < U
内核内存是用户内存的一个子集。这种设置在每个 cgroup 的总内存被超额分配的部署中非常有用。超额分配内核内存限制绝对不推荐,因为系统仍然可能耗尽不可回收的内存。在这种情况下,管理员可以设置 K,使得所有组的总和永远不会大于总内存,并自由设置 U,但会牺牲其 QoS。
警告
在当前实现中,当 cgroup 达到 K 但仍低于 U 时,不会触发内存回收,这使得此设置不实用。
- U != 0, K >= U
由于 kmem 费用也将计入用户计数器,并且两种内存都会触发 cgroup 的回收。此设置为管理员提供了内存的统一视图,对于只想跟踪内核内存使用情况的人也很有用。
3. 用户界面¶
要使用用户界面:
启用 CONFIG_CGROUPS 和 CONFIG_MEMCG 选项
准备 cgroup(有关背景信息,请参阅 为什么需要 cgroup?)
# mount -t tmpfs none /sys/fs/cgroup # mkdir /sys/fs/cgroup/memory # mount -t cgroup none /sys/fs/cgroup/memory -o memory
创建新组并将 bash 移入其中
# mkdir /sys/fs/cgroup/memory/0 # echo $$ > /sys/fs/cgroup/memory/0/tasks
既然我们处于 0 cgroup 中,我们可以修改内存限制
# echo 4M > /sys/fs/cgroup/memory/0/memory.limit_in_bytes
现在可以查询限制
# cat /sys/fs/cgroup/memory/0/memory.limit_in_bytes 4194304
注意
我们可以使用后缀(k、K、m、M、g 或 G)来表示千字节、兆字节或吉字节的值。(这里,Kilo、Mega、Giga 是 Kibibytes、Mebibytes、Gibibytes。)
注意
我们可以写入“-1”来重置 *.limit_in_bytes(unlimited)
。
注意
我们不能再对根 cgroup 设置限制。
我们可以检查使用情况
# cat /sys/fs/cgroup/memory/0/memory.usage_in_bytes
1216512
成功写入此文件并不能保证成功将此限制设置为写入文件中的值。这可能由于多种因素造成,例如向上舍入到页面边界或系统上内存的总可用性。用户需要在写入后重新读取此文件,以保证内核提交的值
# echo 1 > memory.limit_in_bytes
# cat memory.limit_in_bytes
4096
memory.failcnt 字段表示 cgroup 限制被超过的次数。
memory.stat 文件提供记账信息。现在,显示了缓存、RSS 以及活跃页/非活跃页的数量。
4. 测试¶
有关测试功能和实现,请参阅 内存资源控制器 (Memcg) 实现备忘录。
性能测试也很重要。为了查看纯内存控制器的开销,在 tmpfs 上进行测试将为您提供少量开销的良好数据。示例:在 tmpfs 上执行内核 make。
页面故障可伸缩性也很重要。在测量并行页面故障测试时,多进程测试可能优于多线程测试,因为它具有共享对象/状态的噪声。
但以上两种都属于极端情况测试。在内存控制器下进行常规测试总是有帮助的。
4.1 故障排除¶
有时用户可能会发现 cgroup 下的应用程序被 OOM killer 终止。这有几个原因:
cgroup 限制太低(低到无法进行任何有用的操作)
用户正在使用匿名内存,并且交换已关闭或太低
执行 sync 后再执行 echo 1 > /proc/sys/vm/drop_caches 将有助于清除 cgroup 中缓存的一些页面(页面缓存页)。
要了解发生了什么,可以按照 “10. OOM 控制”(下文)禁用 OOM_Kill 并观察结果,这将很有帮助。
4.2 任务迁移¶
当一个任务从一个 cgroup 迁移到另一个 cgroup 时,其费用默认不会被带走。从原始 cgroup 分配的页面仍然由其计费,当页面被释放或回收时,费用才会被取消。
您可以将任务的费用随任务迁移一同移动。参见 8. “任务迁移时移动费用”
4.3 移除 cgroup¶
cgroup 可以通过 rmdir 移除,但如 第 4.1 节 和 第 4.2 节 所述,即使所有任务都已从中迁移,cgroup 可能仍然与某些费用相关联。(因为我们是按页面计费,而不是按任务计费。)
我们将统计信息移动到父级,除了取消子级的计费外,计费没有变化。
cgroup 移除时,交换信息中记录的费用不会更新。记录的信息将被丢弃,使用交换(swapcache)的 cgroup 将被记为新的所有者。
5. 其他接口¶
5.1 force_empty¶
提供了 memory.force_empty 接口以清空 cgroup 的内存使用量。当向此写入任何内容时,
# echo 0 > memory.force_emptycgroup 将被回收,并尽可能多地回收页面。
此接口的典型用例是在调用 rmdir() 之前。尽管 rmdir() 会下线 memcg,但由于已计费的文件缓存,memcg 仍可能存在。一些不用的页面缓存可能会保持计费状态,直到发生内存压力。如果您想避免这种情况,force_empty 将会很有用。
5.2 stat 文件¶
memory.stat 文件包含以下统计信息:
每个内存 cgroup 的本地状态
缓存 (cache)
页面缓存内存的字节数。
常驻集大小 (rss)
匿名和交换缓存内存的字节数(包括透明大页)。
rss_huge
匿名透明大页的字节数。
映射文件 (mapped_file)
映射文件的字节数(包括 tmpfs/shmem)
pgpgin
内存 cgroup 的计费事件次数。每次页面被记为映射匿名页 (RSS) 或缓存页 (Page Cache) 到 cgroup 时,都会发生计费事件。
pgpgout
内存 cgroup 的解除计费事件次数。每次页面从 cgroup 中解除计费时,都会发生解除计费事件。
交换 (swap)
交换使用量的字节数
swapcached
内存中缓存的交换页字节数
脏页 (dirty)
等待写回磁盘的字节数。
回写 (writeback)
排队等待同步到磁盘的文件/匿名缓存的字节数。
inactive_anon
非活跃 LRU 列表上的匿名和交换缓存内存的字节数。
active_anon
活跃 LRU 列表上的匿名和交换缓存内存的字节数。
inactive_file
非活跃 LRU 列表上的文件支持内存和 MADV_FREE 匿名内存(LazyFree 页)的字节数。
active_file
活跃 LRU 列表上的文件支持内存的字节数。
不可回收 (unevictable)
无法回收的内存字节数(mlocked 等)。
考虑层次结构的状态(参见 memory.use_hierarchy 设置)
hierarchical_memory_limit
内存 cgroup 所属层次结构下的内存限制字节数
hierarchical_memsw_limit
内存 cgroup 所属层次结构下的内存+交换限制字节数。
total_<counter>
是
的分层版本,除了 cgroup 自己的值外,还包括所有分层子 cgroup 的 值之和,例如 total_cache 附加 VM 参数(取决于 CONFIG_DEBUG_VM)
recent_rotated_anon
VM 内部参数。(参见 mm/vmscan.c)
recent_rotated_file
VM 内部参数。(参见 mm/vmscan.c)
recent_scanned_anon
VM 内部参数。(参见 mm/vmscan.c)
recent_scanned_file
VM 内部参数。(参见 mm/vmscan.c)
提示
recent_rotated 表示 LRU 轮换的最近频率。recent_scanned 表示最近对 LRU 的扫描次数。显示这些是为了更好的调试,请参阅代码以了解其含义。
注意
只有匿名和交换缓存内存被列为“rss”统计的一部分。这不应与真正的“常驻集大小”或 cgroup 使用的物理内存量混淆。
“rss + mapped_file”将给出 cgroup 的常驻集大小。
请注意,某些内核配置可能会将完整的较大分配(例如 THP)计入“rss”和“mapped_file”,即使只有部分而非全部内存被映射。
(注意:文件和共享内存可能在其他 cgroup 之间共享。在这种情况下,只有当内存 cgroup 是页面缓存的所有者时,才会记账 mapped_file。)
5.3 swappiness¶
覆盖特定组的 /proc/sys/vm/swappiness。根 cgroup 中的可调参数对应于全局 swappiness 设置。
请注意,与全局回收不同,限制回收强制规定 0 swappiness 确实能阻止任何交换,即使存在交换存储。如果文件页无可回收,这可能导致 memcg OOM killer。
5.4 failcnt¶
内存 cgroup 提供 memory.failcnt 和 memory.memsw.failcnt 文件。此 failcnt(== 失败计数)显示使用计数器达到其限制的次数。当内存 cgroup 达到限制时,failcnt 会增加,并且其下的内存将被回收。
您可以通过向 failcnt 文件写入 0 来重置 failcnt
# echo 0 > .../memory.failcnt
5.5 usage_in_bytes¶
为了效率,与其他内核组件一样,内存 cgroup 使用了一些优化来避免不必要的缓存行错误共享。usage_in_bytes 受此方法影响,不会显示内存(和交换)使用量的“精确”值,它是一个用于高效访问的模糊值。(当然,必要时会同步。)如果您想了解更精确的内存使用量,应该使用 memory.stat 中的 RSS+CACHE(+SWAP) 值(参见 5.2)。
5.6 numa_stat¶
这与 numa_maps 类似,但按每个 memcg 操作。这对于提供 memcg 内的 numa 局部性信息可见性很有用,因为页面可以从任何物理节点分配。其中一个用例是通过将此信息与应用程序的 CPU 分配结合起来评估应用程序性能。
每个 memcg 的 numa_stat 文件包括每个节点的“total”、“file”、“anon”和“unevictable”页面计数,以及“hierarchical_<counter>”,后者除了 memcg 自己的值外,还汇总了所有层次子节点的值。
memory.numa_stat 的输出格式是
total=<total pages> N0=<node 0 pages> N1=<node 1 pages> ...
file=<total file pages> N0=<node 0 pages> N1=<node 1 pages> ...
anon=<total anon pages> N0=<node 0 pages> N1=<node 1 pages> ...
unevictable=<total anon pages> N0=<node 0 pages> N1=<node 1 pages> ...
hierarchical_<counter>=<counter pages> N0=<node 0 pages> N1=<node 1 pages> ...
“total”计数是 file + anon + unevictable 的总和。
6. 层次结构支持¶
内存控制器支持深层层次结构和分层记账。层次结构是通过在 cgroup 文件系统中创建适当的 cgroup 来创建的。例如,考虑以下 cgroup 文件系统层次结构:
root
/ | \
/ | \
a b c
| \
| \
d e
在上图中,启用分层记账后,e 的所有内存使用量都会记账到其所有祖先(即 c 和根)。如果其中一个祖先超过其限制,回收算法会从祖先及其子任务中回收内存。
6.1 分层记账和回收¶
分层记账默认启用。禁用分层记账已弃用。尝试禁用将导致失败,并向 dmesg 打印警告。
出于兼容性原因,向 memory.use_hierarchy 写入 1 始终会通过
# echo 1 > memory.use_hierarchy
7. 软限制(已弃用)¶
此功能已弃用!
软限制允许更大程度地共享内存。软限制背后的理念是允许控制组根据需要使用尽可能多的内存,前提是:
没有内存争用
它们不超过其硬限制
当系统检测到内存争用或内存不足时,控制组将被推回到它们的软限制。如果每个控制组的软限制非常高,它们将被尽可能地推回,以确保一个控制组不会使其他控制组的内存饿死。
请注意,软限制是一个尽力而为的功能;它不提供任何保证,但它会尽力确保在内存严重争用时,根据软限制提示/设置分配内存。目前,基于软限制的回收设置为从 balance_pgdat (kswapd) 调用。
7.1 接口¶
软限制可以通过以下命令设置(在此示例中,我们假设软限制为 256 MiB):
# echo 256M > memory.soft_limit_in_bytes
如果我们想将其更改为 1G,我们可以随时使用:
# echo 1G > memory.soft_limit_in_bytes
注意
软限制会在很长一段时间内生效,因为它们涉及回收内存以在内存 cgroup 之间进行平衡
注意
建议将软限制始终设置在硬限制之下,否则硬限制将优先。
8. 任务迁移时移动费用(已弃用!)¶
此功能已弃用!
读取 memory.move_charge_at_immigrate 将始终返回 0,写入它将始终返回 -EINVAL。
9. 内存阈值¶
内存 cgroup 使用 cgroup 通知 API(参见 控制组)实现内存阈值。它允许注册多个内存和 memsw 阈值,并在超过阈值时获取通知。
要注册阈值,应用程序必须:
使用 eventfd(2) 创建一个 eventfd;
打开 memory.usage_in_bytes 或 memory.memsw.usage_in_bytes;
向 cgroup.event_control 写入字符串,如 “<event_fd> <fd of memory.usage_in_bytes> <threshold>”。
当内存使用量向任何方向超过阈值时,应用程序将通过 eventfd 收到通知。
它适用于根 cgroup 和非根 cgroup。
10. OOM 控制(已弃用)¶
此功能已弃用!
memory.oom_control 文件用于 OOM 通知和其他控制。
内存 cgroup 使用 cgroup 通知 API(参见 控制组)实现 OOM 通知器。它允许注册多个 OOM 通知传递,并在 OOM 发生时获取通知。
要注册通知器,应用程序必须:
使用 eventfd(2) 创建一个 eventfd
打开 memory.oom_control 文件
向 cgroup.event_control 写入字符串,如 “<event_fd> <fd of memory.oom_control>”
当 OOM 发生时,应用程序将通过 eventfd 收到通知。OOM 通知不适用于根 cgroup。
您可以通过向 memory.oom_control 文件写入“1”来禁用 OOM-killer,如下所示:
#echo 1 > memory.oom_control
如果 OOM-killer 被禁用,cgroup 下的任务在请求可记账内存时将挂起/休眠在内存 cgroup 的 OOM-waitqueue 中。
要运行它们,您必须通过以下方式缓解内存 cgroup 的 OOM 状态:
增大限制或减少使用量。
要减少使用量:
杀死一些任务。
通过账户迁移将一些任务移动到其他组。
删除一些文件(在 tmpfs 上?)
然后,停止的任务将再次工作。
读取时,将显示 OOM 的当前状态。
oom_kill_disable 0 或 1(如果为 1,则 OOM-killer 已禁用)
under_oom 0 或 1(如果为 1,则内存 cgroup 处于 OOM 状态,任务可能会停止。)
oom_kill 整数计数器 被任何 OOM killer 杀死的属于此 cgroup 的进程数量。
11. 内存压力(已弃用)¶
此功能已弃用!
压力级别通知可用于监控内存分配成本;应用程序可以根据压力实现不同的内存资源管理策略。压力级别定义如下:
“低”级别表示系统正在为新分配回收内存。监控这种回收活动可能有助于维持缓存级别。收到通知后,程序(通常是“Activity Manager”)可能会分析 vmstat 并提前采取行动(即提前关闭不重要的服务)。
“中”级别表示系统正在经历中等内存压力,系统可能正在进行交换、将活跃文件缓存分页出等。在此事件发生时,应用程序可以决定进一步分析 vmstat/zoneinfo/memcg 或内部内存使用统计信息,并释放任何可以轻松重建或从磁盘重新读取的资源。
“临界”级别表示系统正在积极地抖动,即将内存不足 (OOM),甚至内核中的 OOM killer 正在准备触发。应用程序应该尽其所能帮助系统。此时咨询 vmstat 或任何其他统计信息可能为时已晚,因此建议立即采取行动。
默认情况下,事件会向上传播,直到事件被处理,即事件不是直通的。例如,您有三个 cgroup:A->B->C。现在您在 cgroup A、B 和 C 上设置了一个事件监听器,并且假设 cgroup C 遇到了一些压力。在这种情况下,只有 cgroup C 会收到通知,即 cgroup A 和 B 不会收到。这样做是为了避免过多的消息“广播”,这会干扰系统,尤其是在内存不足或抖动时更糟糕。只有当 cgroup C 没有事件监听器时,cgroup B 才会收到通知。
有三种可选模式,用于指定不同的传播行为:
“default”:“默认”:这是上面指定的默认行为。此模式与省略可选模式参数相同,以保持向后兼容性。
“hierarchy”:“层次结构”:事件总是传播到根目录,类似于默认行为,不同之处在于,无论每个级别是否存在事件监听器,传播都会继续,采用“hierarchy”模式。在上述示例中,组 A、B 和 C 将收到内存压力通知。
“local”:“本地”:事件是直通的,即只有当注册了通知的 memcg 中出现内存压力时,它们才会收到通知。在上述示例中,如果组 C 注册了“本地”通知并且该组遇到内存压力,则组 C 将收到通知。但是,无论组 C 是否有事件监听器,如果组 B 注册了本地通知,组 B 将永远不会收到通知。
级别和事件通知模式(如果需要,可以是“hierarchy”或“local”)由逗号分隔的字符串指定,例如“low,hierarchy”指定所有祖先 memcg 的分层、直通通知。默认的、非直通行为的通知不指定模式。“medium,local”指定中等级别的直通通知。
文件 memory.pressure_level 仅用于设置 eventfd。要注册通知,应用程序必须:
使用 eventfd(2) 创建一个 eventfd;
打开 memory.pressure_level;
向 cgroup.event_control 写入字符串,例如 “<event_fd> <fd of memory.pressure_level> <level[,mode]>”。
当内存压力达到特定级别(或更高)时,应用程序将通过 eventfd 收到通知。对 memory.pressure_level 的读/写操作未实现。
测试
这是一个小脚本示例,它创建一个新的 cgroup,设置内存限制,在 cgroup 中设置通知,然后使子 cgroup 经历临界压力:
# cd /sys/fs/cgroup/memory/ # mkdir foo # cd foo # cgroup_event_listener memory.pressure_level low,hierarchy & # echo 8000000 > memory.limit_in_bytes # echo 8000000 > memory.memsw.limit_in_bytes # echo $$ > tasks # dd if=/dev/zero | read x(预期会收到大量通知,最终会触发 OOM-killer。)
12. 待办事项¶
使每个 cgroup 扫描器首先回收非共享页面
教控制器记账共享页面
当尚未达到限制但使用量正在接近时,在后台启动回收
总结¶
总的来说,内存控制器一直是一个稳定的控制器,并且在社区中得到了广泛的评论和讨论。
参考资料¶
Menage, Paul. 控制组 v10, http://lwn.net/Articles/236032/
Vaidyanathan, Srinivasan. 控制组:页面缓存记账和控制子系统 (v3), http://lwn.net/Articles/235534/
Singh, Balbir. RSS 控制器 v2 测试结果 (lmbench), https://lore.kernel.org/r/464C95D4.7070806@linux.vnet.ibm.com
Singh, Balbir. RSS 控制器 v2 AIM9 结果 https://lore.kernel.org/r/464D267A.50107@linux.vnet.ibm.com
Singh, Balbir. 内存控制器 v6 测试结果, https://lore.kernel.org/r/20070819094658.654.84837.sendpatchset@balbir-laptop
Singh, Balbir. 内存控制器介绍 (v6), https://lore.kernel.org/r/20070817084228.26003.12568.sendpatchset@balbir-laptop
Corbet, Jonathan. 控制 cgroup 中的内存使用, http://lwn.net/Articles/243795/