記憶體回收:逐出和交換

在先前章節中,我們已瞭解核心如何管理整個系統的記憶體。本章將深入探討記憶體控制群組 (memcg),這是 Android 用於分割及控制個別應用程式記憶體用量的機制。

瞭解 memcg 至關重要,因為系統可以針對個別應用程式進行回收或設定記憶體限制,進而主動管理應用程式的記憶體用量。

記憶體控制群組 (memcg)

控制群組 (cgroup) 是 Linux 核心功能,可將程序組織成階層群組,並在群組之間分配系統資源 (例如 CPU、記憶體和 I/O)。memcg 是專門用於記憶體的 cgroup 控制器。

memcg 重要概念

  • Memcg Charge:當 memcg 中的程序分配記憶體頁面 (匿名或檔案備份) 時,該頁面會「計入」memcg。memcg 的總費用是其中所有程序使用的所有頁面總和。
  • 階層式記帳:記憶體用量會向上計入樹狀結構。子 cgroup 的費用也會計入父項的使用量。
  • 記憶體限制:每個 memcg 都支援透過 memory.high 設定軟回收門檻,應用程式記憶體預算會使用這個門檻觸發指定回收和分配節流,而不會叫用 OOM 終止器。此外,它也支援透過 memory.max 設定硬性上限,當回收機制無法將用量控制在上限內時,系統記憶體限制會使用這個上限來終止失控的程序。
  • 個別 memcg 回收:當 memcg 超出限制,或系統需要記憶體時,核心可以指定特定 memcg 進行回收。也就是說,系統會移除其檔案頁面,或將其匿名頁面交換至 ZRAM。

Android memcg 階層

Android 會使用特定階層管理應用程式程序。這種結構可讓系統對不同類型的應用程式 (例如前景與背景) 套用不同政策。

Android memcg 階層

  • /sys/fs/cgroup/apps/:所有 Android 應用程式的根目錄。
  • uid_<UID>/:每個應用程式使用者 ID 的目錄。屬於同一個應用程式套件的所有程序都會共用這個群組。
  • pid_<PID>/:每個個別程序和任何從中分支的子程序的目錄。這可讓您精細控管及計算具有多個程序的應用程式。

實作練習:探索 memcg

在本練習中,您將找到 MemoryLab 的 memcg 目錄,並即時觀察其記憶體用量。

1. 啟動 MemoryLab

確認裝置正在執行 MemoryLab。

2. 尋找 MemoryLab 的 memcg

首先,取得執行中 MemoryLab 應用程式的 PID:

adb shell pidof com.android.memorylab
# Example output: 11672

現在,請找出其 cgroup 目錄。您可以在 /proc/<PID>/cgroup 中找到這項資訊:

adb shell cat /proc/11672/cgroup
# Example output: 0::/apps/uid_10274/pid_11672

在 cgroup v2 中,0:: 後方的路徑代表相對於 /sys/fs/cgroup 的 memcg 階層。因此,完整路徑為 /sys/fs/cgroup/apps/uid_10274/pid_11672/。

3. 讀取 memcg 統計資料

進入該目錄並查看金鑰檔案:

# Current memory usage (in bytes)
adb shell cat /sys/fs/cgroup/apps/uid_10274/pid_11672/memory.current
# Example output:
# 1591324672

memory.current 顯示目前向這個 cgroup 收取的記憶體總量 (以位元組為單位)。

# Detailed statistics
adb shell cat /sys/fs/cgroup/apps/uid_10274/pid_11672/memory.stat | head -n 10
# Example output:
# anon 1585098752
# file 741376
# kernel 5484544
# kernel_stack 393216
# pagetables 4583424
# sec_pagetables 0
# percpu 216
# sock 0
# vmalloc 4096
# shmem 20480

memory.stat 提供細目:* anon:匿名記憶體 (堆積、堆疊) 的數量。file:檔案支援的記憶體量 (頁面快取)。* swap:交換至 ZRAM 的記憶體量。

4. 觀察變化

  1. 開啟 MemoryLab。
  2. 記下 memory.current 的值。
  3. 輕觸「Allocate Java Memory (10MB)」數次。
  4. 請再次閱讀memory.current。每次輕觸後,記憶體用量應該會增加約 10 MB。
  5. 輕觸「Allocate Bitmaps」。查看 memory.stat,瞭解 anon 增幅。

使用 memory.reclaim 主動回收

