内存管理守护程序

Android 17 及更高版本支持内存管理守护程序 (mmd),这是一个系统守护程序,用于处理守护程序配置、可调参数以及正在进行的交换或 ZRAM 维护任务。

背景

在引入 mmd 之前,Android 的 ZRAM 配置非常分散,可自定义程度有限。mmd 通过集中管理 ZRAM 来解决此问题,从而实现更复杂的配置逻辑,并简化新功能和架构改进的添加。 mmd 还在基于 Java 的 system_server 进程与内核级交换或内存管理之间建立了清晰的职责分离。

架构和 ZRAM 管理

在启动完成时(即当 sys.boot_completed=1 时),mmd_setup 会尝试使用指定的参数配置 ZRAM。ZRAM 设置完成后,系统会启用 mmd 服务,该服务负责处理持续维护任务。

mmd 项目中,维护操作通过以下方式从 system_server 发起:使用 IMmd 接口向 mmd 发送 Binder 请求。mmd 根据其自身的内部政策引擎处理执行 ZRAM 回写、重新压缩和按进程回写的维护任务。ActivityManagerService 中的调度和 ZRAM 维护政策均可使用系统属性进行配置。

系统服务器集成 (system_server)

基于 Java 的 system_server 进程会确定何时调用 mmd。该流程将全局维护扫描与针对每个应用的内存优化分开。

正常的后期处理维护

全局 ZRAM 维护由 ActivityManagerService 使用 com.android.server.memory.ZramMaintenance 驱动。

zram-maintenance

图 1. ZRAM 维护调度流程。

  • 调度引擎ZramMaintenance 向 Android 的 JobScheduler 注册一个周期性后台作业。
  • 作业限制:为防止前台界面卡顿或 CPU 争用,作业明确配置了 setRequiresDeviceIdle(true)setRequiresBatteryNotLow(true)
  • Binder 触发:当调度器触发 onStartJob() 时,system_server 会调用 mmd.doZramMaintenanceAsync()。这是一个单向异步 Binder 调用;system_server 不会阻塞,等待维护扫描完成。mmd 将此任务排入后台工作器线程的队列,以按顺序执行重新压缩和回写。

按进程回写

ActivityManagerService 使用 com.android.server.am.CachedAppOptimizer 管理有针对性的按进程内存逐出。

mmd-writeback

图 2. 每个进程的 mmd 回写流程。

当进程转换到后台缓存状态时,ActivityManager 会执行内存压缩。如果进程的低内存终止对用户可见(即进程托管 Activity),并且 ZRAM 的进程回写会将进程的内存占用量降至接近于零,则系统会执行以下步骤:

  1. 压缩完成后,CachedAppOptimizer 会向其内部压缩处理程序发布延迟消息 (ZRAM_WRITEBACK_MSG)(延迟时间为 mZramWritebackWaitSeconds)。
  2. 当延迟时间到期时,ActivityManager 会打开一个安全进程文件描述符 pidfd
  3. 系统服务器调用 mmd.asyncWritebackProcessZramMemory(pfd, callback)
  4. mmd 执行每个进程的回写 ioctl,并使用 IMmdProcessWritebackCallback 进行报告。如果成功,ActivityManager 会标记进程记录 (setIsZramWrittenBack(app, true)) 以提升进程的 oom_score_adj,并将指标记录到 FrameworkStatsLog.ZRAM_WRITEBACK_EVENT

按进程预提取

当用户重新启动之前缓存的应用(因 UNFREEZE_REASON_ACTIVITY 而解冻)时,ActivityManager 会最大限度地减少因来自后备存储空间的主要页面错误而导致的应用启动延迟:

  1. CachedAppOptimizer 拦截取消冻结事件并调用 prefetchZram(app)
  2. 系统服务器使用 mmd.asyncPrefetchProcessZramMemory(pfd) 通过 Binder 调度应用的 pidfdmmd 会发出 ZRAM_ANDROID_IOC_PROCESS_PREFETCH ioctl,指示内核在应用的主界面线程初始化时异步预提取换出的页面并将其放回 RAM 中。

维护和后处理任务概览

本部分介绍了 mmd 为优化交换空间和系统内存而运行的后台维护操作和后处理任务。

以 mmd 为单位的维护

