Keyboard shortcuts

Press ← or → to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

Android 17 cgroup v1/v2 混合层级与进程资源隔离机制

Android 17 使用 cgroup(control group,控制组)管理 CPU 调度、CPU 集合、I/O、内存、进程冻结与进程组生命周期。平台通过 libprocessgroup 和任务配置(task profile)隐藏底层文件路径,使 framework 和 native service 只表达“后台”“top-app”“冻结”等意图。

本文沿用内核术语:controller 表示 CPU、memory 这类资源控制器,hierarchy 表示 cgroup 的父子树,cgroupfs 是把控制接口暴露成文件的虚拟文件系统。cgroup v1 可以让不同 controller 使用不同树;v2 则让接入 v2 的 controller 共用一棵统一树。task profile 是一套有稳定名称的操作配方,其中 attribute 把名称映射到控制文件,action 执行加入分组、写值或调整调度等操作,aggregate profile 再按顺序组合多个 profile。

android-17.0.0_r1 的 AOSP 默认配置仍是 cgroup v1/v2 混合拓扑。cgroup v2 的“统一层级”只约束 v2 控制器;它没有把 Android 的 v1 cpu、cpuset、blkio 自动并入同一棵树。

以下行为以 AOSP android-17.0.0_r1 和内核 android17-6.18-2026-06_r6 为准。设备厂商可以覆盖控制器版本、挂载点、子组和参数,设备实际值需要另行核对。

1. Android 17 的默认拓扑

1.1 cgroups.json 给出的基线

system/core/libprocessgroup/profiles/cgroups.json 直接定义了控制器版本与挂载点:

{
  "Cgroups": [
    { "Controller": "blkio", "Path": "/dev/blkio" },
    { "Controller": "cpu", "Path": "/dev/cpuctl" },
    { "Controller": "cpuset", "Path": "/dev/cpuset" }
  ],
  "Cgroups2": {
    "Path": "/sys/fs/cgroup",
    "Controllers": [
      { "Controller": "freezer", "Path": "." },
      {
        "Controller": "memory",
        "Path": ".",
        "NeedsActivation": true,
        "MaxActivationDepth": 3,
        "Optional": true
      }
    ]
  }
}

这段配置得到的 AOSP 基线是:

资源cgroup 版本默认挂载点Android 17 中的主要用途
CPU 调度组v1/dev/cpuctlbackground、foreground、top-app、system、rt 等组
CPU 集合v1/dev/cpuset约束线程可运行的 CPU
块 I/Ov1/dev/blkiobackground I/O 权重与优先级
freezerv2 核心接口/sys/fs/cgroup冻结 app/system 的 per-process cgroup
memoryv2,可选/sys/fs/cgroupper-process 统计、保护、限制与回收接口

Android 17 AOSP 中不存在统一的 /dev/cgroot/cpu/top-app 基线路径。CPU profile 写 /dev/cpuctl/...,cpuset profile 写 /dev/cpuset/...,freezer 和 v2 memory 则写 /sys/fs/cgroup/...。

1.2 一个进程可以同时出现在多棵树

在混合拓扑下,同一进程可以同时属于:

  • /dev/cpuctl/top-app;
  • /dev/cpuset/top-app;
  • /dev/blkio 或 /dev/blkio/background;
  • /sys/fs/cgroup/apps/uid_<uid>/pid_<pid>。

/proc/<pid>/cgroup 会按 hierarchy 列出这些归属。看到 v2 行中的 0::/apps/...,不能据此推断 CPU 与 cpuset 也已迁入 v2;必须结合 /proc/<pid>/mountinfo(该进程看到的挂载信息)或设备上的 cgroups.json 解释每一行。

Android 17 AOSP 的 cgroup v1/v2 混合归属

图:AOSP 固定基线中 CPU、cpuset、blkio 使用 v1;freezer 和可选 memory 位于 v2。箭头只表示同一任务的层级归属,设备可以覆盖版本、路径与参数。

2. init 如何建立层级

2.1 控制器挂载由 CgroupSetup() 完成

