JasonWang's Blog

如何利用cgroups优化Android系统性能

字数统计: 3.2k阅读时长: 14 min
2024/04/15

cgroups(Control Groups)即控制分组,是Linux中的一种进程资源分组访问控制机制,用于将系统中的进程划分为不同的分组(形成一种树状层级的结构),利用不同的分组可以实现对各个进程的资源使用,如CPU、IO、内存、网络等系统资源进行优先级管理,可以确保在系统资源紧张的情况下,高优先级的进程可以获得更多的系统资源。简单来说,通过cgroups,我们可以实现对系统资源的分配、访问优先级、访问限制以及管理、监控等更精细的控制,从而提升系统的性能。本文主要介绍Android系统如何利用cgroups来改善系统性能,主要分为以下几个部分:

  • 简单介绍cgroup的实现原理
  • Android中的cgroup分组管理策略
  • 如何利用cgroup优化Android系统性能

cgroup的实现原理

Linux内核在初始化时,会初始化cgroup相关的配置,创建一个根cgroup分组,并注册一个虚拟的文件系统挂载到/sys/fs/cgroup目录下,这样用户空间执行mount之后就可以通过这些接口进行相关的操作。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39

/**
* cgroup_init_early - cgroup initialization at system boot
*
* Initialize cgroups at system boot, and initialize any
* subsystems that request early init.
*/
int __init cgroup_init_early(void)
{
static struct cgroup_fs_context __initdata ctx;
struct cgroup_subsys *ss;
int i;

ctx.root = &cgrp_dfl_root;
init_cgroup_root(&ctx);
cgrp_dfl_root.cgrp.self.flags |= CSS_NO_REF;

RCU_INIT_POINTER(init_task.cgroups, &init_css_set);

for_each_subsys(ss, i) {
WARN(!ss->css_alloc || !ss->css_free || ss->name || ss->id,
"invalid cgroup_subsys %d:%s css_alloc=%p css_free=%p id:name=%d:%s\n",
i, cgroup_subsys_name[i], ss->css_alloc, ss->css_free,
ss->id, ss->name);
WARN(strlen(cgroup_subsys_name[i]) > MAX_CGROUP_TYPE_NAMELEN,
"cgroup_subsys_name %s too long\n", cgroup_subsys_name[i]);

ss->id = i;
ss->name = cgroup_subsys_name[i];
if (!ss->legacy_name)
ss->legacy_name = cgroup_subsys_name[i];

if (ss->early_init)
cgroup_init_subsys(ss, true);
}
return 0;
}


目前Linux内核中常用的cgroup有如下几种:

  • cpuset: 控制CPU核的分组,可以将指定的CPU核心分配到某个cgroup中,从而控制系统中的CPU资源的使用;要使用cpuset需要开启内核配置CONFIG_CPUSETS
  • cpu: 控制CPU分组调度,用于控制不同分组的调度时间片分配,确保高优先级的任务可以得到更多的时间片,对应的内核配置为CONFIG_CGROUP_SCHED
  • cpuacct: 用于控制不同分组的CPU使用状态统计,可以看到各个分组的CPU调度与使用状态数据,要使用cpuacct需要开启内核配置CONFIG_CGROUP_CPUACCT
  • blkio: 用于控制不同分组的磁盘IO资源的使用,比如保证前台的应用IO优先级与带宽,减少后台应用对系统IO的抢占,要使用blkio需要开启内核配置CONFIG_BLK_CGROUP
  • memcg: 控制不同分组的内存分配与使用,比如限制某些进程的内存使用量;比如在虚拟机的场景,限制虚拟机总的内存使用量;memcg对应的内存配置CONFIG_MEMCG
  • freezer: 冻结分组子系统,通常用于进程的冻结控制,比如系统资源紧张时,主动冻结后台的某些任务,减少系统资源压力,要使用freezer需要开启内核配置CONFIG_FREEZER

