内存回收:逐出和交换

在前面的部分中,我们了解了内核如何在系统范围内管理内存。在本章中,我们将深入探讨 内存控制组 (memcg),这是 Android 用于为各个应用划分和控制内存用量的机制。

了解 memcg 至关重要,因为系统可以在此级别针对单个应用进行回收或设置内存限制,从而主动管理应用内存占用空间。

内存控制组 (memcg)

控制组 (cgroup) 是一项 Linux 内核功能,可将进程组织成分层组,并在这些组之间分配系统资源(如 CPU、内存和 I/O)。memcg 是专门用于内存的 cgroup 控制器。

关键的 memcg 概念

  • Memcg Charge:当 memcg 中的进程分配内存页(匿名或文件支持)时,该内存页会“计入”memcg。内存控制组的总费用是其中所有进程使用的所有内存页的总和。
  • 分层统计:内存用量会向上统计到树中。子 cgroup 中的费用也会计入其父 cgroup 的使用量。
  • 内存限制:每个 memcg 都可以设置限制(例如 memory.maxmemory.high),如果超出限制,则会触发回收甚至 OOM Killer,而与全局系统内存无关。
  • 按 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。您应该会看到它每次点按增加约 10MB。
  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)。这会触发系统范围内的压力。
    • 在单独的终端中,回收 200MB:

      adb shell "echo 200M > /sys/fs/cgroup/apps/uid_10274/pid_11672/memory.reclaim"
      
  4. 故障恢复:切换回 MemoryLab。点按 Thrash Pagecache(重新错误测试)

  5. 停止轨迹:在轨迹终端中按 Ctrl+C。

在 Perfetto 中分析回收

打开轨迹后,您可以看到系统范围和每个应用的回收操作:

Perfetto 显示直接和 memcg 回收

在轨迹中查找以下内容:

  • kswapd:在内核线程下搜索 kswapd0(在上面的屏幕截图中,它被手动固定在顶部)。您会看到,当系统难以找到空闲页面时,它会唤醒并运行(绿色切片)。
  • 直接回收:查看 com.android.memorylab 进程线程。 您会看到紫色 ftrace 事件切片(例如 mm_vmscan_direct_reclaim_begin)直接显示在线程的调度轨道下方。这表示应用线程本身因等待内核释放页面而停滞。
  • RSS 和交换计数器
    • mem.rss.anon:随着您点按分配按钮而增加。
    • mem.swap:随着 kswapd 和应用自己的线程(受直接回收影响)将这些匿名页面压缩到 ZRAM 中,该值会稳步增加。
  • memcg 回收:如果您放大手动触发回收的那一刻,会看到 rss.anonrss.file 均急剧下降,同时伴有 mm_vmscan_memcg_reclaim 事件。

← 系统级 | ↑ 向上 | kswapd 和 lmkd 互动 →