PID 1,也就是 init 进程,在 SetupCgroupsAction 中调用 CgroupSetup()。libprocessgroup/setup/cgroup_map_write.cpp 根据 descriptor(控制器配置描述)分别处理:

  • v1:以 cgroup 文件系统挂载单个控制器;
  • v2:以 cgroup2 挂载统一层级;
  • v2 memory:在允许的层级深度写 +memory 到 cgroup.subtree_control;
  • v2 根:创建 apps 和 system 两个子层级。

v2 挂载会尝试使用 memory_recursiveprot,使 memory.min/memory.low 的保护可以递归作用于子树;旧内核不支持时再回退到普通挂载。Android 17 的内核锚点支持该挂载选项。

2.2 per-UID、per-process 目录

createProcessGroup() 为进程建立 v2 cgroup。task_profiles.cpp 的路径规则为:

UID >= AID_APP_START:
/sys/fs/cgroup/apps/uid_<uid>/pid_<pid>

UID < AID_APP_START:
/sys/fs/cgroup/system/uid_<uid>/pid_<pid>

源码以 AID_APP_START(应用 UID 范围的起点)为分界选择 apps 或 system,并不是根据包名、签名或进程名判断。创建过程会建立 UID 目录、按需激活 memory controller,再建立 PID 目录,并把初始 PID 写入该目录的 cgroup.procs。这一级目录同时承载 freezer、cgroup.kill 和可用的 memory 文件。

这种布局解决的是应用/系统进程隔离与生命周期管理。它不决定 top-app 或 background 的 CPU 组;CPU 和 cpuset 仍通过各自的 v1 层级管理。

2.3 cgroup.subtree_control 的作用边界

cgroup v2 controller 默认不会自动在子层级生效。父 cgroup 只能启用 cgroup.controllers 中列出的 controller,并且要满足 top-down(子层不能启用父层未启用的控制器)和 no-internal-process(非根 cgroup 向子层分配域控制器资源时,自身不能直接承载进程)约束。根 cgroup 不受后一条限制。Android 的 NeedsActivation、MaxActivationDepth 和 ActivateControllers() 封装了这部分操作。

freezer 不走上面这条路径。cgroup.freeze 是 cgroup v2 的核心接口,存在于非根 cgroup;它不作为 +freezer 写入 cgroup.subtree_control。AOSP 在 cgroups.json 中把它命名为 freezer,是为了让 task profile 通过统一的 controller/attribute 抽象找到文件。

3. libprocessgroup:把意图翻译成文件操作

3.1 两类配置文件

libprocessgroup 使用两套 JSON:

文件作用
cgroups.json定义 controller 名称、版本、挂载点、权限、可选性和激活要求
task_profiles.json定义 attribute、profile action 和 aggregate profile

Android 17 支持按产品首发 API、vendor 和 system_ext 覆盖或追加配置。源码的加载顺序是:

  1. 系统默认文件;
  2. ro.product.first_api_level 对应的 API-level(产品首发 Android API 版本)文件;
  3. vendor 文件;
  4. task profile 还会读取 system_ext 文件。

同名定义由后加载的文件替换,新增名称则加入集合。分析 OEM 设备时,AOSP 源文件只能作为起点;设备上的实际文件和运行时 cgroupfs 内容才是有效配置。

3.2 配置动作

Android 17 的 action 不限于移动 cgroup:

  • JoinCgroup:把线程或进程加入指定 v1 controller 的子组;TaskProfiles::Load() 对 v2 controller 的这类 action 会记录警告并忽略;
  • SetAttribute:通过 attribute 名查找进程/线程对应文件并写值;
  • WriteFile:写指定路径;
  • SetTimerSlack:写 /proc/<tid>/timerslack_ns,设置定时器允许合并唤醒的时间余量;
  • SetSchedulerPolicy:设置 Linux scheduler policy 和优先级/nice(普通调度优先级调整值);
  • Compact:通过 memory cgroup 的 memory.reclaim 发起回收;
  • aggregate profile:按顺序执行多个 profile。