mmd 中,维护是指计划的后台维护扫描,用于优化交换空间和物理内存利用率,而不会影响活跃用户的前台性能。维护操作不是持续同步扫描(这会导致严重的 CPU 唤醒和界面卡顿),而是异步驱动的:

  1. system_server 会定期通过 Binder 触发 doZramMaintenanceAsync()

  2. mmd 将请求放入后台工作队列 LowPrioWorkItem::ZramMaintenance

  3. mmd 中有一个工作器线程,用于同时管理高优先级队列和低优先级队列。高优先级工作项(例如按进程预提取)会优先处理,并且可以抢占低优先级工作项。维护和按进程回写操作作为低优先级工作项运行。弹出时,工作器线程会按顺序执行两项主要维护操作:

    • ZRAM 重新压缩:扫描现有交换页面,并使用更高比率的辅助压缩算法(例如 zstd)重新压缩空闲页面。

    • ZRAM 回写:扫描空闲页面,并将其从 RAM 完全逐出到 /data 上文件中的后备闪存存储环路设备。

ZRAM 中的后处理任务

在 Linux 内核 ZRAM 模块和 mmd 架构中,后处理任务是指在内存页已被内核的标准回收路径(kswapd 或内存整理)换出应用于内存页的异步转换。

当页面最初被换出时,系统会优先考虑速度:它会使用快速的主压缩算法(例如 lz4),并将压缩后的页面存储在 RAM 中。不过,随着时间的推移,许多交换的页面会变得冷或空闲,例如,数小时未恢复的后台缓存应用。将冷页面留在快速、轻度压缩的 ZRAM 中效率低下。

后处理流水线

mmd 实现了一个多阶段后处理生命周期,用于优化这些网页:

mmd-page-lifecycle

图 3. mmd 页面生命周期。

  1. 阶段 1:初始换出(快速压缩):首先通过 kswapd 或应用压缩回收内存。通常,首次回收使用 lz4 等快速压缩算法执行,内容存储在 RAM 中。

  2. 第 2 阶段:空闲标记(老化和跟踪)mmd 空闲跟踪会访问内核内存跟踪 (CONFIG_ZRAM_TRACK_ENTRY_ACTIME) 或使用其软件空闲标记来跟踪页面保持未触碰状态的时间。

  3. 第 3 阶段:后处理 1 - 重新压缩(内存回收):达到重新压缩空闲时间(min_idle_secondsmax_idle_seconds)的页面会进行重新压缩。mmd 会写入 /sys/block/zram0/recompress,以指示内核解压缩 lz4 页面并使用 zstd 重新压缩。这样可以回收物理 RAM,而不会导致闪存写入磨损。

  4. 第 4 阶段:后处理 2 - 回写(逐出到闪存):如果内存压力持续存在,并且页面达到回写空闲时间(通常为 20 小时或更长时间),mmd 会触发回写。mmd 写入 /sys/block/zram0/idle/sys/block/zram0/writeback,以将压缩页面完全从 RAM 中逐出到后备闪存。

ZRAM 设置配置

mmd 会加载并处理以下 ZRAM 设置属性:

属性 使用 默认
mmd.zram.enabled mmd ZRAM 设置是否已启用。 false
mmd.zram.num_devices 要配置的 ZRAM 设备数量。对于数字 N,必须存在设备 zram0zram<N-1>,系统才会设置 sys.boot_completed=1。 可以针对每个设备配置每 ZRAM 设备列表中的属性。 1
mmd.zram.device_priority 调用 swapon 时要传递的优先级值。 未设置
mmd.zram.comp_algorithm ZRAM 压缩算法。如果未指定,则使用内核默认压缩算法。 未设置
mmd.zram.size ZRAM 设备大小(以字节为单位),或设备 RAM 大小的百分比,例如 75% 50%
mmd.zram.writeback.enabled 是否启用 ZRAM 回写。 false
mmd.zram.writeback.device_size 回写设备的大小(以字节为单位)或数据分区的大小(以百分比表示)。实际设备大小可根据数据分区上的可用空间进行调整。 1073741824(1 GiB)
mmd.zram.writeback.min_free_space_mib 设置回写设备后需要提供的最小可用空间(以 MiB 为单位)。 1536 (1.5 GiB)
mmd.zram.writeback.use_nr_tags_prop true 时,使用 mmd.zram.writeback.nr_tags 中的值来配置支持 ZRAM 回写的环回设备的队列深度。这是一种权宜措施,用于解决以下情况:无法配置供应商 SELinux 政策以允许 mmd 直接读取支持 /data 的块设备的 nr_tags false
mmd.zram.writeback.nr_tags 请参阅 mmd.zram.writeback.use_nr_tags_prop 未设置
mmd.zram.recompression.enabled 是否启用 ZRAM 重新压缩功能。 false
mmd.zram.recompression.algorithm 辅助 ZRAM 重新压缩算法。 zstd

