Android CGroup实际情况调研

阿凡达2018-07-04 12:45
内容概要:对于Android线程所属的CGroup,一些资料认为是通过线程优先级来进行划分的。但经实践,发现部分Android版本与该观点并不一致,那么实际情况又是怎样的呢?本篇文章就该问题进行了探讨。 
一、CGroup简要介绍
在Linux中,不同线程分配cpu时间片的策略首先是基于线程优先级的,线程优先级越高,越容易分配到cpu。但是这样就产生了低优先级线程一直都被抢占cpu时间的问题,为解决该问题,Linux 2.6.23版本中引入了CFS策略,该策略不但会参考单个线程的优先级,还会追踪每个线程已经获取到的time slice数量,如果高优先级的线程已经执行了很长时间,但低优先级的线程一直在等待,后续系统会保证低优先级的线程也能获取更多的CPU时间。但这就产生了新的问题:优先级高的线程并不一定总能在争取时间片上有绝对的优势,反映在Android中,就会出现UI线程被后台线程抢占cpu时间的问题。所以在Linux 2.6.24中又引入了cgroups的概念,让从属于特定CGroup的线程能够占据更多的时间片而不被低线程抢占,从而提升了总体效率 [1]
二、Android中不同版本所属CGroup调研
在Android中,存在两类特别重要的CGroup,一类是foreground group,UI线程属于这一类。另一类是background group,工作线程属于这一类。那么你可能要问,线程所属foreground group和background group到底怎样划分呢?查阅API文档以及相关资料 [2],当我们使用setThreadPriority,nice值大于等于THREAD_PRIORITY_BACKGROUND将属于background group,其余属于foreground group。
但是,这种说法正确嘛?让我们来做个实验,首先通过以下代码设置线程优先级。
private static final ThreadFactory sDBThreadFactory = new ThreadFactory() {
        private final AtomicInteger mCount = new AtomicInteger(1);

        public Thread newThread(@NonNull Runnable r) {
            WorkerRunnable wr = new WorkerRunnable(r, Process.THREAD_PRIORITY_BACKGROUND);
            return new Thread(wr, "Async DB Thread #" + mCount.getAndIncrement());
        }
};


private static class WorkerRunnable implements Runnable {
        Runnable runnable;
        int priority;
        String tag;

        public WorkerRunnable(Runnable runnable, int priority) {
            this(runnable, priority, "WorkerRunnable");
        }

        public WorkerRunnable(Runnable runnable, int priority, String tag) {
            this.runnable = runnable;
            this.priority = priority;
            this.tag = tag;
        }

        @Override
        public void run() {
            android.os.Process.setThreadPriority(priority);
            runnable.run();
        }

        @Override
        public String toString() {
            return tag + ":" + runnable;
        }
    }

在Android5.1.0中当线程优先级为Process.THREAD_PRIORITY_BACKGROUND的Async DB Thread所属的进程处于前台时,其cgroup却为fg,当所属进程进入到后台时,其cgroup又为bg:

而在Android 4.1中,无论是否处于前台,其cgroup都为bg:
看来,实际情况是和版本有关,那么线程的cgroup是具体怎样设置的呢?查阅相关源代码 [3],找到是在sched_policy.c的get_sched_policy方法进行相关设置的,这里以Android4.1为例,其他版本逻辑基本一致,除了Android7.0 [4],增加了一层宏定义判断#ifdef USE_CPUSETS。
int get_sched_policy(int tid, SchedPolicy *policy)
{
#ifdef HAVE_GETTID
    if (tid == 0) {
        tid = gettid();
    }
#endif
    pthread_once(&the_once, __initialize);
    if (__sys_supports_schedgroups) {
        char grpBuf[32];
        if (getSchedulerGroup(tid, grpBuf, sizeof(grpBuf)) < 0)
            return -1;
        if (grpBuf[0] == '\0') {
            *policy = SP_SYSTEM;
        } else if (!strcmp(grpBuf, "apps/bg_non_interactive")) {
            *policy = SP_BACKGROUND;
        } else if (!strcmp(grpBuf, "apps")) {
            *policy = SP_FOREGROUND;
        } else {
            errno = ERANGE;
            return -1;
        }
    } else {
        int rc = sched_getscheduler(tid);
        if (rc < 0)
            return -1;
        else if (rc == SCHED_NORMAL)
            *policy = SP_FOREGROUND;
        else if (rc == SCHED_BATCH)
            *policy = SP_BACKGROUND;
        else {
            errno = ERANGE;
            return -1;
        }
    }
    return 0;
}
上述代码基本逻辑概要如下:

该方法首先查看变量__sys_supports_schedgroups是否为true,
if (!access("/dev/cpuctl/tasks", F_OK)) {
        __sys_supports_schedgroups = 1;
}
也就是查看/dev/cpuctl/tasks是否存在:当/dev/cpuctl/tasks存在时,access返回0,满足条件__sys_supports_schedgroups为1。
那么Android5.1满足什么样的条件呢?实际上,在Android5.1的init.rc [5]中会创建了该tasks文件,但4.1.1 [6]不存在该文件。那么5.1.0系统就使用getSchedulerGroup得到cgroup,也就是查找/proc/线程id/cgroup文件中的2:cpu:/一行的内容。
当整个进程在前台时,其2:cpu:/的值为空,而处于后台时,则为bg_non_interactive。
而/proc/<pid>/cgroup 文件中的2:cpu:的值又是怎样写进去的呢?参考Linux源码cgroup.c中的处理 [7]和相关资料 [8],发现创建的子线程也是同样用的所属进程的cgroup。
接下来是Android4.1.1,他直接使用sched_getscheduler,也就是我们之前sched_setscheduler设置的policy:
sched_setscheduler(tid, (policy == SP_BACKGROUND) ? SCHED_BATCH : SCHED_NORMAL, ¶m);
而sched_setscheduler则是由set_sched_policy调用:
if (gDoSchedulingGroup) {
    if (prio >= ANDROID_PRIORITY_BACKGROUND) {
        set_sched_policy(androidGetTid(), SP_BACKGROUND);
    } else if (prio > ANDROID_PRIORITY_AUDIO) {
        set_sched_policy(androidGetTid(), SP_FOREGROUND);
    } else {
         // defaults to that of parent, or as set by requestPriority()
    }
}
也就是通过线程的优先级来设置所属CGroup。
上面提到的/dev/cpuctl/tasks和在Android7.0中引入的/dev/cpuset/等文件的创建在init.rc中进行定义,不同的版本有不同的定义,这里查阅不同版本的源码,做一下Android不同版本的CGroup的实际情况的总结:
Android 4.0 Ice Cream Sandwich,根据线程优先级设置cgroup [9]
Android 4.1/4.2/4.3 Jelly Bean,根据线程优先级设置cgroup [6]
Android 4.4 KitKat,根据线程优先级设置cgroup [10]
Android 5.0/5.1 Lollipop,取所属进程的cgroup值 [5]
Android 6.0 Marshmallow,取所属进程的cgroup值 [11]
Android 7.0 Nougat,取所属进程的cgroup值 [12] 

本文来自网易实践者社区,经作者冯越新授权发布。