SetClamps 不是 Android 17 task_profiles.cpp 支持的 action。UClamp(utilization clamping,利用率钳制)通过 UClampMin、UClampMax、UClampLatencySensitive attribute 或 cgroup 初始配置表达。

3.3 AOSP 默认 Profile 的映射

下面只列出 android-17.0.0_r1 中能直接从 JSON 确认的映射:

ProfileAction实际控制器
HighEnergySaving加入 cpu/backgroundv1 /dev/cpuctl/background
HighPerformance加入 cpu/foregroundv1 /dev/cpuctl/foreground
HighPerformanceWI加入 cpu/foreground_windowv1 /dev/cpuctl/foreground_window
MaxPerformance加入 cpu/top-appv1 /dev/cpuctl/top-app
ProcessCapacityLow加入 cpuset/backgroundv1 /dev/cpuset/background
ProcessCapacityHigh加入 cpuset/foregroundv1 /dev/cpuset/foreground
ProcessCapacityMax加入 cpuset/top-appv1 /dev/cpuset/top-app
LowIoPriority加入 blkio/backgroundv1 /dev/blkio/background
Frozen / Unfrozen将 FreezerState 写为 1 / 0v2 per-process cgroup.freeze

MaxPerformance 本身只移动 CPU controller。聚合配置负责把 CPU 与 cpuset 一起切到 top-app:

SCHED_SP_TOP_APP
  = MaxPerformance + MaxIoPriority + TimerSlackNormal

CPUSET_SP_TOP_APP
  = MaxPerformance + ProcessCapacityMax
    + MaxIoPriority + TimerSlackNormal

对于这里的 top-app 配置,SCHED_SP_TOP_APP 不移动 cpuset,CPUSET_SP_TOP_APP 包含 ProcessCapacityMax。其他组要逐个展开 aggregate profile;例如 SCHED_SP_COMPUTE 也包含 ProcessCapacityHigh,会改变 cpuset,不能只凭入口或 SCHED_SP_* 前缀判断。

4. Java/JNI 到 task profile 的调用链

JNI(Java Native Interface)把 framework 的 Java 调用接到 libprocessgroup 的 native 实现;init 侧则在启动 service 时应用 profile,入口是 .rc 里的 task_profiles。

4.1 三个常用 JNI 入口

Android 17 的 android_util_Process.cpp 把这三个入口映射到两组 profile 名:

Java 入口Native 调用Profile 名来源
Process.setThreadGroup(tid, group)SetTaskProfiles(..., use_fd_cache=true)SCHED_SP_*
Process.setThreadGroupAndCpuset(tid, group)SetTaskProfiles(..., use_fd_cache=true)CPUSET_SP_*
Process.setProcessGroup(pid, group)SetProcessProfilesCached(uid, pid, ...)CPUSET_SP_*

SetProcessProfilesCached 函数名中的 Cached 指文件描述符(fd)缓存,可减少反复打开固定 cgroup 文件的成本。这个缓存不会先比较进程的旧状态、再跳过所有 profile action。带 <uid>/<pid> 的动态路径也不会使用同一套全局 fd 缓存。

进程级 setProcessGroup 还保留历史约束:不能直接传 THREAD_GROUP_FOREGROUND;默认组会按 API 语义处理。阅读 framework 调用时,应继续追踪实际配置名,不要只看 THREAD_GROUP_* 的整数。

4.2 init service 的启动配置

Android Init Language 支持:

service zygote /system/bin/app_process64 ...
    task_profiles ProcessCapacityHigh MaxPerformance

Service::Start() 在子进程执行 exec 前应用这些 profile。Android 12 起,task_profiles 替代直接写 writepid /dev/.../tasks 的旧写法;writepid 在 Android 12 已弃用。profile 名稳定后,产品可以改变底层 controller 或路径而不修改 service 定义,但仍要满足对应 action 的版本限制。

5. OomAdjuster 如何决定应用的调度组

5.1 procstate 与 sched group 没有固定一一映射