Androidcgroup配置都放在描述文件cgroups.json/system/core/libprocessgroup/profiles/)中进行配置,init进程启动的时候会主动读取该配置文件,然后将各个分组控制器挂载到/dev/xxx对应的节点下,比如cpu分组控制器对应的目录为/dev/cpuctl; cpuset对应的目录为/dev/cpuset; memory对应的目录为/dev/memcg.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78

static bool SetupCgroup(const CgroupDescriptor& descriptor) {
const format::CgroupController* controller = descriptor.controller();

int result;
if (controller->version() == 2) {
result = 0;
if (!strcmp(controller->name(), CGROUPV2_CONTROLLER_NAME)) {
// /sys/fs/cgroup is created by cgroup2 with specific selinux permissions,
// try to create again in case the mount point is changed
if (!Mkdir(controller->path(), 0, "", "")) {
LOG(ERROR) << "Failed to create directory for " << controller->name() << " cgroup";
return false;
}

result = mount("none", controller->path(), "cgroup2", MS_NODEV | MS_NOEXEC | MS_NOSUID,
nullptr);

// selinux permissions change after mounting, so it's ok to change mode and owner now
if (!ChangeDirModeAndOwner(controller->path(), descriptor.mode(), descriptor.uid(),
descriptor.gid())) {
LOG(ERROR) << "Failed to create directory for " << controller->name() << " cgroup";
result = -1;
}
} else {
if (!Mkdir(controller->path(), descriptor.mode(), descriptor.uid(), descriptor.gid())) {
LOG(ERROR) << "Failed to create directory for " << controller->name() << " cgroup";
return false;
}

if (controller->flags() & CGROUPRC_CONTROLLER_FLAG_NEEDS_ACTIVATION) {
std::string str = std::string("+") + controller->name();
std::string path = std::string(controller->path()) + "/cgroup.subtree_control";

if (!base::WriteStringToFile(str, path)) {
LOG(ERROR) << "Failed to activate controller " << controller->name();
return false;
}
}
}
} else {
// mkdir <path> [mode] [owner] [group]
if (!Mkdir(controller->path(), descriptor.mode(), descriptor.uid(), descriptor.gid())) {
LOG(ERROR) << "Failed to create directory for " << controller->name() << " cgroup";
return false;
}

// Unfortunately historically cpuset controller was mounted using a mount command
// different from all other controllers. This results in controller attributes not
// to be prepended with controller name. For example this way instead of
// /dev/cpuset/cpuset.cpus the attribute becomes /dev/cpuset/cpus which is what
// the system currently expects.
if (!strcmp(controller->name(), "cpuset")) {
// mount cpuset none /dev/cpuset nodev noexec nosuid
result = mount("none", controller->path(), controller->name(),
MS_NODEV | MS_NOEXEC | MS_NOSUID, nullptr);
} else {
// mount cgroup none <path> nodev noexec nosuid <controller>
result = mount("none", controller->path(), "cgroup", MS_NODEV | MS_NOEXEC | MS_NOSUID,
controller->name());
}
}

if (result < 0) {
bool optional = controller->flags() & CGROUPRC_CONTROLLER_FLAG_OPTIONAL;

if (optional && errno == EINVAL) {
// Optional controllers are allowed to fail to mount if kernel does not support them
LOG(INFO) << "Optional " << controller->name() << " cgroup controller is not mounted";
} else {
PLOG(ERROR) << "Failed to mount " << controller->name() << " cgroup";
return false;
}
}

return true;
}

除此之外,init初始化时,Android会创建各种cgroup分组,然后通过rc配置或者Framework的接口设置各个进程所在的分组状态。接下来我们就来详细看看Android对应的cgroup分组管理策略。

Android中的cgroup分组管理策略

