内存管理守护程序

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

背景

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

架构和 ZRAM 管理

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

借助 mmd 项目,维护操作通过使用 IMmd 接口向 mmd 发送 Binder 请求,从 system_server 发起。 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.enabled 启用 mmd ZRAM 设置时:

  • swapon_all 实现中的 ZRAM 设置会变为无操作。
  • 现有 ZRAM 配置(例如叠加层 config.xml 文件中的 config_zramWritebackro.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 回写已停用,mmd 会在 ZRAM 重新压缩空闲时间过后发生 ZRAM 重新压缩时,将所有 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:目标进程在 mmd 可以将 pidfd 分派给内核之前结束。
    • ENOSPC:后备存储分区已满,或环回设备队列已用尽。
  • ZRAM 未设置: 如果 mmd 无法在启动时配置 ZRAM,可能是因为旧版 swapon_all 或供应商 init 脚本在 mmd 可以执行之前锁定了 /dev/block/zram0