procstate 是 framework 对进程当前业务重要性的分类,sched group 是随后选择的调度资源组,两者不是同一概念。Android 17 的 OomAdjuster 已位于 services/core/java/com/android/server/am/psc/。它综合 Activity、可见 UI、前台服务、广播、绑定关系、屏幕状态、远程动画和限制策略,为每个进程算出四项结果:

  • oom_score_adj(供低内存杀进程决策使用的优先级);
  • process state(进程状态);
  • scheduling group(调度组);
  • capability(进程在当前状态下保留的特定能力)。

同一个 PROCESS_STATE_SERVICE 可能因正在执行前台服务回调、普通后台服务或绑定传播而得到不同 scheduling group。把所有 PROCESS_STATE_SERVICE 固定映射到 foreground,或把所有 cached/empty 固定映射到 restricted,都会丢失策略条件。

5.2 计算结果转换为调度组

OomAdjuster.applyResultsLSP() 对 scheduling group 的关键映射如下:

Framework scheduling groupProcess.THREAD_GROUP_*
SCHED_GROUP_BACKGROUNDTHREAD_GROUP_BACKGROUND
SCHED_GROUP_TOP_APP / TOP_APP_BOUNDTHREAD_GROUP_TOP_APP
SCHED_GROUP_RESTRICTEDTHREAD_GROUP_RESTRICTED
SCHED_GROUP_FOREGROUND_WINDOWTHREAD_GROUP_FOREGROUND_WINDOW
其他默认情形THREAD_GROUP_DEFAULT

OomAdjuster 通过 mProcessGroupHandler 异步发送组变更,再由 callback 调整应用及相关子进程。profile 文件写入不在 OomAdjuster 的计算循环内执行。状态字段先更新、cgroup 迁移随后执行,因此短时间观察可能同时看到“新 sched group”与“旧 cgroup 路径”;这两步之间的间隔不能预设为固定的 100~200 ms。

从 OomAdjuster 调度组到 profile 文件操作

图:调度组先在框架计算,再由 Handler 异步应用。进程级 JNI 入口使用 CPUSET_SP 聚合配置,libprocessgroup 按实际 JSON 展开动作;状态变化与底层写入的时间需要分别观测。

进入或离开 top-app 时,framework 还可能单独调整主线程/RenderThread nice,或在配置允许时切换 FIFO(先进先出实时调度)UI scheduling。这些动作和 cgroup 迁移有关联,但属于不同内核接口,排查时要分别取证。

6. CPU、cpuset 与 UClamp

6.1 Android 17 AOSP 的 CPU 路径仍是 v1

rootdir/init.rc 创建:

/dev/cpuctl/{background,foreground,foreground_window,top-app,rt,system,...}
/dev/cpuset/{background,foreground,foreground_window,top-app,restricted,...}

cpuset 挂载使用 noprefix,实际文件名是 /dev/cpuset/<group>/cpus 和 mems,不是 v2 风格的 cpuset.cpus。

init 只保证组和权限存在,并把根 CPU mask(允许使用的 CPU 编号集合)复制为初始值。具体 SoC(System on Chip,片上系统)的 top-app、foreground、background CPU mask 由设备配置写入。AOSP 不能证明“background 固定为 0-3”或“top-app 固定为 0-7”。

6.2 UClamp 的位置与含义

Android common kernel 把 cpu.uclamp.min、cpu.uclamp.max 和 Android 扩展的 latency-sensitive(延迟敏感)属性暴露在 /dev/cpuctl/<group>/。task_profiles.json 把它们声明为 cpu controller 的 attribute;进程加入某个 CPU group 后,调度会受该组当前参数约束。

UClamp 限定调度器采用的利用率上下界,会影响选核与调频决策输入。它不等于锁定频率,也不保证某个线程立即迁移到大核。热限制、CPU affinity(线程允许运行在哪些 CPU 上)、cpuset、调度类、负载和 governor(CPU 调频策略)仍共同决定执行位置与频率。

6.3 affinity 与 cpuset 取交集

