内存资源控制器(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 换入。在换入时,页面从交换缓存中获取。有两种情况。

  1. 如果 SwapCache 是新分配和读取的,它没有计费。

  2. 如果 SwapCache 已经被进程映射,它已经被计费。

4.2 换出。在换出时,典型的状态转换如下。

  1. 添加到交换缓存。(标记为 SwapCache) swp_entry 的引用计数 += 1。

  2. 完全解除映射。swp_entry 的引用计数 += # of ptes。

  3. 写回交换区。

  4. 从交换缓存中删除。(从 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 也是一个好主意。