memcg (v2) 的強大功能之一是 memory.reclaim 檔案。將值寫入這個檔案,核心就會立即嘗試從這個 memcg 和其下的任何 memcg 回收該數量的記憶體。

Android 如何使用memory.reclaim

Android 的 CachedAppOptimizer 會使用這項功能,在應用程式凍結後盡可能回收記憶體。Android App Freezer 可確保快取的應用程式在未執行時,盡可能減少 RAM 消耗量。當應用程式移至背景並凍結時,系統會將應用程式目前的記憶體用量寫入其 memory.reclaim 檔案。這會強制核心移除所有可能的檔案頁面,並將所有匿名頁面交換至 ZRAM,盡可能減少應用程式的常駐記憶體用量。

您可以讀取 memory.current 並寫回 memory.reclaim,手動達到相同的「最大回收」效果:

# Force reclaim of everything
adb shell "cat /sys/fs/cgroup/apps/uid_10274/pid_11672/memory.current > /sys/fs/cgroup/apps/uid_10274/pid_11672/memory.reclaim"

練習:強制回收和追蹤

現在,我們會強制核心從 MemoryLab 回收記憶體,並在 Perfetto 追蹤記錄中擷取活動。

  1. 準備:確保 MemoryLab 有一些配置 (Java 和點陣圖)。
  2. Start Tracing:使用這個內嵌追蹤設定,擷取排程、回收和頁面快取事件。

    # Use a config that captures scheduling, reclaim and page cache events
    adb shell perfetto -c - --txt -o /data/misc/perfetto-traces/reclaim.perfetto-trace <<EOF
    buffers: {
        size_kb: 131072
        fill_policy: RING_BUFFER
    }
    data_sources: {
        config {
            name: "linux.ftrace"
            ftrace_config {
                ftrace_events: "sched/sched_switch"
                ftrace_events: "sched/sched_wakeup"
                ftrace_events: "kmem/rss_stat"
                ftrace_events: "mm_filemap_add_to_page_cache"
                ftrace_events: "mm_filemap_delete_from_page_cache"
                ftrace_events: "vmscan/mm_vmscan_direct_reclaim_begin"
                ftrace_events: "vmscan/mm_vmscan_direct_reclaim_end"
                ftrace_events: "vmscan/mm_vmscan_memcg_reclaim_begin"
                ftrace_events: "vmscan/mm_vmscan_memcg_reclaim_end"
                symbolize_ksyms: true
            }
        }
    }
    data_sources: {
        config {
            name: "linux.process_stats"
            process_stats_config {
                scan_all_processes_on_start: true
            }
        }
    }
    EOF
    
  3. 觸發壓力與回收:

    • 在 MemoryLab 中,輕觸「Allocate Native Memory (1GB)」。這會觸發系統層級的壓力。
    • 在另一個終端機中,回收 200 MB:

      adb shell "echo 200M > /sys/fs/cgroup/apps/uid_10274/pid_11672/memory.reclaim"
      
  4. Fault Back In:切換回 MemoryLab。輕觸「Thrash Pagecache (Refault test)」。

  5. 停止追蹤:在追蹤記錄終端機中按下 Ctrl+C 鍵。

在 Perfetto 中分析回收

開啟追蹤記錄時,您可以看到系統層級和個別應用程式的回收作業:

Perfetto 顯示直接和 memcg 回收

在追蹤記錄中尋找下列項目:

  • kswapd:在「Kernel threads」下方搜尋 kswapd0 (在上方螢幕截圖中,該項目已手動固定在頂端)。當系統難以找到可用頁面時,您會看到它喚醒並執行 (綠色切片)。
  • 直接回收:查看 com.android.memorylab 程序執行緒。您會看到紫色的 ftrace 事件切片 (例如 mm_vmscan_direct_reclaim_begin) 直接顯示在執行緒的排程追蹤下方。這表示應用程式執行緒本身因等待核心釋出頁面而停滯。
  • RSS 和 Swap 計數器:
    • mem.rss.anon:輕觸分配按鈕後,這個值會增加。
    • mem.swap:隨著 kswapd 和應用程式本身的執行緒 (受直接回收影響) 將這些匿名頁面壓縮到 ZRAM,此值會穩定增加。
  • memcg 回收:如果放大觸發手動回收的時間點,會看到 rss.anon 和 rss.file 急遽下降,並伴隨 mm_vmscan_memcg_reclaim 事件。

← 整個系統 | ↑ 向上 | kswapd 和 lmkd 互動 →