内存资源控制器 (Memcg) 实现备忘录¶
最后更新:2010/2
基本内核版本:基于 2.6.33-rc7-mm(34 的候选版本)。
由于 VM 变得复杂(原因之一是 memcg ...),memcg 的行为也很复杂。本文档用于说明 memcg 的内部行为。请注意,实现细节可能会发生更改。
(*) 关于 API 的主题应在 内存资源控制器 中)
0. 如何记录使用情况?¶
使用 2 个对象。
page_cgroup .... 每个页面的对象。
在启动时或内存热插拔时分配。在内存热移除时释放。
swap_cgroup ... 每个 swp_entry 的一个条目。
在 swapon() 时分配。在 swapoff() 时释放。
page_cgroup 具有 USED 位,并且永远不会发生针对 page_cgroup 的重复计数。swap_cgroup 仅在已收费的页面被换出时使用。
1. 收费¶
页面/swp_entry 可能在以下位置被收费(usage += PAGE_SIZE):
mem_cgroup_try_charge()
2. 取消收费¶
页面/swp_entry 可能被以下操作取消收费(usage -= PAGE_SIZE):
- mem_cgroup_uncharge()
当页面的 refcount 降至 0 时调用。
- mem_cgroup_uncharge_swap()
当 swp_entry 的 refcnt 降至 0 时调用。针对交换区的收费消失。
3. 收费-提交-取消¶
Memcg 页面分两步收费
mem_cgroup_try_charge()
在 try_charge() 中,没有标志表示“此页面已收费”。此时,usage += PAGE_SIZE。
在 commit() 中,页面与 memcg 关联。
在 cancel() 中,简单地 usage -= PAGE_SIZE。
在以下解释中,我们假设 CONFIG_SWAP=y。
4. 匿名¶
- 匿名页面在新分配时
页面错误进入 MAP_ANONYMOUS 映射。
写时复制。
4.1 换入。在换入时,页面从交换缓存中获取。有两种情况。
如果 SwapCache 是新分配的并读取,则它没有收费。
如果 SwapCache 已被进程映射,则已收费。
4.2 换出。在换出时,典型的状态转换如下。
添加到交换缓存。(标记为 SwapCache)swp_entry 的 refcnt += 1。
完全取消映射。swp_entry 的 refcnt += pte 的数量。
写回到交换区。
从交换缓存中删除。(从 SwapCache 中删除)swp_entry 的 refcnt -= 1。
最后,在任务退出时,(e) 调用 zap_pte(),swp_entry 的 refcnt -=1 -> 0。
5. 页面缓存¶
页面缓存在 - filemap_add_folio() 时收费。
逻辑非常清晰。(关于迁移,请参见下文)
- 注意
__filemap_remove_folio() 由 filemap_remove_folio() 和 __remove_mapping() 调用。
6. Shmem(tmpfs) 页面缓存¶
理解 shmem 页面状态转换的最佳方法是阅读 mm/shmem.c。
但是,对 shmem 周围 memcg 行为的简要解释将有助于理解逻辑。
Shmem 的页面(仅是叶子页面,而不是直接/间接块)可以在以下位置:
shmem inode 的 radix-tree 中。
SwapCache。
同时在 radix-tree 和 SwapCache 上。这发生在换入和换出时,
在以下情况下收费...
新页面添加到 shmem 的 radix-tree。
读取 swp 页面。(将费用从 swap_cgroup 转移到 page_cgroup)
7. 页面迁移¶
8. LRU¶
每个 memcg 都有自己的 LRU 向量(非活动匿名、活动匿名、非活动文件、活动文件、不可驱逐)来自每个节点的页面,每个 LRU 在该 memcg 和节点的单个 lru_lock 下处理。
9. 典型测试。¶
针对竞争情况的测试。
9.1 对 memcg 的小限制。¶
当您进行测试以处理竞争情况时,最好将 memcg 的限制设置为非常小,而不是 GB。在 xKB 或 xxMB 限制下的测试中发现了很多竞争。
(GB 下的内存行为和 MB 下的内存行为显示的情况非常不同。)
9.2 Shmem¶
从历史上看,memcg 的 shmem 处理效果很差,我们在这里看到了一些麻烦。这是因为 shmem 是页面缓存,但可以是 SwapCache。使用 shmem/tmpfs 进行测试始终是很好的测试。
9.3 迁移¶
对于 NUMA,迁移是另一个特殊情况。为了进行简单的测试,cpuset 非常有用。以下是执行迁移的示例脚本
mount -t cgroup -o cpuset none /opt/cpuset mkdir /opt/cpuset/01 echo 1 > /opt/cpuset/01/cpuset.cpus echo 0 > /opt/cpuset/01/cpuset.mems echo 1 > /opt/cpuset/01/cpuset.memory_migrate mkdir /opt/cpuset/02 echo 1 > /opt/cpuset/02/cpuset.cpus echo 1 > /opt/cpuset/02/cpuset.mems echo 1 > /opt/cpuset/02/cpuset.memory_migrate在上述集合中,当您将任务从 01 移动到 02 时,将发生页面从节点 0 到节点 1 的迁移。以下是将所有内容迁移到 cpuset 下的脚本。
-- move_task() { for pid in $1 do /bin/echo $pid >$2/tasks 2>/dev/null echo -n $pid echo -n " " done echo END } G1_TASK=`cat ${G1}/tasks` G2_TASK=`cat ${G2}/tasks` move_task "${G1_TASK}" ${G2} & --
9.4 内存热插拔¶
内存热插拔测试是很好的测试之一。
要离线内存,请执行以下操作
# echo offline > /sys/devices/system/memory/memoryXXX/state(XXX 是内存的位置)
这也是测试页面迁移的一种简单方法。
9.5 嵌套 cgroup¶
使用以下测试来测试嵌套 cgroup
mkdir /opt/cgroup/01/child_a mkdir /opt/cgroup/01/child_b set limit to 01. add limit to 01/child_b run jobs under child_a and child_b在作业运行时随机创建/删除以下组
/opt/cgroup/01/child_a/child_aa /opt/cgroup/01/child_b/child_bb /opt/cgroup/01/child_c在新组中运行新作业也是不错的选择。
9.6 使用其他子系统挂载¶
与其他子系统一起挂载是一个很好的测试,因为存在与其他 cgroup 子系统的竞争和锁定依赖关系。
示例
# mount -t cgroup none /cgroup -o cpuset,memory,cpu,devices并在此下执行任务移动、mkdir、rmdir 等操作。
9.7 swapoff¶
除了交换区的管理是 memcg 的复杂部分之一之外,swapoff 的换入调用路径与通常的换入路径不同。值得明确测试。
例如,像下面这样的测试是不错的
(Shell-A)
# mount -t cgroup none /cgroup -o memory # mkdir /cgroup/test # echo 40M > /cgroup/test/memory.limit_in_bytes # echo 0 > /cgroup/test/tasks在此下运行 malloc(100M) 程序。您将看到 60M 的交换区。
(Shell-B)
# move all tasks in /cgroup/test to /cgroup # /sbin/swapoff -a # rmdir /cgroup/test # kill malloc task.当然,也应该测试 tmpfs 与 swapoff 的对比测试。
9.8 OOM 杀手¶
由 memcg 的限制导致的内存不足将杀死 memcg 下的任务。当使用层次结构时,层次结构下的任务将被内核杀死。
在这种情况下,不应调用 panic_on_oom,并且不应杀死其他组中的任务。
在 memcg 下引发 OOM 并不困难,如下所示。
情况 A)当您可以 swapoff 时
#swapoff -a #echo 50M > /memory.limit_in_bytes运行 51M 的 malloc
情况 B)当您使用内存 + 交换限制时
#echo 50M > memory.limit_in_bytes #echo 50M > memory.memsw.limit_in_bytes运行 51M 的 malloc
9.9 在任务迁移时移动费用¶
与任务关联的费用可以随着任务迁移一起移动。
(Shell-A)
#mkdir /cgroup/A #echo $$ >/cgroup/A/tasks在 /cgroup/A 中运行一些使用一定量内存的程序。
(Shell-B)
#mkdir /cgroup/B #echo 1 >/cgroup/B/memory.move_charge_at_immigrate #echo "pid of the program running in group A" >/cgroup/B/tasks您可以通过读取 A 和 B 的
*.usage_in_bytes
或 memory.stat 来查看费用是否已移动。请参阅 内存资源控制器 的 8.2,了解应写入 move_charge_at_immigrate 的值。
9.10 内存阈值¶
内存控制器使用 cgroups 通知 API 实现内存阈值。您可以使用 tools/cgroup/cgroup_event_listener.c 来测试它。
(Shell-A)创建 cgroup 并运行事件侦听器
# mkdir /cgroup/A # ./cgroup_event_listener /cgroup/A/memory.usage_in_bytes 5M(Shell-B)将任务添加到 cgroup 并尝试分配和释放内存
# echo $$ >/cgroup/A/tasks # a="$(dd if=/dev/zero bs=1M count=10)" # a=每次您跨越阈值时,您都会看到来自 cgroup_event_listener 的消息。
使用 /cgroup/A/memory.memsw.usage_in_bytes 来测试 memsw 阈值。
测试根 cgroup 也是一个好主意。