内存回收:逐出和交换

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

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

内存控制组 (memcg)

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

memcg 的关键概念

  • Memcg 扣减:当 memcg 中的进程分配一页内存(匿名或文件支持)时,该页会从 memcg 中“扣减”。memcg 的总费用是其中所有进程使用的所有页面的总和。
  • 分层核算:内存用量会沿树状结构向上核算。子 cgroup 中的费用也会计入其父 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. 点按分配 Java 内存 (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. 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 中,点按分配原生内存 (1GB)。这将触发系统级压力。
    • 在另一个终端中,回收 200MB:

      adb shell "echo 200M > /sys/fs/cgroup/apps/uid_10274/pid_11672/memory.reclaim"
      
  4. Fault Back In:切换回 MemoryLab。点按 Thrash Pagecache(Refault 测试)。

  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.anon 和 rss.file 急剧下降,并伴有 mm_vmscan_memcg_reclaim 事件。

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