本文出现的文件路径,均以Android U为准,不同版本文件路径可能略有不同,只讲解Cgroup V1版本,Cgroup V2版本概念类似,只是挂载路径略有不同。Cgroup V1版本,Cgroup V2可能会混用,分别管理不同的资源控制器。
一、出现背景
Android 引入 Cgroup,根本原因是:手机上长期同时存在很多进程,而系统必须根据应用的重要程度,动态分配有限的 CPU、内存、I/O 和功耗预算。
Linux 原有机制为什么不够?
早期 Linux 主要提供进程级控制:
- nice:调整单个进程的 CPU 调度优先级。
- sched_setscheduler():设置线程调度策略。
- setrlimit():限制单进程资源。
- taskset/CPU affinity:指定线程在哪些 CPU 上运行。
- OOM killer:内存耗尽后选择进程杀死。
这些机制很难解决 Android 的实际问题。一个应用通常包含多个进程、几十个线程,还可能不断创建子进程。Android需要表达的是:
“这个应用整体现在处于后台,所以它的所有进程和线程都应该少用 CPU、限制大核、降低 I/O 优先级,并且在内存紧张时优先回收。”
这属于一组进程的资源管理,正是 Linux Cgroup 要解决的问题。
二、Cgroup概念
2.1 Cgroup 的核心模型
Cgroup 全称 Control Group。它只做两件事:
- 把进程或线程组织成组。
- 给每个组配置资源规则。
可以把它想成:
任务 → 放入某个组 → 内核按照该组规则调度
例如:
所有任务(进程/线程)
├── top-app
├── foreground
├── background
└── system-background
Android 来判断应用当前是前台还是后台,再把它放入对应组。
这里有一个非常重要的边界:
Linux 内核并不知道什么是 Activity、Launcher、前台应用或缓存应用。这些概念由 Android Framework 判断,cgroup 只负责执行最后的资源策略。
2.2 Cgroup 通过虚拟文件接口来管理
Cgroup 使用一种虚拟文件系统作为内核接口。目录表示组(上面提到的组概念),文件表示该组的参数或操作。
例如:
/dev/cpuctl/
├── cpu.shares
├── cgroup.procs
├── background/
│ ├── cpu.shares
│ └── cgroup.procs
└── top-app/
├── cpu.shares
└── cgroup.procs
如果执行:
echo 1234 > /dev/cpuctl/background/cgroup.procs
含义是:
把 PID 1234 对应的进程迁移到 CPU background 组
如果执行:
echo 1250 > /dev/cpuctl/background/tasks
含义是:
把 TID 1250 对应的线程迁移到 CPU background 组
这些节点虽然看起来是文件,但本质上是内核提供给外部调用的接口,一般是给Android Framework调用的:
- 写 PID/TID:迁移任务。
- 写数字:改变资源规则。
- 读文件:查询配置、状态或统计结果。
2.3 Cgroup有不同的Controller
Cgroup 本身只负责分组,真正管理资源的是 Controller。
Android 常用的几个 Controller 是:
| Controller | 解决的问题 | 当前仓库默认路径 |
|---|---|---|
cpu | 各组如何竞争 CPU 时间 | /dev/cpuctl |
cpuset | 任务允许在哪些 CPU 上运行 | /dev/cpuset |
blkio | 块设备 I/O 权重 | /dev/blkio |
memory | 内存记账、保护和限制 | /dev/memcg |
freezer | 暂停和恢复进程 | /sys/fs/cgroup |
仓库默认定义在system/core/libprocessgroup/profiles/cgroups.json。
例如:
{
"Controller": "cpu",
"Path": "/dev/cpuctl"
}
它告诉 Android:
名字为 cpu 的 Controller,可以在 /dev/cpuctl 找到
这里给出一份完整的cgroups.json作为参考,这里同时用到了Cgroups和Cgroups2两个版本,Cgroups2只管理freezer Controller,Cgroups管理其他Controller:
{
"Cgroups": [
{
"Controller": "blkio",
"Path": "/dev/blkio",
"Mode": "0775",
"UID": "system",
"GID": "system"
},
{
"Controller": "cpu",
"Path": "/dev/cpuctl",
"Mode": "0755",
"UID": "system",
"GID": "system"
},
{
"Controller": "cpuset",
"Path": "/dev/cpuset",
"Mode": "0755",
"UID": "system",
"GID": "system"
},
{
"Controller": "memory",
"Path": "/dev/memcg",
"Mode": "0700",
"UID": "root",
"GID": "system",
"Optional": true
}
],
"Cgroups2": {
"Path": "/sys/fs/cgroup",
"Mode": "0775",
"UID": "system",
"GID": "system",
"Controllers": [
{
"Controller": "freezer",
"Path": "."
}
]
}
}
以上就是Cgroup的基本概念,重要的两个概念是分组和Controller, 理解了上述概念之后就可以看看实际工作中是如何配置的了
三、Cgroup系统在Android中的构建过程
3.1 init进程如何创建这些 Controller
启动早期,PID 1 的 init 调用 CgroupSetup()。
入口在 /system/core/init/init.cpp,具体实现在 system/core/libprocessgroup/setup/cgroup_map_write.cpp。
核心过程是:
init 启动
↓
读取 /etc/cgroups.json
↓
读取 API 级别配置
↓
读取 /vendor/etc/cgroups.json
↓
挂载各个 Controller
↓
生成运行时 cgroup 映射文件
源码中的顺序在 /system/core/libprocessgroup/setup/cgroup_map_write.cpp:227:
ReadDescriptorsFromFile(CGROUPS_DESC_FILE, descriptors);
ReadDescriptorsFromFile(api_cgroups_path, descriptors);
ReadDescriptorsFromFile(CGROUPS_DESC_VENDOR_FILE, descriptors);
因此优先级是:
system 默认配置
↓ 可以被覆盖
API level 配置
↓ 可以继续被覆盖
vendor 配置
同名 Controller 会被后加载的配置覆盖,新名字则会被添加。挂载之后cgroups.json中定义的Controller路径就可以使用了
3.2 理解 Android 的 Task Profile
Android 不希望 Framework 到处写这种代码:
write(pid, "/dev/cpuctl/background/cgroup.procs");
write(pid, "/dev/cpuset/background/cgroup.procs");
因为不同内核、Android 版本和设备的路径可能不同。
所以 Android 增加了 Task Profile:
上层只说:我要应用 CPUSET_SP_BACKGROUND配置
配置文件决定:
加入哪个 CPU 组
加入哪个 Cpuset 组
加入哪个 I/O 组
设置多少 Timer Slack
配置文件是:
/system/core/libprocessgroup/profiles/task_profiles.json,这里只截取了部分,方便后续讲解:
{
"Attributes": [
{
"Name": "LowCapacityCPUs",
"Controller": "cpuset",
"File": "background/cpus"
},
{
"Name": "HighCapacityCPUs",
"Controller": "cpuset",
"File": "foreground/cpus"
},
{
"Name": "MaxCapacityCPUs",
"Controller": "cpuset",
"File": "top-app/cpus"
},
{
"Name": "MemStats",
"Controller": "memory",
"File": "memory.stat"
}
],
"Profiles": [
{
"Name": "HighEnergySaving",
"Actions": [
{
"Name": "JoinCgroup",
"Params":
{
"Controller": "cpu",
"Path": "background"
}
}
]
},
{
"Name": "Frozen",
"Actions": [
{
"Name": "SetAttribute",
"Params":
{
"Name": "FreezerState",
"Value": "1"
}
}
]
}
],
"AggregateProfiles": [
{
"Name": "SCHED_SP_DEFAULT",
"Profiles": [ "TimerSlackNormal" ]
},
{
"Name": "SCHED_SP_BACKGROUND",
"Profiles": [ "HighEnergySaving", "LowIoPriority", "TimerSlackHigh" ]
},
{
"Name": "SCHED_SP_FOREGROUND",
"Profiles": [ "HighPerformance", "HighIoPriority", "TimerSlackNormal" ]
}
}
里面有三层概念。
第一层:Attribute
Attribute 给内核节点取一个抽象名字:
{
"Name": "FreezerState",
"Controller": "freezer",
"File": "cgroup.freeze"
}
含义是:
FreezerState对应:freezer Controller (freezer Controller 路径在哪里需要看2.3节说的cgroups.json)下的 cgroup.freeze 文件
这里定义好别名之后,方便后面调用这个节点,只用写别名就行,不用写完整路径。
第二层:Profile
Profile 是一组具体动作,具体动作是什么要看,Actions的Name, 例如下面例子中的JoinCgroup和SetAttribute,就对应着把任务加入某个分组和写某个属性。
例如:
{
"Name": "ProcessCapacityLow",
"Actions": [
{
"Name": "JoinCgroup",
"Params": {
"Controller": "cpuset",
"Path": "background"
}
}
]
}
它的含义是:
如果应用 ProcessCapacityLow 的话
↓
把任务加入 cpuset/background分组
另一个例子:
{
"Name": "Frozen",
"Actions": [
{
"Name": "SetAttribute",
"Params": {
"Name": "FreezerState",
"Value": "1"
}
}
]
}
含义是:
如果应用 Frozen 的话
↓
找到 FreezerState(cgroup.freeze的别名)这个属性
↓
写 cgroup.freeze = 1
第三层:AggregateProfile
AggregateProfile 把多个 Profile (多个动作)组合起来。
例如 CPUSET_SP_BACKGROUND
{
"Name": "CPUSET_SP_BACKGROUND",
"Profiles": [
"HighEnergySaving",
"ProcessCapacityLow",
"LowIoPriority",
"TimerSlackHigh"
]
}
具体这几个Profiles干了什么事情,就要到定义他们的地方去找了。
所以 CPUSET_SP_BACKGROUND 实际不是只设置 Cpuset,而是一套完整的后台资源策略。
这也是为什么不要只根据 API 名字猜行为,应该展开 AggregateProfiles 看实际动作。
3.3 跟踪应用进入前台后Cgroup变化的完整调用链
现在跟一条真正的源码链路,帮助读者理解Android Framework如何利用Cgroup机制调整应用资源。
假设 Launcher 启动某个应用,这个应用成为当前 top app。
-
Framework 计算进程状态
OomAdjuster 根据 Activity、Service、Provider、进程依赖等信息计算进程的重要程度。
当调度组发生变化时,代码进入: frameworks/base/services/core/java/com/android/server/am/OomAdjuster.java
关键映射是:
case SCHED_GROUP_BACKGROUND:
processGroup = THREAD_GROUP_BACKGROUND;
break;
case SCHED_GROUP_TOP_APP:
case SCHED_GROUP_TOP_APP_BOUND:
processGroup = THREAD_GROUP_TOP_APP;
break;
然后通过 Handler 异步执行:
mProcessGroupHandler.sendMessage(...)
使用 Handler 是因为迁移整个进程可能要处理许多线程,不适合一直占用 AMS 的关键锁。
-
Java 调用 JNI
Handler 最终调用:
Process.setProcessGroup(pid, group);
JNI 实现在:
/frameworks/base/core/jni/android_util_Process.cpp
核心代码:
SetProcessProfilesCached(
0,
pid,
{get_cpuset_policy_profile_name((SchedPolicy)grp)});
如果 grp 是 SP_TOP_APP,profile 名字就是:
CPUSET_SP_TOP_APP
所以调用链现在变成:
OomAdjuster
↓
Process.setProcessGroup(pid, THREAD_GROUP_TOP_APP)
↓
JNI
↓
SetProcessProfilesCached(pid, "CPUSET_SP_TOP_APP")
-
libprocessgroup 找到 Profile
如果是线程级 API,则 /system/core/libprocessgroup/sched_policy.cpp 直接映射:
case SP_TOP_APP:
return SetTaskProfiles(
tid,
{"CPUSET_SP_TOP_APP"},
true) ? 0 : -1;
SetTaskProfiles() 在 /system/core/libprocessgroup/task_profiles.cpp中:
TaskProfile* profile = GetProfile(name);
profile->ExecuteForTask(tid);
逻辑是:
用名字查找 Profile
↓
遍历 Profile 中的所有 Action
↓
逐个执行
-
JoinCgroup 变成内核文件写入
加载 JSON 时,JoinCgroup 会转换成 SetCgroupAction:
profile->Add(
std::make_unique<SetCgroupAction>(controller, path));
位置在 /system/core/libprocessgroup/task_profiles.cpp。
执行进程级迁移时:
std::string procs_path =
controller()->GetProcsFilePath(path_, uid, pid);
open(procs_path, O_WRONLY);
AddTidToCgroup(pid, fd, controller()->name());
见 /system/core/libprocessgroup/task_profiles.cpp。
最终效果相当于:
echo PID > 对应控制组的 cgroup.procs
完整链路因此是:
应用变成 top app
↓
OomAdjuster 得到 SCHED_GROUP_TOP_APP
↓
Process.setProcessGroup()
↓
SetProcessProfilesCached()
↓
CPUSET_SP_TOP_APP
↓
展开 AggregateProfile
↓
执行多个 JoinCgroup / SetTimerSlack
↓
写入 cgroup.procs 等内核节点
↓
内核按 top-app 策略调度
这条链路就是 Android Framework 策略落到 Linux 内核机制的过程
3.4 理解进程级和线程级 Profile
Android代码有两个重要接口:
SetProcessProfiles(uid, pid, profiles)
SetTaskProfiles(tid, profiles)
区别是:
| 接口 | 操作对象 | 常见写入节点 |
|---|---|---|
SetProcessProfiles | 整个进程 | cgroup.procs |
SetTaskProfiles | 单个线程 | tasks |
对应实现在:
ExecuteForProcess()
→ GetProcsFilePath()
→ 写 PID
ExecuteForTask()
→ GetTasksFilePath()
→ 写 TID
代码见 /system/core/libprocessgroup/task_profiles.cpp。
为什么要支持线程级操作?
因为一个 Android 应用进程内部有很多性质不同的线程:
应用进程
├── Main Thread
├── RenderThread
├── Binder Thread
├── Audio Thread
└── 后台工作线程
某些线程需要单独调整。例如:
- RenderThread 需要低延迟。
- Audio Thread 可能使用实时调度。
- 后台计算线程不应该抢占 UI 线程。
因此排查问题时,不要只看:
cat /proc/PID/cgroup
还要看关键线程:
cat /proc/PID/task/TID/cgroup
特别是 UI 卡顿场景,应分别检查:
主线程
RenderThread
Binder 线程
3.4 组资源参数如何设置,并且怎样产生实际效果
组参数可以通过rc脚本中的write在启动时写入
1. Cpuset
例如 ohm 设备配置:/device/amlogic/ohm/init.amlogic.board.rc
write /dev/cpuset/top-app/cpus 0-3
write /dev/cpuset/foreground/cpus 0-3
write /dev/cpuset/background/cpus 0-1
write /dev/cpuset/system-background/cpus 0-1
write /dev/cpuset/restricted/cpus 0-1
它表示:
top-app 可运行在 CPU 0-3
foreground 可运行在 CPU 0-3
background 只能运行在 CPU 0-1
system-background 只能运行在 CPU 0-1
restricted 只能运行在 CPU 0-1
如果一个后台线程持续做大量计算,它最多只能在 CPU 0、1 范围内被调度。
注意:
cpus = 0-3
表示任务可以在这些 CPU 中选择,并不表示一个线程能同时占用四个 CPU。
2. CPU shares
Amlogic mbox 的公共配置在:/device/amlogic/common/products/mbox/init.amlogic.system.rc
其中:
write /dev/cpuctl/background/cpu.shares 1024
write /dev/cpuctl/system/cpu.shares 20480
write /dev/cpuctl/foreground/cpu.shares 20480
write /dev/cpuctl/top-app/cpu.shares 20480
在相同父组下、CPU 出现竞争时,可以粗略理解为:
background : top-app
1024 : 20480
1 : 20
即 top-app 的相对 CPU 权重明显更高。
但 cpu.shares 不是硬限制。
如果系统里只有 background 任务在运行,它仍然可以使用空闲 CPU。权重主要在多个组同时竞争时体现。
3. Uclamp
同一文件还设置:
write /dev/cpuctl/background/cpu.uclamp.max 50
write /dev/cpuctl/system-background/cpu.uclamp.max 50
write /dev/cpuctl/dex2oat/cpu.uclamp.max 60
Uclamp 控制的是调度器看到的性能需求范围。
可以这样理解:
uclamp.min:任务至少希望获得怎样的性能水平
uclamp.max:不让任务表现出高于某个上限的性能需求
它可能影响:
- 调度器选择高性能核还是低性能核。
schedutil调频器选择的 CPU 频率。- 任务是否被当作延迟敏感任务。
它不等于 CPU 使用率上限,也不等于 CFS quota。
以上几个参数只是抛砖引玉,Cgroup还有很多其他参数可以设置,当需要修改的时候可以修改rc脚本
四、在设备上完成一次观察实验
找一个应用包名:
adb shell pidof com.example.app
假设得到 PID 4321。
观察进程归属
adb shell cat /proc/4321/cgroup
然后找线程:
adb shell ps -T -p 4321
假设 RenderThread 的 TID 是 4380:
adb shell cat /proc/4321/task/4380/cgroup
观察进程和线程的Cpuset
adb shell cat /proc/4321/status | grep Cpus_allowed_list
adb shell cat /proc/4321/task/4380/status | grep Cpus_allowed_list
再读取系统控制器Controller组是如何配置的:
adb shell cat /dev/cpuset/top-app/cpus
adb shell cat /dev/cpuset/background/cpus
adb shell cat /dev/cpuset/foreground/cpus
观察 CPU 权重
adb shell cat /dev/cpuctl/top-app/cpu.shares
adb shell cat /dev/cpuctl/background/cpu.shares
adb shell cat /dev/cpuctl/foreground/cpu.shares
做一次状态切换,看看Cgroup是如何变化的
- 让应用显示在最前面。
- 读取
/proc/PID/cgroup。 - 按 Home 返回 Launcher。
- 再读取一次。
- 等进程进入 cached 状态,再次读取。
可能会有类似变化:
在前台交互:
cpuset/top-app 或 foreground
schedtune/top-app 或 foreground
退到后台:
cpuset/background
schedtune/background
blkio/background
实际变化取决于该应用是否还有前台服务、绑定关系、音频播放等活动。
读取上述路径有助于我们分析系统给对应的APP分配的资源情况。