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 v1cgroup 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 到线程上;

猜你喜欢

转载自blog.csdn.net/jingerppp/article/details/131854291