线程允许使用的 CPU 是 thread affinity 与有效 cpuset 的交集。出现“设置 affinity 后仍不上大核”时,应同时读取:

  • /proc/<tid>/status 的 Cpus_allowed_list;
  • /proc/<tid>/cgroup 的 cpuset hierarchy;
  • /dev/cpuset/<group>/cpus;
  • CPU online 状态、thermal(温控限制)与调度 trace。

只检查 sched_setaffinity() 的返回值不能证明线程可在目标 CPU 上运行。

6.4 cpu.weight / cpu.max 何时成立

cpu.weight 和 cpu.max 是 cgroup v2 CPU controller 接口。内核 6.18 中:

  • cpu.weight 在非 idle 组中以 1~10000 的权重,在活跃兄弟 cgroup 间分配 fair-class(普通公平调度类)CPU 时间;cpu.idle=1 时读到的权重为 0;
  • cpu.max 用 $MAX $PERIOD 限制公平调度类的 CPU 带宽,max 表示不设上限;该接口不限制实时调度类。

Android 17 AOSP 默认把 CPU controller 挂在 v1 /dev/cpuctl,不能用这两个 v2 文件解释平台基线。厂商若把 CPU 改为 v2,需要同时核对控制器配置、cpu.weight、cpu.max、subtree activation 和任务归属的实现。只修改 cgroups.json 不会让原有 JoinCgroup cpu/top-app 继续生效:Android 17 的 TaskProfiles::Load() 会忽略指向 v2 controller 的 JoinCgroup。应结合加载警告、设备文件、mountinfo 和 /proc/<tid>/cgroup 确认实际行为。

7. v2 memory controller

7.1 AOSP 提供的 attribute

Android 17 的 task_profiles.json 为 memory controller 定义了:

  • MemEvents → memory.events;
  • MemStats → memory.stat;
  • MemHigh → memory.high;
  • MemLimit → v1 的 memory.limit_in_bytes 或 v2 的 memory.max;
  • MemSoftLimit → v1 的 memory.soft_limit_in_bytes 或 v2 的 memory.low;
  • SwapMax → memory.swap.max;
  • MemUsage 等兼容 attribute。

MemLimit、MemSoftLimit 在 JSON 中同时声明 File 与 FileV2。ProfileAttribute::file_name() 只在 controller 为 v2 且 FileV2 非空时选用后者,其余情况都使用 File;字段名 File 本身不表示 v1 专用。MemStats 的 memory.stat 在 v1 和 v2 中都存在,但统计字段与含义需要分别解释。MemHigh、MemEvents、SwapMax 指向 v2 接口,不能直接沿用到 v1 memory controller。

固定内核版本的 mm/memcontrol-v1.c 仍在 mem_cgroup_legacy_files 中注册 stat。配套的 v1 memory 文档开头提醒内容陈旧、细节应查当前源码,并将 memory.soft_limit_in_bytes 标为弃用,且说明该接口不适用于 CONFIG_PREEMPT_RT。这里的 v1 路径用于识别兼容配置,不能据此把 soft limit 与 v2 的 memory.low 视为相同机制。

memory controller 在默认 cgroups.json 中标为 optional。设备不支持、没有启用或被 vendor 改为其他布局时,不能假定相应配置动作会沿 AOSP 路径成功执行。

7.2 四个接口的语义边界

文件内核语义性能风险
memory.low尽力提供回收保护;保护量受祖先和过量承诺影响保护过多会把回收压力转移给其他 cgroup
memory.high超限后让该 cgroup 进入强回收/节流;极端情况下可暂时超过设置过低会增加 direct reclaim(由内存申请线程直接执行回收)与分配延迟
memory.max硬上限;回收无法满足时可能触发 cgroup OOM(Out of Memory,内存不足处理)错误上限会导致进程被杀或分配失败
memory.oom.group设为 1 时把 cgroup 作为不可分割 workload(工作负载整体)处理可能扩大一次 cgroup OOM 的终止范围

memory.low 不能概括成“低于该值绝不回收”;它提供尽力而为的保护。memory.high 也不是普通告警阈值,超限会让该 cgroup 中的进程进入强回收和节流。