Android为了确保前台应用的资源使用,减少后台应用对资源的抢占,保证关键任务的执行与调度,增加了好几个进程分组:

  • foreground: 前台进程分组,大部分的应用都属于这个分组,包括系统服务、应用、桌面、系统UI等。
  • background: 后台进程分组,系统的一些常驻后台进程,如logd等可以放在这个分组中
  • system-background: 系统服务进程分组,Android一些关键系统服务可以放入该分组,如update_engine, traced_perf, system_server等都放在该分组中
  • top-app: 系统交互进程分组,正在执行的系统交互的可见应用都会放入该分组,确保前台交互应用的资源优先级
  • camera-daemon: 摄像头进程分组,摄像头相关的核心服务放入该进程,确保使用摄像头的进程资源分配的优先级

Android系统提供了task_profiles.json/system/core/libprocessgroup/profiles/)任务配置文件描述进程或者线程要执行的特定操作;每组操作都与一个配件名称相关联,并且可以通过函数SetTaskProfiles/SetProcessProfiles进行设置:

例如,原生的task_profiles.json文件:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186

{
"Attributes": [
{
"Name": "MaxCapacityCPUs",
"Controller": "cpuset",
"File": "top-app/cpus"
},
{
"Name": "UClampLatencySensitive",
"Controller": "cpu",
"File": "cpu.uclamp.latency_sensitive"
},
{
"Name": "FreezerState",
"Controller": "freezer",
"File": "cgroup.freeze"
}
],

"Profiles": [
{
"Name": "Frozen",
"Actions": [
{
"Name": "SetAttribute",
"Params":
{
"Name": "FreezerState",
"Value": "1"
}
}
]
},
{
"Name": "HighPerformance",
"Actions": [
{
"Name": "JoinCgroup",
"Params":
{
"Controller": "cpu",
"Path": "foreground"
}
}
]
},
{
"Name": "MaxPerformance",
"Actions": [
{
"Name": "JoinCgroup",
"Params":
{
"Controller": "cpu",
"Path": "top-app"
}
}
]
},
{
"Name": "CameraServicePerformance",
"Actions": [
{
"Name": "JoinCgroup",
"Params":
{
"Controller": "cpu",
"Path": "camera-daemon"
}
}
]
},

{
"Name": "ProcessCapacityLow",
"Actions": [
{
"Name": "JoinCgroup",
"Params":
{
"Controller": "cpuset",
"Path": "background"
}
}
]
},
{
"Name": "ProcessCapacityHigh",
"Actions": [
{
"Name": "JoinCgroup",
"Params":
{
"Controller": "cpuset",
"Path": "foreground"
}
}
]
},

{
"Name": "HighIoPriority",
"Actions": [
{
"Name": "JoinCgroup",
"Params":
{
"Controller": "blkio",
"Path": ""
}
}
]
},

{
"Name": "SFMainPolicy",
"Actions": [
{
"Name": "JoinCgroup",
"Params":
{
"Controller": "cpuset",
"Path": "system-background"
}
}
]
},
{
"Name": "PerfBoost",
"Actions": [
{
"Name": "SetClamps",
"Params":
{
"Boost": "50%",
"Clamp": "0"
}
}
]
},

{
"Name": "LowMemoryUsage",
"Actions": [
{
"Name": "SetAttribute",
"Params":
{
"Name": "MemSoftLimit",
"Value": "16MB"
}
},
{
"Name": "SetAttribute",
"Params":
{
"Name": "MemSwappiness",
"Value": "150"

}
}
]
},
],

"AggregateProfiles": [
{
"Name": "SCHED_SP_BACKGROUND",
"Profiles": [ "HighEnergySaving", "LowIoPriority", "TimerSlackHigh" ]
},
{
"Name": "SCHED_SP_FOREGROUND",
"Profiles": [ "HighPerformance", "HighIoPriority", "TimerSlackNormal" ]
},
{
"Name": "SCHED_SP_TOP_APP",
"Profiles": [ "MaxPerformance", "MaxIoPriority", "TimerSlackNormal" ]
},
{
"Name": "SCHED_SP_SYSTEM",
"Profiles": [ "ServicePerformance", "LowIoPriority", "TimerSlackNormal" ]
},
]
}

