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