AOSP 默认 profile 暴露这些能力,但没有给所有应用统一写入 memory.max/memory.high。是否设置、设置多少属于产品策略。不要用假设的 system_server 上限、工作资料(Work Profile)上限或固定阈值解释未知设备。

7.3 与 LMKD 的边界

LMKD(Low Memory Killer Daemon,低内存终止守护进程)主要根据 PSI(Pressure Stall Information,资源压力停顿指标)、内存状态、oom_score_adj 和配置选择需要终止的进程;memory cgroup 的 reclaim(回收)、memory.max 和 cgroup OOM 由内核执行。两条路径可以同时存在,但触发条件和受影响进程的选择不同。

排查“进程因内存消失”时至少区分:

  • LMKD 日志与杀进程原因;
  • kernel OOM/cgroup OOM 日志;
  • memory.events 中的 high、max、oom、oom_kill 计数;
  • tombstone(native 崩溃转储)、ApplicationExitInfo 和进程自身崩溃证据。

只看到 RSS(驻留物理内存)接近某个数值,不能判断是哪条机制触发。

8. freezer 与 Binder Freezer

8.1 Frozen profile 写入位置

Process.setProcessFrozen(pid, uid, true) 在 JNI 中调用:

SetProcessProfiles(uid, pid, {"Frozen"})
  -> SetAttribute FreezerState = 1
  -> /sys/fs/cgroup/{apps|system}/uid_<uid>/pid_<pid>/cgroup.freeze

解冻使用 Unfrozen 并写 0。这里不使用 cached fd 路径,因为文件名包含 UID/PID,且进程 cgroup 会创建和删除。

向 cgroup.freeze 写 1 会发起该 cgroup 及其后代的冻结。冻结完成可能滞后于写入;内核通过 cgroup.events 的 frozen 字段报告完成状态。“一次写入就原子完成冻结”的说法因此不准确。

8.2 Android 为什么还需要 Binder Freezer

暂停调度不能解决 Binder 中已有事务、同步调用和异步队列的语义。CachedAppOptimizer.freezeProcess() 分两次检查 Binder 状态:

  1. 调用 freezeBinder(pid, true, timeout)。返回值非零表示仍有 outstanding transaction(尚未完成的 Binder 事务),此时不写 cgroup.freeze,交给失败处理逻辑;
  2. Binder 侧满足冻结条件后,调用 setProcessFrozen(..., true) 写入 cgroup freezer,并记录 framework 冻结状态;
  3. 写入后再调用 getBinderFreezeInfo(pid)。若两步之间又出现 TXNS_PENDING_WHILE_FROZEN,同样进入失败处理逻辑。

第二次检查用于覆盖 Binder 冻结与 cgroup 冻结之间的竞态窗口,即两步操作之间状态可能再次变化的时间段。解冻时,源码读取 Binder freeze info;若没有需要终止进程的同步事务,就解冻 Binder,随后写 cgroup freezer。cgroup freezer 负责停止线程运行,Binder 驱动负责 IPC 边界;两者不会被一条 cgroup.freeze 写操作自动合并。

完整的 pending transaction(待处理事务)、oneway(异步单向调用)和同步调用规则见 1.10 Binder 线程池、异步事务与 Freezer。

8.3 对进程内监控的影响

进程冻结后,其普通线程不再获得 CPU,进程内 Watchdog、采样线程和上传线程也会暂停。解冻后的时间间隔突增不能直接算作 ANR、CPU starvation(长时间得不到 CPU)或网络超时。

普通应用没有可依赖的公开“即将被冻结”回调。监控系统若需要区分冻结,应结合系统侧/外部观测、生命周期上下文与采样连续性判断,不能靠在已冻结进程里轮询 cgroup.freeze。

后台 profiler(性能采样器)的 10 ms 周期同样没有证据支持统一换算成 50~100 ms。

9. 设备核对方法

以下只读命令用于确认拓扑与进程归属:

adb shell cat /proc/mounts
adb shell cat /proc/<pid>/cgroup
adb shell cat /system/etc/cgroups.json
adb shell cat /system/etc/task_profiles.json
adb shell cat /vendor/etc/cgroups.json
adb shell cat /vendor/etc/task_profiles.json

