Android 中 cgroup抽象层详解
源码基于: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_.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_.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.hclass 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.cppbool 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 deadreturn true;}PLOG(ERROR) << "set_timerslack_ns write failed";}}// TODO: Remove when /proc//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.cppbool 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.cppvoid 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.cppbool 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.cppstd::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(""), std::to_string(uid));proc_path = regex_replace(proc_path, std::regex(""), 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.cppint SecondStageMain(int argc, char** argv) {...am.QueueBuiltinAction(SetupCgroupsAction, "SetupCgroups");...
}
system/core/init/init.cppstatic Result SetupCgroupsAction(const BuiltinArguments&) {// Have to create using make_dir function// for appropriate sepolicy to be set for itmake_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.cppTaskProfiles::TaskProfiles() {// load system task profilesif (!Load(CgroupMap::GetInstance(), TASK_PROFILE_DB_FILE)) {LOG(ERROR) << "Loading " << TASK_PROFILE_DB_FILE << " for [" << getpid() << "] failed";}// load vendor task profiles if the file existsif (!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.cppbool SetProcessProfiles(uid_t uid, pid_t pid, const std::vector& profiles) {return TaskProfiles::GetInstance().SetProcessProfiles(uid, pid, profiles);
}
这是一个全局的函数,通过 TaskProfiles 的单例调用 task profiles 下的SetProcessProfiles():
system/core/libprocessgroup/task_profiles.cppbool TaskProfiles::SetProcessProfiles(uid_t uid, pid_t pid,const std::vector& 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.cppbool SetTaskProfiles(int tid, const std::vector& 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 到线程上;
本文来自互联网用户投稿,文章观点仅代表作者本人,不代表本站立场,不承担相关法律责任。如若转载,请注明出处。 如若内容造成侵权/违法违规/事实不符,请点击【内容举报】进行投诉反馈!