每个 ZRAM 设备的属性

mmd.zram.num_devices 大于 1 时,可以选择按 ZRAM 设备配置特定属性,方法是将该属性设置为包含正好 mmd.zram.num_devices 个元素的以英文逗号分隔的值。这些属性包括:

  • mmd.zram.size
  • mmd.zram.comp_algorithm
  • mmd.zram.device_priority
  • mmd.zram.recompression.enabled
  • mmd.zram.recompression.huge_idle.enabled
  • mmd.zram.recompression.idle.enabled
  • mmd.zram.recompression.huge.enabled
  • mmd.zram.recompression.threshold_bytes
  • mmd.zram.recompression.algorithm
  • mmd.zram.writeback.device_size
  • mmd.zram.writeback.huge_idle.enabled
  • mmd.zram.writeback.idle.enabled
  • mmd.zram.writeback.huge.enabled

弃用现有 ZRAM 设置

虽然 Android 中仍提供 swapon_all 来设置 ZRAM 和基于磁盘的交换空间,但 mmd 是 ZRAM 管理的首选方法,可实现更轻松的配置和 ZRAM 重新压缩等高级功能。

mmd ZRAM 设置由 mmd.zram.enabled 启用时:

  • swapon_all 实现中的 ZRAM 设置变为空操作。
  • 系统会忽略叠加层 config.xml 文件中的现有 ZRAM 配置(例如 config_zramWriteback)和 ro.zram.* 回写系统属性。

ZRAM 维护可调参数

ZRAM 维护应开箱即用,您可以使用本部分中的系统属性进一步微调。

ZRAM 维护调度

这些属性控制 system_server 如何以及何时安排 ZRAM 维护任务。

属性 使用 默认
mm.zram.maintenance.first_delay_seconds 首次启动 ZRAM 维护之前的延迟时间。 3600(1 小时)
mm.zram.maintenance.periodic_delay_seconds 后续 ZRAM 维护调度之间的延迟。 3600(1 小时)
mm.zram.maintenance.require_device_idle 是否仅在设备处于空闲状态时启动 ZRAM 维护。 true
mm.zram.maintenance.require_battery_not_low 是否需要在启动 ZRAM 维护之前要求电池电量不低。 true

ZRAM 回写政策

以下参数用于控制何时将哪种类型的内存写入后备设备:

属性 使用 默认
mmd.zram.writeback.backoff_seconds 自上次回写操作以来的退避时间。 600(10 分钟)
mmd.zram.writeback.min_idle_seconds mmd.zram.writeback.max_idle_seconds 结合使用,可根据内存利用率分数计算出页面符合回写条件的空闲时间。计算出的空闲期限在两个参数之间进行指数插值,以在没有内存压力的情况下最大限度地减少工作量。 72000(20 小时)
mmd.zram.writeback.max_idle_seconds 用于根据内存利用率动态计算空闲页面年龄的最大秒数。 90000(25 小时)
mmd.zram.writeback.huge.enabled 是否启用 HUGE 页面回写。 false
mmd.zram.writeback.idle.enabled 是否启用 IDLE 页面回写。 true
mmd.zram.writeback.huge_idle.enabled 是否启用 HUGE_IDLE 页面回写。 true
mmd.zram.writeback.min_bytes 一次空闲写回中要写回的最小字节数。 5242880(5 MiB)
mmd.zram.writeback.max_bytes 一次空闲写回中要写回的最大字节数。 314572800(300 MiB)
mmd.zram.writeback.max_bytes_per_day 24 小时内要写回的最大字节数。 25769803776(24 GiB)
mmd.zram.writeback.limit.enabled 是否启用每日回写预算限额结算。 true

ZRAM 重新压缩政策

以下参数用于控制何时以及重新压缩哪种类型的内存:

