記憶體資源控制器(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 也是一個好主意。