文件可能通过根目录映射显示为 /etc/...,vendor/system_ext 也可能追加定义。命令失败还可能来自 SELinux 访问控制或 shell 权限,不能把“读不到”解释为 controller 不存在。

再按资源检查实际值:

# v1 CPU / UClamp
adb shell cat /dev/cpuctl/top-app/cpu.uclamp.min
adb shell cat /dev/cpuctl/background/cpu.uclamp.max

# v1 cpuset
adb shell cat /dev/cpuset/top-app/cpus
adb shell cat /dev/cpuset/background/cpus

# v2 per-process
adb shell cat /sys/fs/cgroup/apps/uid_<uid>/pid_<pid>/cgroup.freeze
adb shell cat /sys/fs/cgroup/apps/uid_<uid>/pid_<pid>/memory.events

这些值应和 /proc/<pid>/cgroup 指向的实际组对应。top-app 组存在不代表目标线程已经加入,profile 名存在也不代表所有动作都已成功执行。

Perfetto 适合验证后果:

  • sched_switch / thread state:线程何时可运行、在哪个 CPU 执行;
  • CPU frequency/idle:UClamp 和负载变化后的频率与空闲状态;
  • Binder/freezer 事件:冻结、解冻和事务失败;
  • memory counter/PSI:回收、压力与 cgroup memory 事件的时间关系。

Perfetto 中看到大核迁移或频率上升,只能作为结果证据。要证明 cgroup 切换,还需配合 profile 调用、cgroupfs 或 /proc 快照。

10. OEM 覆盖的审计方法

厂商常见改动包括:

  • 修改 cpuset 的 CPU mask;
  • 修改 /dev/cpuctl/<group>/cpu.uclamp.*;
  • 增加游戏、相机、NNAPI(Android 神经网络 API)或系统服务 profile;
  • 把更多 controller 迁入 cgroup v2;
  • 调整 per-app(每应用)memory controller 的启用与阈值;
  • 由 power/thermal(电源/温控)服务在运行时重写组参数。

审计时按以下顺序收集证据:

  1. 查看 mountinfo,确认 controller 版本和挂载点;
  2. 按加载顺序读取 system、产品首发 API-level、vendor 的实际 JSON;task profile 还要读取最后加载的 system_ext 文件;
  3. 展开 aggregate profile,列出每个 action;
  4. 对照目标 PID/TID 的 /proc/.../cgroup;
  5. 读取目标组的当前文件值;
  6. 用 Perfetto 观察调度、频率、freezer、PSI 和用户可见延迟。

不要从 AOSP profile 名推断厂商参数。MaxPerformance 只声明策略意图(把线程加入 cpu/top-app),具体 CPU mask、UClamp 与 thermal 上限由产品配置决定。

11. Android 17 的资源分组层次

Android 17 的资源分组可以分成三层:

  • framework 计算进程重要性和 scheduling group;
  • libprocessgroup 将稳定的 profile 名转换为 cgroup、scheduler、timer slack(定时器余量)或 memory action;
  • 内核按 v1/v2 controller 的实际配置执行调度、限制、回收和冻结。

平台基线中,CPU、cpuset、blkio 仍在 v1,freezer 与可选 memory 位于 v2。v2 的 per-UID/per-process 树提供进程隔离、冻结、kill 和 memory 文件;top-app/background 的 CPU 差异仍来自 /dev/cpuctl、/dev/cpuset 及设备参数。

定位问题时,应从设备当前拓扑出发:确认 controller 版本与路径,再确认 profile action 和 PID/TID 归属,然后用 trace 验证性能后果。把“Android 17 支持 cgroup v2”理解为“所有 controller 已迁入统一树”,会让后续 CPU、memory、freezer 结论全部偏离源码。

相关章节:CPU affinity、cpuset、task profile、EAS(Energy Aware Scheduling,能耗感知调度)与 UClamp 见 5.1;Binder Freezer 见 1.10;OomAdjuster 见 1.12;LMKD/PSI 见内存管理章节。