内存资源控制器(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()
当页面的引用计数降至 0 时调用。
- mem_cgroup_uncharge_swap()
当 swp_entry 的引用计数降至 0 时调用。对交换的计费消失。
3. 计费-提交-取消¶
Memcg 页面的计费分两步进行
mem_cgroup_try_charge()
mem_cgroup_commit_charge() 或 mem_cgroup_cancel_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 的引用计数 += 1。
完全解除映射。swp_entry 的引用计数 += # of ptes。
写回交换区。
从交换缓存中删除。(从 SwapCache 中移除) swp_entry 的引用计数 -= 1。
最后,在任务退出时,调用 (e) zap_pte(),swp_entry 的引用计数 -=1 -> 0。
5. 页缓存¶
页缓存是在 filemap_add_folio() 时计费的。
逻辑非常清晰。(关于迁移,请参见下文)
- 注意
__filemap_remove_folio() 由 filemap_remove_folio() 和 __remove_mapping() 调用。
6. Shmem(tmpfs) 页缓存¶
理解 shmem 页面状态转换的最佳方式是阅读 mm/shmem.c。
但简要解释 memcg 围绕 shmem 的行为将有助于理解其逻辑。
Shmem 的页面(仅叶子页面,而非直接/间接块)可能位于
shmem inode 的基数树上。
交换缓存中。
同时在基数树和交换缓存中。这发生在换入和换出时,
它在以下情况被计费...
一个新页面被添加到 shmem 的基数树。
一个交换页被读取。(将计费从 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 是页缓存,但也可以是交换缓存。使用 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-Killer¶
由 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 也是一个好主意。