内存资源控制器 (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 页面分两步收费

在 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 的 refcnt += 1。

  2. 完全取消映射。swp_entry 的 refcnt += pte 的数量。

  3. 写回到交换区。

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