源码基于:Android R
0. 前言
在之前的博文《Android中app freezer原理》一文中,我们看到冻结器的enable、freeze、unfreeze 都是通过 cgroup 的机制进行处理。
本文将介绍下 Android 中 cgroup 的抽象层基本信息和使用方式。
1. cgroups 简介
cgroups (全称:control groups) 是 Linux 内核提供的一种可以限制单个进程或者多个进程所使用资源的机制,可以对 CPU、memory 等资源实现精细化的控制。目前越来越活的轻量级容器 Docker 就使用了 cgroups 提供的资源限制能力来完成 CPU、memory 等部门的资源控制。
cgroups 为每种可以控制的资源定义了一个子系统,典型的子系统如下:
- cpu:主要限制进程的 cpu 使用率;
- cpuaat:可以统计 cgroups 中的进程的 cpu 使用报告;
- cpuset:可以为 cgroups 中的进程分配单独的 cpu 节点或内存节点;
- memory:可以限制进程的 memory 使用量;
- blkio:可以限制进程的块设备 io;
- devices:可以控制进程能够访问某些设备;
- freezer:可以挂起或恢复 cgroups 中的进程;
- net_cls:可以标记 cgroups 中进程的网络数据包,然后可以使用 tc (traffic control)模块对数据包进行控制;
- ns:可以使不同的 cgroups 下面的进程使用不同的 namespace;
Android 使用 cgroups 来控制和考量如 CPU、memory 等系统资源的使用和分配情况,并支持 Linux 内核 cgroup v1 和 cgroup v2 版本。
2. Android cgroup 抽象层简介
在 Android Q(10) 或更高版本通过 task profiles 使用 cgroup 抽象层,task profiles 可以用来描述应用于某个线程或进程的一个set 或 sets 的限制。系统依照 task profiles 的规定选择一个或多个适当的 cgroups。通过这种限制,可以对底层的 cgroup 功能集进行更改,而不会影响较高的软件层。
在 Android P(9) 和更低版本,可用的 cgroups 以及他们的挂载点、版本都会在 init.rc 中设定。虽然这些信息可以更改,但 Android framework 的设定 (基于init.rc) 是一组特定的 cgroups 存在于特定的位置,并具有特定版本和子group 层级。这限制了选择下一个 cgroup 版本使用的能力,也限制了更改 cgroup 层级去使用新功能的能力。
在 Android Q(10) 或更高版本,将 cgroups 和 task profiles 搭配使用:
- cgroup 配置:开发人员在 cgroups.json 文件中 cgroups 描述 cgroups 配置,以此定义 cgroups 组以及他们的挂载点和 attibutes。所有的 cgroups 将在 early-init 阶段被挂载上。
- task profiles:这些配置文件提供了一种抽象概念,将必需的功能与该功能的实现详情分离。Android framework 使用 SetTaskProfiles 和 SetProcessProfiles 接口(这些API 是 Android R 或更高版本独有),按照 task_profiles.json 文件中描述将 task profiles 应用到一个进程或一个线程。
3. cgroups.json
文件路径:system/core/libprocessgroup/profiles/cgroups.json
system/core/libprocessgroup/profiles/cgroups.json
{
"Cgroups": [
{
"Controller": "blkio",
"Path": "/dev/blkio",
"Mode": "0755",
"UID": "system",
"GID": "system"
},
{
"Controller": "cpu",
"Path": "/dev/cpuctl",
"Mode": "0755",
"UID": "system",
"GID": "system"
},
{
"Controller": "cpuacct",
"Path": "/acct",
"Mode": "0555"
},
{
"Controller": "cpuset",
"Path": "/dev/cpuset",
"Mode": "0755",
"UID": "system",
"GID": "system"
},
{
"Controller": "memory",
"Path": "/dev/memcg",
"Mode": "0700",
"UID": "root",
"GID": "system"
},
{
"Controller": "schedtune",
"Path": "/dev/stune",
"Mode": "0755",
"UID": "system",
"GID": "system"
}
],
"Cgroups2": {
"Path": "/sys/fs/cgroup",
"Mode": "0755",
"UID": "system",
"GID": "system",
"Controllers": [
{
"Controller": "freezer",
"Path": "freezer",
"Mode": "0755",
"UID": "system",
"GID": "system"
}
]
}
}
cgroup v1 和 cgroup v2 描述的规则不一样。
对于 cgroup v1,必须拥有:
- Controller:指定 cgroups 子系统名称,之后 task profiles 中设定需要依赖该名称;
- Path:指定挂载的路径,有了该路径 task profiles 下才可以指定文件名;
- Mode:用于指定Path 目录下文件的执行 mode;
- UID:指定 user ID,指定Path 目录下文件的owner;
- GID:指定 group ID,指定Path 目录下文件的owner;
对于 cgroup v2,基本同 v1,Controllers 中定义了子 cgroup,这些都挂载在同一个目录下。子 cgroup 中的 Path 是相对于根 Path。例如这里 freezer 的 Path 设定了 freezer,就是在 根 Path /sys/fs/cgroup/ 目录下创建一个目录 freezer。
另外 cgroups.json 文件可能不止一个:
/system/core/libprocessgroup/profiles/cgroups.json //默认文件
/system/core/libprocessgroup/profiles/cgroups_<API level>.json //API级别的文件,R版本没有,S版本很多
/vendor/xxx/cgroups.json //vendor自定义文件
这三种文件加载顺序是:默认 -> API 级别 -> vendor,于是就存在一个覆盖的流程,只要后面的文件中定义的 Controller 值与前面的相同,就会覆盖前者的定义。
4. task profiles
文件路径:system/core/libprocessgroup/profiles/task_profiles.json
{
"Attributes": [
{
"Name": "MemSoftLimit",
"Controller": "memory",
"File": "memory.soft_limit_in_bytes"
},
{
"Name": "MemSwappiness",
"Controller": "memory",
"File": "memory.swappiness"
},
{
"Name": "FreezerState",
"Controller": "freezer",
"File": "cgroup.freeze"
}
],
"Profiles": [
{
"Name": "Frozen",
"Actions": [
{
"Name": "JoinCgroup",
"Params":
{
"Controller": "freezer",
"Path": ""
}
}
]
},
{
"Name": "TimerSlackHigh",
"Actions": [
{
"Name": "SetTimerSlack",
"Params":
{
"Slack": "40000000"
}
}
]
},
{
"Name": "PerfBoost",
"Actions": [
{
"Name": "SetClamps",
"Params":
{
"Boost": "50%",
"Clamp": "0"
}
}
]
},
{
"Name": "HighMemoryUsage",
"Actions": [
{
"Name": "SetAttribute",
"Params":
{
"Name": "MemSoftLimit",
"Value": "512MB"
}
},
{
"Name": "SetAttribute",
"Params":
{
"Name": "MemSwappiness",
"Value": "100"
}
}
]
},
{
"Name": "FreezerEnabled",
"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" ]
},
{
"Name": "SCHED_SP_TOP_APP",
"Profiles": [ "MaxPerformance", "MaxIoPriority", "TimerSlackNormal" ]
},
...
]
}
整个文件配置由一个大括号包含,总体由三部分组成:
- Attributes
- Profiles
- AggregateProfiles
另外,task_profiles.json 文件也不止一个:
system/core/libprocessgroup/profiles/task_profiles.json //默认
system/core/libprocessgroup/profiles/task_profiles_<API level>.json //API级别的文件,R版本没有,S有很多
vendor/xxx/task_profiles.json //vendor配置
加载、覆盖的顺序同cgroups.json,按照 Name 来匹配,只要两个文件中定义同名项,后者就会覆盖前者的定义。
4.1 Attributes 段
Attributes 中 cgroups 中特定的文件。
Attributes 是task profiles 文件定义中的引用。在 task profiles 之外,只有当 framework 请求直接访问这些文件,且无法使用 task profiles 抽象访问时。其他情况下,使用 task profiles,它可以更好地分离所需行为及其实现详情。
Attributes 中每一项包含:
- Name: 该 Attribute 的名称,profiles 中引用时使用该Name 值;
- Controller:引用 cgroups.json 文件中的一个 cgroup controller,引用 cgroup 的Controller 值;
- File:在 cgroup Controller 所在的目录下的一个特殊文件;
如上面:
"Attributes": [
{
"Name": "FreezerState",
"Controller": "freezer",
"File": "cgroup.freeze"
}
],
用的是 Controller 为 freezer 的 cgroup,从上面第 3 节中得知,它采用 cgroups v2 的格式,cgroup Path 为 /sys/fs/cgroup/freezer/,这里定义的 attribute 指定的是该目录下 cgroup.freeze 文件。
在代码中,通过 ProfileAttribute 类来管理每个 Attribute:
system/core/libprocessgroup/task_profiles.h
class ProfileAttribute {
public:
ProfileAttribute(const CgroupController& controller, const std::string& file_name)
: controller_(controller), file_name_(file_name) {}
const CgroupController* controller() const { return &controller_; }
const std::string& file_name() const { return file_name_; }
void Reset(const CgroupController& controller, const std::string& file_name);
bool GetPathForTask(int tid, std::string* path) const;
private:
CgroupController controller_;
std::string file_name_;
};
4.2 Profiles 段
每一项定义包含:
- Name:指定 profile name;
- Actions:罗列该 profile 被应用时,需要执行的 actions 集合,每个 action 包含:
- Name:需要执行的 action 类别;
- Params:该 action 所需的参数的集合;
下面来看下Actions 中 Name 可选的类别及其Params 配置:
Action | Parameter | Description |
---|---|---|
SetTimerSlack | Slack | 定时器可宽延的时间,单位为 ns |
SetAttribute | Name | 引用Attributes 中的某一个属性的名称 |
Value | 要写入到attribute指定文件中的数据 | |
WriteFile | FilePath | 文件路径 |
Value | 要写入到文件的值 | |
JoinCgroup | Controller | 文件 cgroups.json 中的cgroup名称 |
Path | cgroup 层次结构中的额子组路径 |
4.2.1 SetTimerSlack
SetTimerSlack 只有一个参数 Slack,这个参数对应 /proc/PID/timerslack_ns 节点。TimerSlack 是Linux 系统为了降低系统功耗、避免 timer 时间参差不齐、过于频繁的唤醒 cpu,而设置的一种对齐策略。这个值关系到进程的定时器,如 select、epoll_wait、sleep 等API 的唤醒时间。
在Linux 4.6+ 版本,都是支持 /proc/PID/timerslack_ns 节点。
具体参考:https://cloud.tencent.com/developer/article/1836285
在代码中,通过 SetTimerSlackAction 类来管理该 profile:
system/core/libprocessgroup/task_profiles.cpp
bool SetTimerSlackAction::ExecuteForTask(int tid) const {
static bool sys_supports_timerslack = IsTimerSlackSupported(tid);
if (sys_supports_timerslack) {
auto file = StringPrintf("/proc/%d/timerslack_ns", tid);
if (!WriteStringToFile(std::to_string(slack_), file)) {
if (errno == ENOENT) {
// This happens when process is already dead
return true;
}
PLOG(ERROR) << "set_timerslack_ns write failed";
}
}
// TODO: Remove when /proc/<tid>/timerslack_ns interface is backported.
if (tid == 0 || tid == GetThreadId()) {
if (prctl(PR_SET_TIMERSLACK, slack_) == -1) {
PLOG(ERROR) << "set_timerslack_ns prctl failed";
}
}
return true;
}
4.2.2 SetAttribute
SetAttribute则跟 task_profiles.json 中的Attributes挂钩起来,对应了SetAttributeAction。
SetAttribute 有两个参数,Name 指的就是之前定义的 Attribute 的名称,Value 则是往 Attribute 对应的cgroup 的子节点写入的值。
在代码中,通过 SetAttributeAction 类来管理 SetAttribute 的profile:
system/core/libprocessgroup/task_profiles.cpp
bool SetAttributeAction::ExecuteForTask(int tid) const {
std::string path;
if (!attribute_->GetPathForTask(tid, &path)) {
LOG(ERROR) << "Failed to find cgroup for tid " << tid;
return false;
}
if (!WriteStringToFile(value_, path)) {
PLOG(ERROR) << "Failed to write '" << value_ << "' to " << path;
return false;
}
return true;
}
类中会有一个成员变量 attribute,类型为 ProfileAttribute。
代码中可以获知,先根据 Attribute 中的path,再将 value 写入文件节点中。
4.2.3 JoinCgroup
JoinCgroup 只有两个参数Controller 和 Path,Controller 指的是 cgroups 的 subsystem,Path 则是指该 subsystem 下的路径,也就是子 cgroup。通过该配置,将设置成这个profile 的进程或线程加入到该 subsystem 的子 cgroup中,受这个cgroup 的资源限制。
在代码中通过 SetCgroupAction 类来管理这个profile。
例如上面的:
{
"Attributes": [
...
],
"Profiles": [
{
"Name": "Frozen",
"Actions": [
{
"Name": "JoinCgroup",
"Params":
{
"Controller": "freezer",
"Path": ""
}
}
]
}
],
"AggregateProfiles": [
...
]
}
这里配置的 profile 名字为 Frozen,利用的是Cgroup Controller 为 freezer,Path 为空。
也就是说该 profile 需要使用 /sys/fs/cgroup/freezer/ 目录下的某个子 cgroup 文件。具体看系统调用。通过查找,系统在 CachedAppOptimizer 类中会调用 Process.setProcessFrozen(),进而调用到 jni android_util_Process_setProcessFrozen() 接口:
frameworks/base/core/jni/android_util_Process.cpp
void android_os_Process_setProcessFrozen(
JNIEnv *env, jobject clazz, jint pid, jint uid, jboolean freeze)
{
bool success = true;
if (freeze) {
success = SetProcessProfiles(uid, pid, {"Frozen"});
} else {
success = SetProcessProfiles(uid, pid, {"Unfrozen"});
}
if (!success) {
signalExceptionForGroupError(env, EINVAL, pid);
}
}
当进程进行 freeze 或 unfreeze 的时候,会调用 SetProcessProfiles(),精细是 SetCgroupAction 类型的profile,最终调用 ExecuteForProcess():
system/core/libprocessgroup/task_profiles.cpp
bool SetCgroupAction::ExecuteForProcess(uid_t uid, pid_t pid) const {
std::string procs_path = controller()->GetProcsFilePath(path_, uid, pid);
unique_fd tmp_fd(TEMP_FAILURE_RETRY(open(procs_path.c_str(), O_WRONLY | O_CLOEXEC)));
if (tmp_fd < 0) {
PLOG(WARNING) << "Failed to open " << procs_path;
return false;
}
if (!AddTidToCgroup(pid, tmp_fd)) {
LOG(ERROR) << "Failed to add task into cgroup";
return false;
}
return true;
}
通过函数,先通过 Controller 的 GetProcsFilePath() 接口获取该profile 需要修改的path,参数为该 profile 配置的 Path:
system/core/libprocessgroup/cgroup_map.cpp
std::string CgroupController::GetProcsFilePath(const std::string& rel_path, uid_t uid,
pid_t pid) const {
std::string proc_path(path());
proc_path.append("/").append(rel_path);
proc_path = regex_replace(proc_path, std::regex("<uid>"), std::to_string(uid));
proc_path = regex_replace(proc_path, std::regex("<pid>"), std::to_string(pid));
return proc_path.append(CGROUP_PROCS_FILE);
}
最终写的文件就是 CGROUP_PROCS_FILE,也就是 cgroup.procs 文件。
4.3 AggregateProfiles 段
在 Android 12 或更高版本中,task_profiles.json 文件中还包含了 AggregateProfiles 段。
这里定义一个或多个 profile 的别名,由一下内容组成:
- Name:指定aggregate profile 的名称;
- Profiles:该 aggregate profile 包含的 profil 名称集合;
当一个 aggregate profile 被应用时,里面包含的所有的 profile 都会被自动的应用。
如上面:
"AggregateProfiles": [
{
"Name": "SCHED_SP_FOREGROUND",
"Profiles": [ "HighPerformance", "HighIoPriority", "TimerSlackNormal" ]
},
...
]
当应用 SCHED_SP_FOREGROUND 这个 aggregate profile时,里面包含的所有的 profiles (High
Performance、HighIoPriority、TimerSlackNormal) 都会被应用。
另外,如果没有递归,aggregate profiles 中可以包含单独的profiles 或其他 aggregate profiles。
5. cgroups 初始化
在 init 启动的第二阶段会调用:
system/core/init/init.cpp
int SecondStageMain(int argc, char** argv) {
...
am.QueueBuiltinAction(SetupCgroupsAction, "SetupCgroups");
...
}
system/core/init/init.cpp
static Result<void> SetupCgroupsAction(const BuiltinArguments&) {
// Have to create <CGROUPS_RC_DIR> using make_dir function
// for appropriate sepolicy to be set for it
make_dir(android::base::Dirname(CGROUPS_RC_PATH), 0711);
if (!CgroupSetup()) {
return ErrnoError() << "Failed to setup cgroups";
}
return {};
}
创建一个 CGROUPS_RC_PATH 文件:/dev/cgroup_info/cgroup.rc
之后将 cgroups.json 文件的信息写入到 cgroup.rc 文件中,以供 task_profiles 读取controller 信息。
6. Task profiles
通过代码,我们其实可以清晰看到,TaskProfiles 类在构造的时候开始解析 task_profile.json:
syste/core/libprocessgroup/task_profiles.cpp
TaskProfiles::TaskProfiles() {
// load system task profiles
if (!Load(CgroupMap::GetInstance(), TASK_PROFILE_DB_FILE)) {
LOG(ERROR) << "Loading " << TASK_PROFILE_DB_FILE << " for [" << getpid() << "] failed";
}
// load vendor task profiles if the file exists
if (!access(TASK_PROFILE_DB_VENDOR_FILE, F_OK) &&
!Load(CgroupMap::GetInstance(), TASK_PROFILE_DB_VENDOR_FILE)) {
LOG(ERROR) << "Loading " << TASK_PROFILE_DB_VENDOR_FILE << " for [" << getpid()
<< "] failed";
}
}
主要通过 Load() 去解析两个文件:
- TASK_PROFILE_DB_FILE (/etc/task_profiles.json)
- TASK_PROFILE_DB_VENDOR_FILE (/vendor/etc/task_profiles.json)
在Load() 中会分别解析 task_profiles.json 文件中的 Attributes、Profiles、AggregateProfiles 段 。这里暂不过多剖析。我们在 task profiles 解析完成之后,系统通过 SetProcessProfiles() 或 SetTaskProfiles() 来达到应用 profile 的目的。
6.1 SetProcessProfiles()
system/core/libprocessgroup/processgroup.cpp
bool SetProcessProfiles(uid_t uid, pid_t pid, const std::vector<std::string>& profiles) {
return TaskProfiles::GetInstance().SetProcessProfiles(uid, pid, profiles);
}
这是一个全局的函数,通过 TaskProfiles 的单例调用 task profiles 下的SetProcessProfiles():
system/core/libprocessgroup/task_profiles.cpp
bool TaskProfiles::SetProcessProfiles(uid_t uid, pid_t pid,
const std::vector<std::string>& profiles) {
for (const auto& name : profiles) {
TaskProfile* profile = GetProfile(name);
if (profile != nullptr) {
if (!profile->ExecuteForProcess(uid, pid)) {
PLOG(WARNING) << "Failed to apply " << name << " process profile";
}
} else {
PLOG(WARNING) << "Failed to find " << name << "process profile";
}
}
return true;
}
进一步通过 profiles 的name,确定精细的 profile,进而调用 ExecuteForProcess() 函数,如上面的第 4.2.3 节,最终精细的就是 SetCgroupAction 这个 profile。
流程大致如下:
6.2 SetTaskProfiles()
system/core/libprocessgroup/processgroup.cpp
bool SetTaskProfiles(int tid, const std::vector<std::string>& profiles, bool use_fd_cache) {
return TaskProfiles::GetInstance().SetTaskProfiles(tid, profiles, use_fd_cache);
}
具体的流程同 SetProcessProfiles() 函数,最终调用的是 profile action 的ExecuteForTask() 函数。
至此,关于Android 中 cgroup 的抽象层大致讲述完了,代码逻辑很清晰,主要的内核代码在后期会详细剖析。这里总结下:
- 通过 cgroups.json 配置 cgroup 的所有子系统,Controller 的名称会在后面 Attributes 或 Profiles 中使用到。另外,这样的 cgroups.json 文件可能还有很多,有加载顺序,也就有了覆盖;
- 通过 task_profiles.json 配置所有活动,利用之前的 cgroups.json 中定义的子系统,进一步定义Attributes、Profiles 及 AggregateProfiles。同样的,也有加载顺序,也就有了覆盖;
- cgroups.json 的解析是在 init 的第二阶段完成;
- 系统会创建一个 TaskProfiles 的单例,管理所有的 profile,而 profile 中也维护着对应的 actions;
- 通过接口 SetProcessProfiles() 来应用特定的 profile 到进程上;
- 通过接口 SetTaskProfiles() 来应用特定的 profile 到线程上;