ソースコードベース: Android R
0. 序文
前回のブログ投稿「Android におけるアプリ フリーザーの原則」では、フリーザーの有効化、フリーズ、フリーズ解除がすべて cgroup メカニズムを通じて処理されることを説明しました。
この記事では、Androidのcgroup抽象化レイヤーの基本情報と使い方を紹介します。
1. cgroup の概要
cgroups (正式名: control groups ) は、Linux カーネルが提供する機構で、単一プロセスまたは複数プロセスが使用するリソースを制限し、CPU やメモリなどのリソースのきめ細かな制御を実現できます。現在普及が進んでいる軽量コンテナであるDockerは、cgroupsが提供するリソース制限機能を利用して、CPUやメモリなどのリソース制御を完結します。
cgroups は、制御可能なリソースごとにサブシステムを定義します。一般的なサブシステムは次のとおりです。
- cpu: 主にプロセスの CPU 使用量を制限します。
- cpuaat: cgroup 内のプロセスの CPU 使用率レポートをカウントできます。
- cpuset: cgroup 内のプロセスに個別の CPU ノードまたはメモリ ノードを割り当てることができます。
- メモリ: プロセスのメモリ使用量を制限できます。
- blkio: プロセスを制限できるブロックデバイス IO。
- デバイス: 特定のデバイスにアクセスするプロセスの機能を制御できます。
- フリーザー: cgroup 内のプロセスを一時停止または再開できます。
- net_cls: cgroup 内のプロセスのネットワーク パケットにマークを付けることができ、tc (トラフィック制御) モジュールを使用してパケットを制御できます。
- ns: 異なる cgroup 下のプロセスが異なる名前空間を使用できるようにします。
Android は cgroup を使用して、CPU やメモリなどのシステム リソースの使用と割り当てを制御および検討し、Linux カーネルのcgroup v1およびcgroup v2バージョンをサポートします。
2. Android cgroup 抽象化レイヤーの概要
Android Q(10) 以降では、タスク プロファイルを通じて cgroup 抽象化レイヤーが使用されます。タスク プロファイルを使用して、スレッドまたはプロセスに適用されるセットの制限を記述することができます。システムは、タスク プロファイルで指定されているように 1 つ以上の適切な cgroup を選択します。この制限により、上位のソフトウェア層に影響を与えることなく、基礎となる cgroup 機能セットに変更を加えることができます。
Android P(9)以前のバージョンでは、利用可能な cgroup、そのマウント ポイント、およびバージョンは init .rcに設定されます。この情報は変更できますが、Android フレームワークの設定 (init.rc に基づく) では、cgroup の特定のセットが特定の場所に、特定のバージョンとサブグループ階層で存在することになります。これにより、使用する次の cgroup バージョンを選択する機能が制限され、新しい機能を利用するために cgroup 階層を変更する機能も制限されます。
Android Q(10) 以降では、タスク プロファイルで cgroup を使用します。
- cgroup 構成:開発者は、cgroups.json ファイルに cgroups 構成を記述して、cgroup グループとそのマウント ポイントおよび属性を定義します。すべての cgroup は初期初期 段階でマウントされます。
- タスク プロファイル: これらのプロファイルは、必要な機能をその機能の実装の詳細から分離する抽象化を提供します。Android フレームワークは、SetTaskProfilesインターフェイス とSetProcessProfiles インターフェイス (これらの API は Android R 以降に固有)を使用して、 task_profiles.json ファイルに記述されているようにタスク プロファイルをプロセスまたはスレッドに適用します。
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 の場合は、以下が必要です。
- コントローラー: cgroups サブシステムの名前を指定すると、タスク プロファイルの設定はこの名前に依存する必要があります。
- パス:マウント パスを指定します。このパスでのみタスク プロファイルでファイル名を指定できます。
- モード:パス ディレクトリ内のファイルの実行モードを指定するために使用されます。
- UID:ユーザー ID とパス ディレクトリ内のファイルの所有者を指定します。
- GID:グループ ID を指定し、パス ディレクトリ内のファイルの所有者を指定します。
cgroup v2 の場合、基本的に v1 と同じですが、Controller にサブ cgroup が定義されており、すべて同じディレクトリにマウントされています。子 cgroup 内のパスはルート パスを基準としています。たとえば、ここでフリーザーのパスはフリーザーを設定します。これは、ルート パス/sys/fs/cgroup/ディレクトリの下にディレクトリ フリーザーを作成することを意味します。
さらに、複数の cgroups.jsonファイルが存在する場合があります。
/system/core/libprocessgroup/profiles/cgroups.json //默认文件
/system/core/libprocessgroup/profiles/cgroups_<API level>.json //API级别的文件,R版本没有,S版本很多
/vendor/xxx/cgroups.json //vendor自定义文件
これら 3 つのファイルの読み込み順序は、デフォルト -> API レベル -> ベンダー であるため、上書き処理が行われ、後のファイルで定義されたコントローラーの値が前のファイルと同じである限り、前の定義は上書きされます。 。
4. タスクプロファイル
ファイルパス: 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" ]
},
...
]
}
ファイル構成全体は中括弧で囲まれており、次の 3 つの部分で構成されます。
- 属性
- プロフィール
- 集約プロファイル
さらに、複数の 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 と同じで、名前に基づいて照合され、2 つのファイルに同じ名前の項目が定義されている限り、後者が前者の定義を上書きします。
4.1 属性セクション
属性の cgroup 内の特定のファイル。
属性は、タスク プロファイル ファイル定義内の参照です。タスク プロファイルの外部では、これらのファイルへの直接アクセスは、フレームワークが要求した場合にのみ可能であり、タスク プロファイルの抽象化を使用してアクセスすることはできません。他の場合には、タスク プロファイルを使用します。これにより、目的の動作とその実装の詳細をより適切に分離できます。
属性の各項目には次のものが含まれます。
- 名前:属性の名前。名前の値は、プロファイルで参照されるときに使用されます。
- コントローラー: cgroups.json ファイル内の cgroup コントローラーを指し、cgroup のコントローラー値を指します。
- ファイル: cgroup コントローラーが配置されているディレクトリ内の特別なファイル。
上記のように:
"Attributes": [
{
"Name": "FreezerState",
"Controller": "freezer",
"File": "cgroup.freeze"
}
],
コントローラーが freezer である cgroup が使用されます。上記のセクション 3 から、cgroups v2 の形式が採用されていることがわかります。cgroup のパスは/sys/fs/cgroup/freezer/です。ここで定義された属性は、このディレクトリ内の cgroup を指定します。ファイルを凍結します。
コードでは、 各属性はProfileAttributeクラスを通じて管理されます。
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 プロファイルセクション
それぞれの定義には次のものが含まれます。
- Name:指定 profile name;
- アクション: プロファイルの適用時に実行する必要があるアクションのセットをリストします。各アクションには次のものが含まれます。
- 名前: 実行する必要があるアクション カテゴリ。
- Params: アクションに必要なパラメータのコレクション。
アクションの名前のオプション カテゴリとその Params 設定を見てみましょう。
アクション | パラメータ | 説明 |
---|---|---|
SetTimerSlack |
Slack |
タイマーを延長できる時間 (ns 単位) |
SetAttribute |
Name |
「属性」で属性の名前を参照します。 |
Value |
属性で指定されたファイルに書き込まれるデータ | |
WriteFile |
FilePath |
ファイルパス |
Value |
ファイルに書き込まれる値 | |
JoinCgroup |
Controller |
書類 cgroups.json 中的cgroup名称 |
Path |
cgroup 階層内のサブグループ パス |
4.2.1 SetTimerSlack
SetTimerSlack には、 /proc/PID/timerslack_nsノードに対応する Slack パラメータが 1 つだけあります。TimerSlack は、システムの電力消費を削減し、タイマー時間の不均一を回避し、CPU を頻繁にウェイクアップすることを目的として、Linux システムによって設定された調整戦略です。この値は、select、epoll_wait、sleep、その他の API のウェイクアップ時間など、プロセスのタイマーに関連します。
Linux 4.6 以降のバージョンでは、/proc/PID/timerslack_ns ノードがサポートされます。
具体的な参照: https://cloud.tencent.com/developer/article/1836285
コードでは、SetTimerSlackAction クラスを通じてプロファイルを管理します。
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 は、SetAttributeAction に対応する task_profiles.json 内の属性にリンクされます。
SetAttribute には 2 つのパラメータがあり、Name は以前に定義された属性の名前を指し、Value は属性に対応する cgroup の子ノードに書き込まれる値を指します。
コードでは、SetAttribute プロファイルはSetAttributeActionクラスを通じて管理されます。
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;
}
クラスには、ProfileAttribute 型のメンバー変数属性があります。
コードから、属性内のパスに従って、値が最初にファイル ノードに書き込まれることがわかります。
4.2.3 Cgroup に参加する
JoinCgroup には、Controller と Path の 2 つのパラメータしかありません。Controller は cgroup のサブシステムを指し、Path はサブシステム (subcgroup) の下のパスを指します。この構成により、このプロファイルに設定されたプロセスまたはスレッドがサブシステムのサブ cgroup に追加され、この cgroup のリソース制限の対象になります。
このプロファイルは、コード内の SetCgroupActionクラスを通じて管理されます。
たとえば上記のような場合:
{
"Attributes": [
...
],
"Profiles": [
{
"Name": "Frozen",
"Actions": [
{
"Name": "JoinCgroup",
"Params":
{
"Controller": "freezer",
"Path": ""
}
}
]
}
],
"AggregateProfiles": [
...
]
}
ここで構成されたプロファイル名は Frozen、使用される Cgroup コントローラーは freezer、パスは空です。
つまり、このプロファイルは /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);
}
}
プロセスがフリーズまたはフリーズ解除すると、 SetProcessProfiles() が呼び出され、具体的には SetCgroupAction タイプのプロファイルが呼び出され、最後に 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;
}
この関数を使用して、最初に、コントローラーの GetProcsFilePath() インターフェイスを通じて、プロファイルに対して変更する必要があるパスを取得します。パラメーターは、プロファイルに構成されたパスです。
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 セクションも含まれています。
ここでは、次の内容で構成される 1 つ以上のプロファイル エイリアスが定義されます。
- 名前: 集約プロファイルの名前を指定します。
- プロファイル: この集約プロファイルに含まれるプロファイル名のコレクション。
集約プロファイルが適用されると、それに含まれるすべてのプロファイルが自動的に適用されます。
上記のように:
"AggregateProfiles": [
{
"Name": "SCHED_SP_FOREGROUND",
"Profiles": [ "HighPerformance", "HighIoPriority", "TimerSlackNormal" ]
},
...
]
SCHED_SP_FOREGROUND 集約プロファイルを適用すると、それに含まれるすべてのプロファイル (高)
Performance、HighIoPriority、TimerSlackNormal)が適用されます。
さらに、再帰を使用しない場合、集約プロファイルには個別のプロファイルまたは他の集約プロファイルを含めることができます。
5. cgroupsの初期化
init 起動の第 2 フェーズでは、次のように呼び出されます。
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 がコントローラー情報を読み取れるようにします。
6. タスクプロファイル
コードを通して、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() を使用して 2 つのファイルを解析します。
- 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 セクションがそれぞれ解析されます。ここではあまり分析しません。タスク プロファイルの解析が完了すると、システムは SetProcessProfiles() または SetTaskProfiles() を使用して、プロファイルを適用する目的を達成します。
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 のシングルトンを通じてタスク プロファイルの下で 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;
}
さらに、プロファイルの名前によって詳細プロファイルを決定し、上記のセクション4.2.3に示すようにExecuteForProcess()関数を呼び出します。最終的な詳細プロファイルは SetCgroupAction です。
プロセスは大まかに次のとおりです。
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() 関数と同様で、最終的にはプロファイルアクションの ExecuteForTask() 関数が呼び出されます。
この時点で、Android の cgroups の抽象層について大まかに説明しました。コード ロジックは非常に明確です。メインのカーネル コードは後で詳細に分析します。要約は次のとおりです。
- cgroups.json を通じて cgroup のすべてのサブシステムを構成します。コントローラーの名前は、後で属性またはプロファイルで使用されます。さらに、そのような cgroups.jsonファイルが多数存在する可能性があり、ロード順序によっては、対象範囲が存在します。
- すべてのアクティビティは task_profiles.json を通じて構成され、cgroups.json で以前に定義されたサブシステムを利用して、属性、プロファイル、および AggregateProfile をさらに定義します。同様に、ロード順序もあり、カバレッジもあります。
- cgroups.json の解析は init の第 2 フェーズで完了します。
- システムは、すべてのプロファイルを管理するために TaskProfile の単一インスタンスを作成し、対応するアクションもプロファイル内に維持されます。
- SetProcessProfiles() インターフェイスを通じて特定のプロファイルをプロセスに適用します。
- SetTaskProfiles() インターフェイスを通じて特定のプロファイルをスレッドに適用します。