任务配置文件主要包括两个部分:AttributesProfilesAttributes部分描述了如何配置控制组的属性,主要包含如下内容:

  • name字段: 制定Attribute的名称
  • Controller字段: 按照名称引用cgroups.json文件的cgroup控制器
  • File字段:相应控制器下的特定文件

Attributes是任务配置文件定义中的引用;在任务配置文件之外,仅当框架需要直接访问相应文件且无法使用任务配置文件抽象访问时,才应使用属性。在所有其他情况下,应该使用任务配置文件;它们可以更好地分离所需行为及其实现详情。Profiles部分使用以下字段来包含任务配置文件定义:

  • Name字段:定义配置文件的名称
  • Actions部分: 列出配置文件对应执行的一组操作。每项操作包含如下几项:
    • Name字段: 指定操作
    • Params字段: 指定操作的一组参数

下表是常用的受支持的操作:

操作 参数 说明
SetTimerSlack Slack 定时器可宽延时间(ns)
SetAttribute Name/Value 引用Attributes部分中某一个属性的名称和值
WriteFile FilePath/Value 文件的路径和要写入的文件值
JoinCgroup Controller/Path 指定控制组的名称和对应的cgroup路径

Android12及以上的版本有一个AggregateProfiles项,包含了聚合的配置文件,每个聚合配置文件对应了一个或者多个配置文件的别名。其包含了两个部分的内容:

  • Name字段:定义聚合配置文件的名称
  • Profiles字段: 聚合配置文件中包含的配置文件名称

利用cgroup优化Android系统性能

Android提供了两种方式来控制进程的cgroup分组以及对应的优先级状态,一种是通过init脚本语言命令来设置,一种是通过libprocessgroup的接口来设置:

  • init脚本语言提供了一个task_profiles命令来设置进程的cgroup分组状态。task_profiles命令的格式如下:
1
2
3

task_profiles MaxPerformance

Android 12以下的版本也可以通过writepid来写入到对应的cgroup目录:

1
2
3

writepid /dev/cpuctl/top-app/tasks

  • API接口设置进程的cgroup分组状态: 为了保持兼容,Android 10及更高版本保留了cutils/sched_policy.h的接口:set_cpuset_policyset_sched_policyget_sched_policy ,但Android 10以上的版本已经将对应的接口全部移到了libprocessgroup中,因此建议使用processgroup/sched_policy.h的接口。

那么,利用Android提供的cgroup机制,我们可以做哪些方面的性能优化了?cgroup的核心是资源的分配与控制,确保系统优先级的任务得到更多资源,从这个角度出发,我们可以大致有如下几个优化的方向:

  • 通过cpucgroup分组管理,合理分配系统大小核心,这对于移动端大小核的异构架构来说,尤其重要。例如,将top-app, camera-daemon相关的分组绑定到大核,而backgroud/system-backgroud等分组绑定到小核,可以确保系统关键的任务得到更多资源,确保系统响应延迟
  • 为了减少渲染延迟,可以适当的将SurfaceFlinger相关的线程与服务都尽量绑定到大核上,从而提升系统渲染的帧率,减少卡顿、丢帧
  • 通过blkio分组,可以用来控制前后台的I/O资源使用,在系统高负载时限制后台的I/O资源使用,从而提高前台应用的I/O响应延迟

参考资料

原文作者:Jason Wang

更新日期:2024-06-07, 17:14:43

版权声明:本文采用知识共享署名-非商业性使用 4.0 国际许可协议进行许可

CATALOG
  1. 1. cgroup的实现原理
  2. 2. Android中的cgroup分组管理策略
  3. 3. 利用cgroup优化Android系统性能
  4. 4. 参考资料