記憶體回收:逐出和交換

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

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

記憶體控制群組 (memcg)

控制群組 (cgroups) 是 Linux 核心功能,可將程序整理成階層式群組,並在這些群組之間分配系統資源 (例如 CPU、記憶體和 I/O)。memcg 是專為記憶體設計的 cgroup 控制器。

memcg 重要概念

  • Memcg Charge:當 memcg 中的程序分配記憶體頁面 (匿名或檔案支援) 時,該頁面會「計入」memcg。memcg 的總費用是其中所有程序使用的所有頁面總和。
  • 階層式會計:記憶體用量會計入樹狀結構。子項 Cgroup 的費用也會計入父項的使用量。
  • 記憶體限制:每個 memcg 都可以設有限制 (例如 memory.maxmemory.high),如果超出限制,就會觸發回收程序,甚至啟動 OOM 終止程序,不受全域系統記憶體影響。
  • 每個 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)」(配置 Java 記憶體 (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 應用程式凍結功能可確保快取應用程式在未執行時,盡可能減少耗用 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. 開始追蹤:使用這個內嵌追蹤設定,擷取排程、回收和網頁快取事件。

    # 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 和交換計數器
    • mem.rss.anon:輕觸分配按鈕時會增加。
    • mem.swap:隨著 kswapd 和應用程式本身的執行緒 (須直接回收) 將匿名頁面壓縮到 ZRAM,這個值會穩定增加。
  • memcg Reclaim:如果放大手動觸發回收的時刻,您會看到 rss.anonrss.file 雙雙急遽下降,並伴隨 mm_vmscan_memcg_reclaim 事件。

← 系統層級 | ↑ 向上 | kswapd 和 lmkd 互動 →