属性 使用 默认
mmd.zram.recompression.backoff_seconds 自上次重新压缩以来的退避时间。 1800(30 分钟)
mmd.zram.recompression.min_idle_seconds mmd.zram.recompression.max_idle_seconds 结合使用,可根据内存利用率分数计算页面符合重新压缩条件的空闲时间。计算出的空闲期限在两个形参之间进行指数插值,以在不承受内存压力的情况下最大限度地减少工作。 7200(2 小时)
mmd.zram.recompression.max_idle_seconds 用于动态计算空闲网页年龄的最大秒数。 14400(4 小时)
mmd.zram.recompression.threshold_bytes 考虑重新压缩的 ZRAM 页面的最小大小(以字节为单位)。 1024(1 KiB)
mmd.zram.recompression.huge.enabled 是否启用 HUGE 页面重新压缩。 true
mmd.zram.recompression.idle.enabled 是否启用 IDLE 页面重新压缩。 true
mmd.zram.recompression.huge_idle.enabled 是否启用 HUGE_IDLE 页面重新压缩。 true

ZRAM 空闲页面跟踪

mmd ZRAM 维护会根据 ZRAM 页面上次访问时间的长短,将 ZRAM 页面标记为空闲。此功能需要启用 CONFIG_ZRAM_TRACK_ENTRY_ACTIMECONFIG_ZRAM_MEMORY_TRACKING 内核配置。在 GKI 内核 6.18 及更高版本中,CONFIG_ZRAM_TRACK_ENTRY_ACTIME 默认处于启用状态。在较早的内核上,它具有内存开销,并且默认情况下未启用。

如果未启用内核配置,mmd ZRAM 维护会回退到软件替代逻辑来跟踪空闲 ZRAM 页面:

  1. mmd 启动时,将所有 ZRAM 页面标记为空闲。

  2. 跳过下一次 ZRAM 维护,直到所需的退避时间过去。

  3. ZRAM 回写或重新压缩空闲页面。如果由于回写限制而导致仍有空闲页面,mmd 会在下一次维护时继续回写页面,而不会将新页面标记为空闲(跳过第 4 步)。

  4. 如果所有空闲页面都已写回,则再次将所有 ZRAM 页面标记为空闲,然后返回到第 2 步。如果 ZRAM 回写处于停用状态,则在重新压缩空闲时长过后发生 ZRAM 重新压缩时,mmd 会将所有 ZRAM 页面标记为空闲。

问题排查和验证指南

请按照以下验证步骤和问题排查程序来验证和诊断 mmd 和 ZRAM 操作。

验证 ZRAM 设置

如需验证 mmd 在启动期间是否成功配置了 ZRAM,请执行以下操作:

  1. 检查有效压缩算法和磁盘大小:

    cat /sys/block/zram0/comp_algorithm
    cat /sys/block/zram0/disksize
    
  2. 验证 mmd 系统属性和正在运行的服务状态:

    getprop | grep mmd.zram
    dumpsys -l | grep mmd
    

验证 ZRAM 维护和回写

验证 ZRAM 回写和重新压缩维护任务是否正常运行:

  1. 检查后备块设备状态:

    cat /sys/block/zram0/bd_stat
    
  2. 通过监控 /sys/block/zram0/mm_stat 检查重新压缩效率。压缩数据大小的变化应在维护周期后显示。

验证每个进程的回写

以下内容可用于验证按进程回写是否正常运行:

  • 检查 adb logcat -s mmd 是否包含成功回写的日志或失败诊断信息。

常见问题和诊断

以下是用户可能会遇到的常见错误情况:

  • WritebackDailyLimitExceeded:此错误表示已达到 mmd.zram.writeback.max_bytes_per_day 配额。发生这种情况时,mmd 会暂停空闲回写,直到 24 小时的滚动窗口推进。
  • Process prefetch or writeback failed:当 ioctl 失败时,可以在 logcat 中观察到此错误。常见原因包括:
    • EBADFESRCH:目标进程在 mmdpidfd 调度到内核之前结束。
    • ENOSPC:后备存储分区已满,或环回设备队列已耗尽。
  • ZRAM 未设置:如果 mmd 在启动时未能配置 ZRAM,可能是因为旧版 swapon_all 或供应商 init 脚本在 mmd 执行之前锁定了 /dev/block/zram0