从 Linux 到 Android 线程调度

起因

最近我们的 APM 上线了应用卡顿的性能检测,我们使用的是和 BlockCanary 同样的方案,通过 Looper Printer 去监控应用的卡顿。在收集到线上数据以后,发现一个比较怪异的现象,大量的卡顿的情况下,当前执行线程(主线程)的执行时间其实并不长,主线程只执行了几毫秒,但是却卡顿1s甚至更长的时间。很明显这个时候是由于主线程没有抢占到CPU导致,为了搞清楚为什么主线程没有抢到CPU,我把 Android 线程调度仔细撸了一遍。

Linux 进程与Android 线程

基础知识

进程是资源管理的最小单位,线程是程序执行的最小单位。在操作系统设计上,从进程演化出线程,最主要的目的就是更好的支持SMP以及减小(进程/线程)上下文切换开销。

无论按照怎样的分法,一个进程至少需要一个线程作为它的指令执行体,进程管理着资源(比如cpu、内存、文件等等),而将线程分配到某个cpu上执行。一个进程当然可以拥有多个线程,此时,如果进程运行在SMP机器上,它就可以同时使用多个cpu来执行各个线程,达到最大程度的并行,以提高效率;同时,即使是在单cpu的机器上,采用多线程模型来设计程序,正如当年采用多进程模型代替单进程模型一样,使设计更简洁、功能更完备,程序的执行效率也更高,例如采用多个线程响应多个输入,而此时多线程模型所实现的功能实际上也可以用多进程模型来实现,而与后者相比,线程的上下文切换开销就比进程要小多了,从语义上来说,同时响应多个输入这样的功能,实际上就是共享了除cpu以外的所有资源的。

针对线程模型的两大意义,分别开发出了核心级线程和用户级线程两种线程模型,分类的标准主要是线程的调度者在核内还是在核外。前者更利于并发使用多处理器的资源,而后者则更多考虑的是上下文切换开销。

内核线程与用户线程

需要理解 Linux 进程与 Android 线程的关系,需要先解释清楚 Linux 中内核线程、用户线程的关系,在 内核线程、轻量级进程、用户线程的区别和联系 中有比较清晰的阐述。可以总结为几点:

  • 内核线程只运行在内核态,不受用户态上下文的拖累。
  • 用户线程是完全建立在用户空间的线程库,用户线程的创建、调度、同步和销毁全由库函数在用户空间完成,不需要内核的帮助。因此这种线程是极其低消耗和高效的。
  • 轻量级进程(LWP)是建立在内核之上并由内核支持的用户线程,它是内核线程的高度抽象,每一个轻量级进程都与一个特定的内核线程关联。内核线程只能由内核管理并像普通进程一样被调度。
  • LinuxThreads 是用户空间的线程库,所采用的是线程-进程 1对1 模型(即一个用户线程对应一个轻量级进程,而一个轻量级进程对应一个特定 的内核线程),将线程的调度等同于进程的调度,调度交由内核完成,而线程的创建、同步、销毁由核外线程库完成(LinuxThtreads已绑定到 GLIBC中发行)。

PS: Linux 在2.6之前使用的是 LinuxThreads 线程库,2.6之后是NPTL(Native Posix Thread Library),NPTL 使用的也是1对1的结构,但是在信号处理,线程同步,存储管理等多方面进行了优化。

此外,Linux 内核不存在真正意义上的线程。Linux 将所有的执行实体都称之为任务(task),每一个任务在 Linux 上都类似于一个单线程的进程,具有内存空间、执行实体、文件资源等。但是,Linux 下不同任务之间可以选择公用内存空间,因而在实际意义上,共享同一个内存空间的多个任务构成了一个进程,而这些任务就成为这个进程里面的线程。

比如在 Android 上我们通过 adb shell进入手机后,可以通过 ps 命令查看某个应用下的所有线程,先通过 ps | grep $包名找到对应进程的进程号,然后执行 ps -t -p -P 6493

同时我们可以,执行 ls /proc/6493/tasks 查看该进程下的所有 tasks,他们之间有完整的对应关系:

PS: 查看 /proc/6493/tasks 需要 root 权限

Linux 进程调度

现在的操作系统都是多任务的,为了能让更多的任务能同时在系统上更好的运行,需要一个管理程序来管理计算机上同时运行的各个任务(也就是进程)。这个管理程序就是调度程序,它的功能说起来很简单:

  • 决定哪些进程运行,哪些进程等待
  • 决定每个进程运行多长时间

此外,为了获得更好的用户体验,运行中的进程还可以立即被其他更紧急的进程打断。总之,调度是一个平衡的过程。一方面,它要保证各个运行的进程能够最大限度的使用CPU(即尽量少的切换进程,进程切换过多,CPU的时间会浪费在切换上);另一方面,保证各个进程能公平的使用CPU(即防止一个进程长时间独占CPU的情况)。

Linux 进程优先级

进程提供了两种优先级,一种是普通的进程优先级,第二个是实时优先级。前者适用 SCHED_NORMAL 调度策略,后者可选 SCHED_FIFOSCHED_RR 调度策略。任何时候,实时进程的优先级都高于普通进程,实时进程只会被更高级的实时进程抢占,同级实时进程之间是按照 FIFO(一次机会做完)或者 RR(多次轮转)规则调度的。

普通进程和实时进程分别用 nice 值和实时优先级(RTPRI)来度量优先级。

nice 值

Linux 中,使用 nice 值来设定一个普通进程的优先级,系统任务调度器根据 nice 值合理安排调度。

  • nice 的取值范围为 -20 到 19。
  • nice 的值越大,进程的优先级就越低,获得 CPU 调用的机会越少,nice值越小,进程的优先级则越高,获得 CPU 调用的机会越多。
  • 一个 nice 值为 -20 的进程优先级最高,nice 值为 19 的进程优先级最低。
  • 父进程 fork 出来的子进程 nice 值与父进程相同。父进程 renice,子进程 nice 值不会随之改变。

实时优先级

  • 实时优先级的范围是 0~99。
  • 与 nice 值的定义相反,实时优先级是值越大优先级越高。
  • 实时进程都是一些对响应时间要求比较高的进程,因此系统中有实时优先级高的进程处于运行队列的话,它们会抢占一般的进程的运行时间。

进程优先级

Linux 进程优先级与 nice 值及实时进程优先级的关系:

通过 ps -p 可以看到这几个值之间的对应关系:

动态优先级

除此之外,在执行阶段,调度程序通过增加或减少进程静态优先级的值,来达到奖励IO消耗型或惩罚cpu消耗型的进程,调整后的进程称为动态优先级。与之对应的我们前面提到的优先级的值被称为静态优先级。

调度原理

优先级,可以决定谁先运行了。但是对于调度程序来说,并不是运行一次就结束了,还必须知道间隔多久进行下次调度。于是就有了时间片的概念。时间片是一个数值,表示一个进程被抢占前能持续运行的时间。也可以认为是进程在下次调度发生前运行的时间(除非进程主动放弃CPU,或者有实时进程来抢占CPU)。时间片的大小设置并不简单,设大了,系统响应变慢(调度周期长);设小了,进程频繁切换带来的处理器消耗。默认的时间片一般是10ms。

举个例子说明调度原理的实现1

假设系统中只有3个进程ProcessA(NI=+10),ProcessB(NI=0),ProcessC(NI=-10),NI表示进程的nice值,时间片=10ms:

  • 调度前,把进程优先级按一定的权重映射成时间片(这里假设优先级高一级相当于多5msCPU时间)。假设ProcessA分配了一个时间片10ms,那么ProcessB的优先级比ProcessA高10(nice值越小优先级越高),ProcessB应该分配105+10=60ms,以此类推,ProcessC分配205+10=110ms。
  • 开始调度时,优先调度分配CPU时间多的进程。由于ProcessA(10ms),ProcessB(60ms),ProcessC(110ms)。显然先调度ProcessC。
  • 10ms(一个时间片)后,再次调度时,ProcessA(10ms), ProcessB(60ms), ProcessC(100ms)。 ProcessC刚运行了10ms,所以变成100ms。此时仍然先调度ProcessC。
  • 再调度4次后(4个时间片),ProcessA(10ms), ProcessB(60ms),ProcessC(60ms)。此时ProcessB和ProcessC的CPU时间一样,这时得看ProcessB和ProcessC谁在CPU运行队列的前面,假设ProcessB在前面,则调度ProcessB
  • 10ms(一个时间片)后,ProcessA(10ms), ProcessB(50ms), ProcessC(60ms)。再次调度ProcessC
  • ProcessB 和 ProcessC 交替运行,直至 ProcessA(10ms), ProcessB(10ms), ProcessC(10ms)。
    这时得看ProcessA,ProcessB,ProcessC谁在CPU运行队列的前面就先调度谁。这里假设调度ProcessA
  • 10ms (一个时间片)后,ProcessA (时间片用完后退出), ProcessB (10ms), ProcessC (10ms)。
  • 再过2个时间片,ProcessB 和 ProcessC 也运行完退出。

这个例子很简单,主要是为了说明调度的原理,实际的调度算法虽然不会这么简单,

进程调度算法 CFS

Linux上的调度算法是不断发展的,在2.6.23内核以后,采用了“完全公平调度算法”,简称CFS。

进程调度

CFS算法的初衷就是让所有进程同时运行在一个CPU上,例如两个进程都需要运行10ms的时间,则CFS算法下,连个进程同时运行在CPU上,且时间为20ms,而不是每个进程分别运行10ms。但是这只是一种理想的运行方式,CFS为了近似这种运行算法,就提出了虚拟运行时间(vruntime)的概念。vruntime记录了一个可执行进程到当前时刻为止执行的总时间(需要以进程总数n进行归一化,并且根据进程的优先级进行加权)。根据vruntime的定义可以知道,vruntime越大,说明该进程运行的越久,所以被调度的可能性就越小。所以我们的调度算法就是每次选择 vruntime 值最小的进程进行调度,内核中使用红黑树可以方便的得到 vruntime 值最小的进程。至于每个进程如何更新自己的 vruntime ?内核中是按照如下方式来更新的: vruntime += delta * NICE_0_LOAD/ se.weight;其中:
NICE_0_LOAD 是个定值,及系统默认的进程的权值;se.weight是当前进程的权重(优先级越高,权重越大);
delta 是当前进程运行的时间;我们可以得出这么个关系:vruntime 与delta 成正比,即当前运行时间越长 vruntime 增长越快
vruntime 与 se.weight 成反比,即权重越大 vunruntime 增长越慢。简单来说,一个进程的优先级越高,而且该进程运行的时间越少,则该进程的 vruntime 就越小,该进程被调度的可能性就越高。

运行时长

CFS 的运行时间是有当前系统中所有可调度进程的优先级的比重来确定的,假如现在进程中有三个可调度进程A、B、C,它们的优先级分别为5,10,15,则它们的时间片分别为5/30,10/30,15/30。而不是由自己的时间片计算得来的,这样的话,优先级为1,2的两个进程与优先级为50,100的两个进程分的时间片是相同的。简单来说,CFS采用的所有进程优先级的比重来计算每个进程的时间片的,是相对的而不是绝对的。

Linux 进程组与 Cgroups

Cgroups是control groups的缩写,是Linux内核提供的一种可以限制、记录、隔离进程组(process groups)所使用的物理资源(如:cpu,memory,IO等等)的机制。最初由google的工程师提出,后来被整合进Linux内核。也是目前轻量级虚拟化技术 lxc (linux container)的基础之一。

Cgroups最初的目标是为资源管理提供的一个统一的框架,既整合现有的cpuset等子系统,也为未来开发新的子系统提供接口。现在的cgroups适用于多种应用场景,从单个进程的资源控制,到实现操作系统层次的虚拟化(OS Level Virtualization)。Cgroups提供了以下功能:

  • 限制进程组可以使用的资源数量(Resource limiting )。比如:memory子系统可以为进程组设定一个memory使用上限,一旦进程组使用的内存达到限额再申请内存,就会出发OOM(out of memory)。
  • 进程组的优先级控制(Prioritization )。比如:可以使用cpu子系统为某个进程组分配特定cpu share。
  • 记录进程组使用的资源数量(Accounting )。比如:可以使用cpuacct子系统记录某个进程组使用的cpu时间
  • 进程组隔离(Isolation)。比如:使用ns子系统可以使不同的进程组使用不同的namespace,以达到隔离的目的,不同的进程组有各自的进程、网络、文件系统挂载空间。
  • 进程组控制(Control)。比如:使用freezer子系统可以将进程组挂起和恢复。

相关概念

  • 任务(task)。在 cgroups 中,任务就是系统的一个进程。
  • 控制族群(control group)。控制族群就是一组按照某种标准划分的进程。Cgroups 中的资源控制都是以控制族群为单位实现。一个进程可以加入到某个控制族群,也从一个进程组迁移到另一个控制族群。一个进程组的进程可以使用 cgroups 以控制族群为单位分配的资源,同时受到 cgroups 以控制族群为单位设定的限制。
  • 层级(hierarchy)。控制族群可以组织成 hierarchical 的形式,既一颗控制族群树。控制族群树上的子节点控制族群是父节点控制族群的孩子,继承父控制族群的特定的属性。
  • 子系统(subsytem)。一个子系统就是一个资源控制器,比如 cpu 子系统就是控制 cpu 时间分配的一个控制器。子系统必须附加(attach)到一个层级上才能起作用,一个子系统附加到某个层级以后,这个层级上的所有控制族群都受到这个子系统的控制。

cgroups 在 Android 中的应用

Android中关于 cpu/cpuset/schedtune 三个子系统的应用都是基于进程优先级的。AMS(ActivityManagerService) 和 PMS(PackageManagerService) 等通过 Process 设置进程优先级、调度策略等;android/osProcess JNI通过调用libcutils.so/libutils.so执行getpriority/setpriority/sched_setscheduler/sched_getschedler系统调用或者直接操作CGroup文件节点以达到设置优先级,限制进程CPU资源的目的。

  • cpu,这个子系统使用调度程序提供对 CPU 的 cgroup 任务访问,连接在 Android 系统的 /dev/cpuctl 层级结构上。
  • cpuset,这个子系统为 cgroup 中的任务分配独立 CPU(在多核系统)和内存节点,连接在 Android 系统的 /dev/cpuset 层级结构上。
  • schedtune,是ARM/Linaro为了EAS新增的一个子系统,主要用来控制进程调度选择CPU以及boost触发,连接在 Android 系统的 /dev/stune 层级结构上。

Android 中在从设置进程优先级到最后映射到不同 cgroups 下的过程,有兴趣的可以参考 Android中关于cpu/cpuset/schedtune的应用 这篇文章。我们这里以 cpu 子系统为例介绍一下再 CPU 子系统下是如何控制不同 cgroup 对 CPU 资源的访问。

CPU 子系统

CPU 子系统连接的 /dev/cpuctl 层级结构下有两个 cgroup,分别是

  • /,对应到 Android 的前台进程组。
  • /bg_non_interactive,对应到 Android 的后台进程组。

在 cgroup 下定义了一些参数,来控制不同的 cgroup 在使用 cpu 资源时的配置:

  • cpu.shares:保存了整数值,用来设置 cgroup 分组任务获得 CPU 时间的相对值。
  • cpu.rt_runtime_us:主要用来设置 cgroup 获得 CPU 资源的周期,单位为微妙。
  • cpu.rt_period_us:主要是用来设置 cgroup 中的任务可以最长获得 CPU 资源的时间,单位为微秒。

通过下面的数据我们可以看到,前台进程组和后台进程组的 cpu.share 值相比接近于 20:1,也就是说前台进程组中的应用可以利用 95% 的 CPU,而处于后台进程组中的应用则只能获得 5% 的 CPU 利用率。

1
2
3
4
shell@hammerhead:/ $ cat /dev/cpuctl/cpu.shares
1024
shell@hammerhead:/ $ cat /dev/cpuctl/bg_non_interactive/cpu.shares
52

同样我们也可查看 cpu.rt_period_uscpu.rt_runtime_us 的时间对比:

1
2
3
4
shell@hammerhead:/ $ cat /dev/cpuctl/cpu.rt_period_us
1000000
shell@hammerhead:/ $ cat /dev/cpuctl/cpu.rt_runtime_us
800000

即单个逻辑CPU下每一秒内可以获得0.8秒的执行时间。

1
2
3
4
shell@hammerhead:/ $ cat /dev/cpuctl/bg_non_interactive/cpu.rt_period_us
1000000
shell@hammerhead:/ $ cat /dev/cpuctl/bg_non_interactive/cpu.rt_runtime_us
700000

即单个逻辑CPU下每一秒内可以获得0.7秒的执行时间。

PS: 最长的获取CPU资源时间取决于逻辑CPU的数量。比如 cpu.rt_runtime_us 设置为200000(0.2秒), cpu.rt_period_us 设置为1000000(1秒)。在单个逻辑CPU上的获得时间为每秒为0.2秒。 2个逻辑CPU,获得的时间则是0.4秒。

SchedPolicy

Android 底层对进程分组的操作最后是通过 sched_policy.c 文件中的 set_sched_policy(int tid, SchedPolicy policy)set_cpuset_policy(int tid, SchedPolicy policy) 函数添加到对应的进程组的,调用这两个函数的传递的 SchedPolicy 定义在 sched_policy.h 中,定义不同的调度策略:

1
2
3
4
5
6
7
8
9
10
11
12
13
/* Keep in sync with THREAD_GROUP_* in frameworks/base/core/java/android/os/Process.java */
typedef enum {
SP_DEFAULT = -1,
SP_BACKGROUND = 0,
SP_FOREGROUND = 1,
SP_SYSTEM = 2, // can't be used with set_sched_policy()
SP_AUDIO_APP = 3,
SP_AUDIO_SYS = 4,
SP_TOP_APP = 5,
SP_CNT,
SP_MAX = SP_CNT - 1,
SP_SYSTEM_DEFAULT = SP_FOREGROUND,
} SchedPolicy;

set_sched_policy 中根据不同的 SchedPolicy 为进程找到不同的进程组,并添加进去。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
// 根据不同调度策略选择不同的进程组
int fd = -1;
int boost_fd = -1;
switch (policy) {
case SP_BACKGROUND:
fd = bg_cgroup_fd;
boost_fd = bg_schedboost_fd;
break;
case SP_FOREGROUND:
case SP_AUDIO_APP:
case SP_AUDIO_SYS:
fd = fg_cgroup_fd;
boost_fd = fg_schedboost_fd;
break;
case SP_TOP_APP:
fd = fg_cgroup_fd;
boost_fd = ta_schedboost_fd;
break;
default:
fd = -1;
boost_fd = -1;
break;
}
// 添加到对应的进程组
if (add_tid_to_cgroup(tid, fd) != 0) {
if (errno != ESRCH && errno != ENOENT)
return -errno;
}

set_cpuset_policy 也有类似的逻辑,这里就不重复列举了,有兴趣的可以去看看源码。

在初始化方法中,可以看到对应不同的进程组和映射到不同的 cgroups 层级架构:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
static void __initialize(void) {
char* filename;
if (!access("/dev/cpuctl/tasks", F_OK)) {
__sys_supports_schedgroups = 1;
filename = "/dev/cpuctl/tasks";
fg_cgroup_fd = open(filename, O_WRONLY | O_CLOEXEC);
if (fg_cgroup_fd < 0) {
SLOGE("open of %s failed: %s
", filename, strerror(errno));
}
filename = "/dev/cpuctl/bg_non_interactive/tasks";
bg_cgroup_fd = open(filename, O_WRONLY | O_CLOEXEC);
if (bg_cgroup_fd < 0) {
SLOGE("open of %s failed: %s
", filename, strerror(errno));
}
} else {
__sys_supports_schedgroups = 0;
}
#ifdef USE_CPUSETS
if (!access("/dev/cpuset/tasks", F_OK)) {
filename = "/dev/cpuset/foreground/tasks";
fg_cpuset_fd = open(filename, O_WRONLY | O_CLOEXEC);
filename = "/dev/cpuset/background/tasks";
bg_cpuset_fd = open(filename, O_WRONLY | O_CLOEXEC);
filename = "/dev/cpuset/system-background/tasks";
system_bg_cpuset_fd = open(filename, O_WRONLY | O_CLOEXEC);
filename = "/dev/cpuset/top-app/tasks";
ta_cpuset_fd = open(filename, O_WRONLY | O_CLOEXEC);
#ifdef USE_SCHEDBOOST
filename = "/dev/stune/top-app/tasks";
ta_schedboost_fd = open(filename, O_WRONLY | O_CLOEXEC);
filename = "/dev/stune/foreground/tasks";
fg_schedboost_fd = open(filename, O_WRONLY | O_CLOEXEC);
filename = "/dev/stune/background/tasks";
bg_schedboost_fd = open(filename, O_WRONLY | O_CLOEXEC);
#endif
}
#endif
}

再回到上面 SchedPolicy 的定义,可以看到 Keep in sync with THREAD_GROUP_* in frameworks/base/core/java/android/os/Process.java 这样的一句注释,看一眼这里 Process.java 对线程组的定义:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
/**
* Default thread group -
* has meaning with setProcessGroup() only, cannot be used with setThreadGroup().
* When used with setProcessGroup(), the group of each thread in the process
* is conditionally changed based on that thread's current priority, as follows:
* threads with priority numerically less than THREAD_PRIORITY_BACKGROUND
* are moved to foreground thread group. All other threads are left unchanged.
*/
public static final int THREAD_GROUP_DEFAULT = -1;
/**
* Background thread group - All threads in
* this group are scheduled with a reduced share of the CPU.
* Value is same as constant SP_BACKGROUND of enum SchedPolicy.
* FIXME rename to THREAD_GROUP_BACKGROUND.
*/
public static final int THREAD_GROUP_BG_NONINTERACTIVE = 0;
/**
* Foreground thread group - All threads in
* this group are scheduled with a normal share of the CPU.
* Value is same as constant SP_FOREGROUND of enum SchedPolicy.
* Not used at this level.
**/
private static final int THREAD_GROUP_FOREGROUND = 1;
/**
* System thread group.
**/
public static final int THREAD_GROUP_SYSTEM = 2;
/**
* Application audio thread group.
**/
public static final int THREAD_GROUP_AUDIO_APP = 3;
/**
* System audio thread group.
**/
public static final int THREAD_GROUP_AUDIO_SYS = 4;
/**
* Thread group for top foreground app.
**/
public static final int THREAD_GROUP_TOP_APP = 5;

可以看到两组定义之间明确的对应关系:

Process 进程组 SchedPolicy 进程组
THREAD_GROUP_DEFAULT SP_DEFAULT
THREAD_GROUP_BG_NONINTERACTIVE SP_BACKGROUND
THREAD_GROUP_FOREGROUND SP_FOREGROUND
THREAD_GROUP_SYSTEM SP_SYSTEM
THREAD_GROUP_AUDIO_APP SP_AUDIO_APP
THREAD_GROUP_AUDIO_SYS SP_AUDIO_SYS
THREAD_GROUP_TOP_APP SP_TOP_APP

至于这里的对应关系是怎么传递对接上的,会在后面进行解释。

Android 的线程调度

Android 进程生命周期与ADJ

Android 开发者应该都知道在系统中进程重要性的划分:

  • 前台进程(Foreground process)
  • 可见进程(Visible process)
  • 服务进程(Service process)
  • 后台进程(Background process)
  • 空进程(Empty process)

相信大家都很清楚,这里就不做过多的介绍了,不过对于进程重要性是通过哪些操作发生变更的,以及和我们前面讲的 Linux 进程分组又是怎么关联和映射上的,是下面要讲述的重点。

Android 进程优先级的相关概念

oom_score_adj 级别

对于每一个运行中的进程,Linux 内核都通过 proc 文件系统暴露 /proc/[pid]/oom_score_adj 这样一个文件来允许其他程序修改指定进程的优先级,这个文件允许的值的范围是:-1000 ~ +1001之间。值越小,表示进程越重要。当内存非常紧张时,系统便会遍历所有进程,以确定哪个进程需要被杀死以回收内存,此时便会读取 oom_score_adj 这个文件的值。

PS:在Linux 2.6.36之前的版本中,Linux 提供调整优先级的文件是 /proc/[pid]/oom_adj 。这个文件允许的值的范围是-17 ~ +15之间。数值越小表示进程越重要。 这个文件在新版的 Linux 中已经废弃。但你仍然可以使用这个文件,当你修改这个文件的时候,内核会直接进行换算,将结果反映到 oom_score_adj 这个文件上。
Android早期版本的实现中也是依赖 oom_adj 这个文件。但是在新版本中,已经切换到使用 oom_score_adj 这个文件。

为了便于管理,ProcessList.java中预定义了 oom_score_adj 的可能取值,这里的预定义值也是对应用进程的一种分类。

Lowmemorykiller 根据当前可用内存情况来进行进程释放,总设计了6个级别,即上表中“解释列”加粗的行,即 Lowmemorykiller 的杀进程的6档,如下:

  • CACHED_APP_MAX_ADJ
  • CACHED_APP_MIN_ADJ
  • BACKUP_APP_ADJ
  • PERCEPTIBLE_APP_ADJ
  • VISIBLE_APP_ADJ
  • FOREGROUND_APP_ADJ

系统内存从很宽裕到不足,Lowmemorykiller 也会相应地从 CACHED_APP_MAX_ADJ (第1档)开始杀进程,如果内存还不足,那么会杀 CACHED_APP_MIN_ADJ (第2档),不断深入,直到满足内存阈值条件。

ProcessRecord中下面这些属性反应了 oom_score_adj 的值:

1
2
3
4
5
6
int maxAdj; // Maximum OOM adjustment for this process
int curRawAdj; // Current OOM unlimited adjustment for this process
int setRawAdj; // Last set OOM unlimited adjustment for this process
int curAdj; // Current OOM adjustment for this process
int setAdj; // Last set OOM adjustment for this process
int verifiedAdj; // The last adjustment that was verified as actually being set

Process State

对应的在 ActivityManager 重定义了 process_state 级别的划分,Android 系统会在修改进程状态的同时更新 oom_score_adj 的分级:

在 ProcessRecord 中,记录了和进程状态相关的属性:

1
2
3
4
int curProcState = PROCESS_STATE_NONEXISTENT; // Currently computed process state
int repProcState = PROCESS_STATE_NONEXISTENT; // Last reported process state
int setProcState = PROCESS_STATE_NONEXISTENT; // Last set process state in process tracker
int pssProcState = PROCESS_STATE_NONEXISTENT; // Currently requesting pss for

Schedule Group

对应到底层进程分组,除了上面提到的 Process.java 定义的不同线程组的定义,同时还为 Activity manager 定义了一套类似的调度分组,和之前的线程分组定义也存在对应关系:

1
2
3
4
5
6
7
8
9
// Activity manager's version of Process.THREAD_GROUP_BG_NONINTERACTIVE
static final int SCHED_GROUP_BACKGROUND = 0;
// Activity manager's version of Process.THREAD_GROUP_DEFAULT
static final int SCHED_GROUP_DEFAULT = 1;
// Activity manager's version of Process.THREAD_GROUP_TOP_APP
static final int SCHED_GROUP_TOP_APP = 2;
// Activity manager's version of Process.THREAD_GROUP_TOP_APP
// Disambiguate between actual top app and processes bound to the top app
static final int SCHED_GROUP_TOP_APP_BOUND = 3;

在 ProcessRecord 中,也记录了和调度组相关的属性:

1
2
int curSchedGroup; // Currently desired scheduling class
int setSchedGroup; // Last set to background scheduling class

Android 进程优先级的变化

我们知道影响 Android 应用进程优先级变化的是根据 Android
应用组件的生命周期变化相关。Android进程调度之adj算法 里面罗列了所有会触发进程状态发生变化的事件,主要包括:

Actvity

  • ActivityStackSupervisor.realStartActivityLocked: 启动Activity
  • ActivityStack.resumeTopActivityInnerLocked: 恢复栈顶Activity
  • ActivityStack.finishCurrentActivityLocked: 结束当前Activity
  • ActivityStack.destroyActivityLocked: 摧毁当前Activity

Service

位于ActiveServices.java

  • realStartServiceLocked: 启动服务
  • bindServiceLocked: 绑定服务(只更新当前app)
  • unbindServiceLocked: 解绑服务 (只更新当前app)
  • bringDownServiceLocked: 结束服务 (只更新当前app)
  • sendServiceArgsLocked: 在bringup或则cleanup服务过程调用 (只更新当前app)

broadcast

  • BQ.processNextBroadcast: 处理下一个广播
  • BQ.processCurBroadcastLocked: 处理当前广播
  • BQ.deliverToRegisteredReceiverLocked: 分发已注册的广播 (只更新当前app)

ContentProvider

  • AMS.removeContentProvider: 移除provider
  • AMS.publishContentProviders: 发布provider (只更新当前app)
  • AMS.getContentProviderImpl: 获取provider (只更新当前app)

Process

位于 ActivityManagerService.java

  • setSystemProcess: 创建并设置系统进程
  • addAppLocked: 创建persistent进程
  • attachApplicationLocked: 进程创建后attach到system_server的过程;
  • trimApplications: 清除没有使用app
  • appDiedLocked: 进程死亡
  • killAllBackgroundProcesses: 杀死所有后台进程.即(ADJ>9或removed=true的普通进程)
  • killPackageProcessesLocked: 以包名的形式 杀掉相关进程;

这些事件都会直接或间接调用到 ActivityManagerService.java 中的 updateOomAdjLocked 方法来更新进程的优先级,updateOomAdjLocked 先通过 computeOomAdjLocked 方法负责计算进程的优先级,再通过调用 applyOomAdjLocked 应用进程的优先级。

computeOomAdjLocked

computeOomAdjLocked 方法负责计算进程的优先级,总计约700行,执行流程比较清晰,步骤如下,由于代码有点多这里就不贴了,想仔细研究的可以比着系统源码看:

空进程判断

空进程中没有任何组件,因此主线程也为null(ProcessRecord.thread描述了应用进程的主线程)。

如果是空进程,则不需要再做后面的计算了。curSchedGroup 直接设置为
ProcessList.SCHED_GROUP_BACKGROUND 进程调度组即可。

app.maxAdj <= ProcessList.FOREGROUND_APP_ADJ 的情况

系统进程或者Persistent进程会通过设置maxAdj来保持其较高的优先级,对于这类进程不用按照普通进程的算法进行计算,直接按照maxAdj的值设置即可,curSchedGroup 设置为THREAD_GROUP_DEFAULT 进程调度组。

是否有前台优先级
Case schedGroup adj procState
当app是当前展示的app SCHED_GROUP_TOP_APP FOREGROUND_APP_ADJ PROCESS_STATE_CUR_TOP
当instrumentation不为空时 SCHED_GROUP_TOP_APP FOREGROUND_APP_ADJ PROCESS_STATE_FOREGROUND_SERVICE
当进程存在正在接收的broadcastrecevier 是否在前台广播组 ? SCHED_GROUP_DEFAULT : SCHED_GROUP_BACKGROUND FOREGROUND_APP_ADJ PROCESS_STATE_RECEIVER
当进程存在正在执行的service 是否前台服务 ? SCHED_GROUP_DEFAULT : SCHED_GROUP_BACKGROUND FOREGROUND_APP_ADJ PROCESS_STATE_SERVICE
以上条件都不符合 SCHED_GROUP_BACKGROUND adj=cachedAdj(>=FOREGROUND_APP_ADJ) PROCESS_STATE_CACHED_EMPTY

PS:Instrumentation 应用是辅助测试用的,正常运行的系统中不用考虑这种应用。

非前台 Activity

遍历进程中的所有Activity,找出其中优先级最高的设置为进程的优先级。

Case schedGroup adj procState
activity可见 SCHED_GROUP_DEFAULT <=VISIBLE_APP_ADJ <=PROCESS_STATE_CUR_TOP
activity正在 pausing 或者已经 pause SCHED_GROUP_DEFAULT <=PERCEPTIBLE_APP_ADJ <=PROCESS_STATE_CUR_TOP
activity正在 stoping - <=PERCEPTIBLE_APP_ADJ <=PROCESS_STATE_LAST_ACTIVITY
以上都不满足 - - <=PROCESS_STATE_CACHED_ACTIVITY
是否有前台服务

通过 startForeground 启动的 Service 被认为是前台 Service。

Case schedGroup adj procState
存在前台service SCHED_GROUP_DEFAULT PERCEPTIBLE_APP_ADJ PROCESS_STATE_FOREGROUND_SERVICE
存在 Overlay UI SCHED_GROUP_DEFAULT PERCEPTIBLE_APP_ADJ PROCESS_STATE_IMPORTANT_FOREGROUND
强制前台 SCHED_GROUP_DEFAULT PERCEPTIBLE_APP_ADJ PROCESS_STATE_TRANSIENT_BACKGROUND
是否特殊进程

特殊类型的进程包括:重量级进程,桌面进程,前一个应用进程,正在执行备份的进程。

  • 重量级进程是指那些通过Manifest指明不能保存状态的应用进程;
  • 桌面进程是指 Android 上的 Launcher;
  • “前一个应用”是指:在启动新的Activity时,如果新启动的Activity是属于一个新的进程的,那么当前即将被stop的Activity所在的进程便会成为“前一个应用”进程;
  • 备份进程,进程是否正在进行备份。
Case schedGroup adj procState
重量级进程 >=SCHED_GROUP_BACKGROUND <=HEAVY_WEIGHT_APP_ADJ <=PROCESS_STATE_HEAVY_WEIGHT
桌面进程 >=SCHED_GROUP_BACKGROUND <=HOME_APP_ADJ <=PROCESS_STATE_HOME
“前一个应用”进程 >=SCHED_GROUP_BACKGROUND <=PREVIOUS_APP_ADJ <=PROCESS_STATE_LAST_ACTIVITY
备份进程 - <=BACKUP_APP_ADJ <=PROCESS_STATE_BACKUP
所有 Service 及连接的 Client

在当前进程满足 adj > ProcessList.FOREGROUND_APP_ADJ || schedGroup == ProcessList.SCHED_GROUP_BACKGROUND || procState > ActivityManager.PROCESS_STATE_TOP 的状态下遍历所有的Service,并且还需要遍历每一个Service的所有连接。然后根据连接的关系确认客户端进程的优先级来确定当前进程的优先级。

这里详细记录了在 bindService 过程中,传递的不同的 FLAG 对于 Service 进程和 Client 进程关联计算 adj 级别。由于涉及的分支判断较多,如果想要仔细研究,最好对着代码一一查看。这里只介绍整个过程中涉及到进程调度组发生的变化:

  • client.curSchedGroup > schedGroup

在某个 client 进行 bindService 过程中,没有设置:

FLAG
BIND_WAIVE_PRIORITY
BIND_NOT_FOREGROUND
BIND_IMPORTANT_BACKGROUND

且 client 的 curSchedGroup 大于当前进程的 schedGroup,则需要重新设置当前进程的调度策略;此时,如果有设置 BIND_IMPORTANT 这个 flag,则赋值 client.curSchedGroupschedGroup,否则则将 schedGroup 设置为 SCHED_GROUP_DEFAULT

1
2
3
4
5
6
7
8
9
10
11
12
13
if ((cr.flags&Context.BIND_WAIVE_PRIORITY) == 0) {
...
if ((cr.flags&Context.BIND_NOT_FOREGROUND) == 0) {
// This will treat important bound services identically to
// the top app, which may behave differently than generic
// foreground work.
if (client.curSchedGroup > schedGroup) {
if ((cr.flags&Context.BIND_IMPORTANT) != 0) {
schedGroup = client.curSchedGroup;
} else {
schedGroup = ProcessList.SCHED_GROUP_DEFAULT;
}
}
  • BIND_ADJUST_WITH_ACTIVITY

如果 client 有设置 Context.BIND_ADJUST_WITH_ACTIVITY 且 client 进程存在前台 Activity 并且 adj > ProcessList.FOREGROUND_APP_ADJ,则根据 client 是否设置 BIND_IMPORTANT flag 来分别设置当前进程的调度策略。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
if ((cr.flags&Context.BIND_WAIVE_PRIORITY) == 0) {
...
if ((cr.flags&Context.BIND_ADJUST_WITH_ACTIVITY) != 0) {
// client 进程存在前台 Activity 并且 adj > ProcessList.FOREGROUND_APP_ADJ
if (a != null && adj > ProcessList.FOREGROUND_APP_ADJ &&
(a.visible || a.state == ActivityState.RESUMED ||
a.state == ActivityState.PAUSING)) {
adj = ProcessList.FOREGROUND_APP_ADJ;
if ((cr.flags&Context.BIND_NOT_FOREGROUND) == 0) {
if ((cr.flags&Context.BIND_IMPORTANT) != 0) {
schedGroup = ProcessList.SCHED_GROUP_TOP_APP_BOUND;
} else {
schedGroup = ProcessList.SCHED_GROUP_DEFAULT;
}
}
app.cached = false;
app.adjType = "service";
app.adjTypeCode = ActivityManager.RunningAppProcessInfo
.REASON_SERVICE_IN_USE;
app.adjSource = a;
app.adjSourceProcState = procState;
app.adjTarget = s.name;
if (DEBUG_OOM_ADJ_REASON) Slog.d(TAG, "Raise to service w/activity: "
+ app);
}
}

关于整计算过程,可以参考 Android进程调度之adj算法 里面的总结,不过根据不同的系统版本,会有稍许差异:

  • 当 service 已启动,则 procState<= PROCESS_STATE_SERVICE
    • 当 service 在 30 分钟内活动过,则adj= SERVICE_ADJ,cached=false;
  • 获取service所绑定的connections
    • 当client与当前app同一个进程,则continue;
    • 当client进程的 ProcState >= PROCESS_STATE_CACHED_ACTIVITY,则设置为空进程
    • 当进程存在显示的 ui,则将当前进程的 adj 和 ProcState 值赋予给 client 进程
    • 当不存在显示的 ui,且 service 上次活动时间距离现在超过30分钟,则只将当前进程的 adj 值赋予给 client 进程
    • 当前进程 adj > client进程adj的情况
      • 当 service 进程比较重要时,则设置adj >= PERSISTENT_SERVICE_ADJ
      • 当 client 进程 adj < PERCEPTIBLE_APP_ADJ,且当前进程 adj > PERCEPTIBLE_APP_ADJ 时,则设置 adj = PERCEPTIBLE;
      • 当 client 进程 adj >= PERCEPTIBLE_APP_ADJ 时,则设置 adj = clientAdj
      • 否则,设置 adj >= VISIBLE_APP_ADJ
      • 若 client 进程不是 cache 进程,则当前进程也设置为非cache进程
    • 当绑定的是前台进程的情况
      • 当 client 进程状态为前台时,则设置 mayBeTop = true,并设置 client 进程 procState = PROCESS_STATE_CACHED_EMPTY
      • 当 client 进程状态 < PROCESS_STATE_TOP 的前提下:若绑定前台 service,则 clientProcState = PROCESS_STATE_BOUND_FOREGROUND_SERVICE;否则clientProcState = PROCESS_STATE_IMPORTANT_FOREGROUND
    • 当connections并没有绑定前台service时,则clientProcState >= PROCESS_STATE_IMPORTANT_BACKGROUND
    • 保证当前进程procState不会必client进程的procState大
  • 当进程adj > FOREGROUND_APP_ADJ,且 client 进程 activity 可见 或者resumed 或 正在暂停,则设置adj = FOREGROUND_APP_ADJ
所有 ContentProvider 及连接的 client

ContentProvider 的遍历和 Service 的遍历是类似的,在满足 (adj > ProcessList.FOREGROUND_APP_ADJ || schedGroup == Process.THREAD_GROUP_BG_NONINTERACTIVE || procState > ActivityManager.PROCESS_STATE_TOP) 的条件下进行两次循环遍,其中涉及到进程调度组发生变更的情况:

  • client.curSchedGroup > schedGroup
1
2
3
if (client.curSchedGroup > schedGroup) {
schedGroup = ProcessList.SCHED_GROUP_DEFAULT;
}
  • provider 存在外部非framework 的进程依赖
1
2
3
4
5
6
7
8
9
10
11
12
13
if (cpr.hasExternalProcessHandles()) {
if (adj > ProcessList.FOREGROUND_APP_ADJ) {
adj = ProcessList.FOREGROUND_APP_ADJ;
schedGroup = ProcessList.SCHED_GROUP_DEFAULT;
app.cached = false;
app.adjType = "ext-provider";
app.adjTarget = cpr.name;
if (DEBUG_OOM_ADJ_REASON) Slog.d(TAG, "Raise to external provider: " + app);
}
if (procState > ActivityManager.PROCESS_STATE_IMPORTANT_FOREGROUND) {
procState = ActivityManager.PROCESS_STATE_IMPORTANT_FOREGROUND;
}
}
  • 20s之内有人使用过当前进程的 ContentProvider
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
if (app.lastProviderTime > 0 &&
(app.lastProviderTime+mConstants.CONTENT_PROVIDER_RETAIN_TIME) > now) {
if (adj > ProcessList.PREVIOUS_APP_ADJ) {
adj = ProcessList.PREVIOUS_APP_ADJ;
schedGroup = ProcessList.SCHED_GROUP_BACKGROUND;
app.cached = false;
app.adjType = "recent-provider";
if (DEBUG_OOM_ADJ_REASON) Slog.d(TAG, "Raise to recent provider: " + app);
}
if (procState > ActivityManager.PROCESS_STATE_LAST_ACTIVITY) {
procState = ActivityManager.PROCESS_STATE_LAST_ACTIVITY;
app.adjType = "recent-provider";
if (DEBUG_OOM_ADJ_REASON) Slog.d(TAG, "Raise to recent provider: " + app);
}
}

完整的 adj 的计算过程,依然请参考 Android进程调度之adj算法 或者源码:

  • 当client与当前app同一个进程,则continue;
  • 当client进程procState >= PROCESS_STATE_CACHED_ACTIVITY,则把client进程设置成procState = PROCESS_STATE_CACHED_EMPTY
  • 没有ui展示,则保证adj >= FOREGROUND_APP_ADJ
  • 当client进程状态= PROCESS_STATE_TOP(前台)时,则设置mayBeTop=true,并设置client进程procState=PROCESS_STATE_CACHED_EMPTY(空进程)
  • 当client进程状态 < PROCESS_STATE_TOP 时,则 clientProcState = PROCESS_STATE_BOUND_FOREGROUND_SERVICE
  • procState 比clientProcState更大时,则取client端的状态值。
  • 当 contentprovider 存在外部进程依赖(非framework)时,则设置adj = FOREGROUND_APP_ADJ, procState = PROCESS_STATE_IMPORTANT_FOREGROUND
  • 20s 之内有人使用过当前进程的 ContentProvider,如果 adj > ProcessList.PREVIOUS_APP_ADJ,adj 设置为 PREVIOUS_APP_ADJ,schedGroup 设置为 SCHED_GROUP_BACKGROUND
保存计算好的值

最后会进行一次 adjapp.maxAdj 的对比,如果 adj > app.maxAdj 并且 app.maxAdj <= ProcessList.PERCEPTIBLE_APP_ADJ 则将 schedGroup 设置为 SCHED_GROUP_DEFAULT, 然后保存之前计算的 adj、schedGroup 和 procState。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
app.curAdj = app.modifyRawOomAdj(adj);
app.curSchedGroup = schedGroup;
app.curProcState = procState;
```
### Android 线程的优先级及变化
Android 线程优先级的变化分为两种,一种是根据上面计算的进程优先级的变化,给 Android 线程带来的变化,另一种是开发者可以在代码中手动改变线程的优先级。
#### 手动设置
##### Java 线程优先级
我们都知道,在利用 Thread 创建线程或者用 ThreadPoolExecutor 创建线程的时候,我们可以为当前设置的线程设置优先级 `setPriority`。这个优先级并不是我们之前讲到的 Nice 值,Java 的优先级分为 10 个等级,取值从 1 到 10,根据取值的大小,优先级越来越高,一般 Android 线程默认启动设置的优先级为 `NORM_PRIORITY = 5`。
虽然 Java 的优先级和 Nice 值不一样,但是它们之间同样存在一定的对应关系,当我们在 Java 层设置优先级的时候,同样会导致 Linux 对应轻量级进程的 Nice 值的变化,它们的对应关系,我们可以在 `thread_android.cc` 中找到它们之间的对应关系:
```c
static const int kNiceValues[10] = {
ANDROID_PRIORITY_LOWEST, // 1 (MIN_PRIORITY)
ANDROID_PRIORITY_BACKGROUND + 6,
ANDROID_PRIORITY_BACKGROUND + 3,
ANDROID_PRIORITY_BACKGROUND,
ANDROID_PRIORITY_NORMAL, // 5 (NORM_PRIORITY)
ANDROID_PRIORITY_NORMAL - 2,
ANDROID_PRIORITY_NORMAL - 4,
ANDROID_PRIORITY_URGENT_DISPLAY + 3,
ANDROID_PRIORITY_URGENT_DISPLAY + 2,
ANDROID_PRIORITY_URGENT_DISPLAY // 10 (MAX_PRIORITY)
};
void Thread::SetNativePriority(int newPriority) {
if (newPriority < 1 || newPriority > 10) {
LOG(WARNING) << "bad priority " << newPriority;
newPriority = 5;
}
int newNice = kNiceValues[newPriority-1];
pid_t tid = GetTid();
...
}

可以看到它们的对应关系:

Java Priority nice值
1 19
2 16
3 13
4 10
5 0
6 -2
7 -4
8 -5
9 -6
10 -8
通过 Process 设置优先级

在 Android 中为线程设置优先级,一般鼓励通过 Process 类进行设置,Process 中 setThreadPriority(int priority) 优先级参数和底层Linux 的 Nice 值有一致的对应关系,而且 Process 还提供设置线程组的方法。

不过需要特别说明的一点是,当我们通过 Process 进行线程优先级设置的以后,并不会改变 Thread 对象里面优先级的值,这从某种角度上来说,是系统的一个 bug。

Android 线程优先级的控制

Android 中常见的几种异步方式有 new Thread()、AysncTask、HandlerThread、ThreadPoolExecutor、IntentService。这几种方式中,除了 AysncTask 以外,其他的创建线程的过程中,默认都是和当前线程(一般是 UI 线程)保持一样的优先级,,只有 AysncTask 默认是 THREAD_PRIORITY_BACKGROUND 的优先级,所以为了保证主线程能够拥有较为优先的执行级别,建议在创建异步线程的过程中注意对优先级的控制。

随着进程改变

除了开发者手动为线程设置的优先级意外,根据我们上面对 Android 进程变化的分析,可以知道,在程序运行过程中,随着应用状态的变化,Android 进程的调度策略会发生变化,接下来我们继续分析进程调度策略的变化如果改变进程的优先级(也就是主线程的优先级)和其他线程的优先级的。

在前面计算完进程的优先级后,会通过 applyOomAdjLocked 方法将对应的优先级、adj、进程状态等值应用到进程上,我们注重关注其中关于进程优先级设置的部分。整个执行的过程可以大概总结为:

其中调度组和进程组的映射关系:

调度组 进程组
SCHED_GROUP_BACKGROUND THREAD_GROUP_BG_NONINTERACTIVE
SCHED_GROUP_TOP_APP SCHED_GROUP_TOP_APP_BOUND THREAD_GROUP_TOP_APP
其他 THREAD_GROUP_DEFAULT
  • 通过设置进程组,改变了进程所在 cgroup,
  • 通过设置调度策略实现主线程在实时优先级和普通优先级的切换,
  • 通过设置优先级改变进程 nice 值,同时在底层会改变进程所在的 cgroup。

由于这段代码不是很长,我们也可以看看代码。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
// 进程调度组发生变化
if (app.setSchedGroup != app.curSchedGroup) {
int oldSchedGroup = app.setSchedGroup;
app.setSchedGroup = app.curSchedGroup;
...
if (app.waitingToKill != null && app.curReceivers.isEmpty()
&& app.setSchedGroup == ProcessList.SCHED_GROUP_BACKGROUND) {
// 满足条件直接 kill 掉
app.kill(app.waitingToKill, true);
success = false;
} else {
// 调度组映射到进程组
int processGroup;
switch (app.curSchedGroup) {
case ProcessList.SCHED_GROUP_BACKGROUND:
processGroup = THREAD_GROUP_BG_NONINTERACTIVE;
break;
case ProcessList.SCHED_GROUP_TOP_APP:
case ProcessList.SCHED_GROUP_TOP_APP_BOUND:
processGroup = THREAD_GROUP_TOP_APP;
break;
default:
processGroup = THREAD_GROUP_DEFAULT;
break;
}
long oldId = Binder.clearCallingIdentity();
try {
// 设置进程组,对应到底层的 cgroup
setProcessGroup(app.pid, processGroup);
if (app.curSchedGroup == ProcessList.SCHED_GROUP_TOP_APP) {
// do nothing if we already switched to RT
if (oldSchedGroup != ProcessList.SCHED_GROUP_TOP_APP) {
// 应用切换成前台应用 mVrController.onTopProcChangedLocked(app);
if (mUseFifoUiScheduling) {
// Switch UI pipeline for app to SCHED_FIFO
app.savedPriority = Process.getThreadPriority(app.pid);
scheduleAsFifoPriority(app.pid, /* suppressLogs */true);
if (app.renderThreadTid != 0) {
scheduleAsFifoPriority(app.renderThreadTid,
/* suppressLogs */true);
if (DEBUG_OOM_ADJ) {
Slog.d("UI_FIFO", "Set RenderThread (TID " +
app.renderThreadTid + ") to FIFO");
}
} else {
if (DEBUG_OOM_ADJ) {
Slog.d("UI_FIFO", "Not setting RenderThread TID");
}
}
} else {
// Boost priority for top app UI and render threads
setThreadPriority(app.pid, TOP_APP_PRIORITY_BOOST);
if (app.renderThreadTid != 0) {
try {
setThreadPriority(app.renderThreadTid,
TOP_APP_PRIORITY_BOOST);
} catch (IllegalArgumentException e) {
// thread died, ignore
}
}
}
}
} else if (oldSchedGroup == ProcessList.SCHED_GROUP_TOP_APP &&
app.curSchedGroup != ProcessList.SCHED_GROUP_TOP_APP) {
// 应用退出前台 mVrController.onTopProcChangedLocked(app);
if (mUseFifoUiScheduling) {
// Reset UI pipeline to SCHED_OTHER
setThreadScheduler(app.pid, SCHED_OTHER, 0);
setThreadPriority(app.pid, app.savedPriority);
if (app.renderThreadTid != 0) {
setThreadScheduler(app.renderThreadTid,
SCHED_OTHER, 0);
setThreadPriority(app.renderThreadTid, -4);
}
} else {
// Reset priority for top app UI and render threads
setThreadPriority(app.pid, 0);
if (app.renderThreadTid != 0) {
setThreadPriority(app.renderThreadTid, 0);
}
}
}
} catch (Exception e) {
if (false) {
Slog.w(TAG, "Failed setting process group of " + app.pid
+ " to " + app.curSchedGroup);
Slog.w(TAG, "at location", e);
}
} finally {
Binder.restoreCallingIdentity(oldId);
}
}
}

到这里我们已经清晰的了解到进程在应用状态变化后,都发生了哪些优先级的变化,接下来还有一个疑团,就是其他线程的优先级的变化,根据观察我们发现,除了主线程的优先级会发生变化,其他子线程在创建以后,除非开发者手动修改其优先级,否则子线程的优先级并不会发生变化。但是在应用状态发生变化的时候,子线程其所在的进程组合主线程(也就是应用进程)是保持一致的,这是由于我们在设置进程组的时候,会遍历当前进程下所有的 task,然后根据不同的 cgroup 子系统设置进程组,这段代码在 android_util_Process.cppandroid_os_Process_setProcessGroup(JNIEnv* env, jobject clazz, int pid, jint grp) 方法中:

这里通过 SchedPolicy sp = (SchedPolicy) grp; 将前文说的 Process 进程组和 SchedPolicy 进程调度组进行对应转化。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
// 打开进程所有 task 目录
sprintf(proc_path, "/proc/%d/task", pid);
if (!(d = opendir(proc_path))) {
// If the process exited on us, don't generate an exception
if (errno != ENOENT)
signalExceptionForGroupError(env, errno, pid);
return;
}
// 遍历所有task
while ((de = readdir(d))) {
int t_pid;
int t_pri;
if (de->d_name[0] == '.')
continue;
t_pid = atoi(de->d_name);
if (!t_pid) {
ALOGE("Error getting pid for '%s'
", de->d_name);
continue;
}
t_pri = getpriority(PRIO_PROCESS, t_pid);
if (t_pri <= ANDROID_PRIORITY_AUDIO) {
int scheduler = sched_getscheduler(t_pid);
if ((scheduler == SCHED_FIFO) || (scheduler == SCHED_RR)) {
// This task wants to stay in its current audio group so it can keep its budget
// don't update its cpuset or cgroup
continue;
}
}
if (isDefault) {
if (t_pri >= ANDROID_PRIORITY_BACKGROUND) {
// This task wants to stay at background
// update its cpuset so it doesn't only run on bg core(s)
#ifdef ENABLE_CPUSETS
int err = set_cpuset_policy(t_pid, sp);
if (err != NO_ERROR) {
signalExceptionForGroupError(env, -err, t_pid);
break;
}
#endif
continue;
}
}
int err;
#ifdef ENABLE_CPUSETS
// set both cpuset and cgroup for general threads
err = set_cpuset_policy(t_pid, sp);
if (err != NO_ERROR) {
signalExceptionForGroupError(env, -err, t_pid);
break;
}
#endif
err = set_sched_policy(t_pid, sp);
if (err != NO_ERROR) {
signalExceptionForGroupError(env, -err, t_pid);
break;
}
}
closedir(d);

小结

在 Android 应用状态发生变化以后,会导致进程的 oom_score_adjprocStateschedGroup 等进程状态的重新计算和设置,从而改变进程的优先级和调度策略,帮助系统进行更合理资源分配和资源回收。

Android 中的线程对应到 Linux 的内核中的轻量级进程,所以 Linux 为其分配资源适用 Linux 进程调度策略。其中主线程等同于应用进程的优先级,一般由 Android 系统根据应用状态的变化自行调控,不建议开发者手动设置,不过我们为应用中各子线程设置的优先级,将直接影响到主线程在抢占各种系统资源尤其是 CPU 资源时候的优先级,所以为了保证主线程执行的顺畅,我们应尽量控制子线程的优先级。

解决问题

经过一些调试以后,我们发现应用在启动 1-2s 以后,主线程的优先级就从 TOP_APP_PRIORITY_BOOST(-10) 降为 THREAD_PRIORITY_BACKGROUND(10) 后台进程的优先级,这直接导致主线程在大多数情况下的优先级是低于其他线程的,从而在抢占 CPU 资源时处于劣势。根据之前对于 Android 线程调度分析,可以排除是系统降低的可能,同时我们对比了其他应用,发现所有其他应用当处于前台的时候,主线程的优先级都是 TOP_APP_PRIORITY_BOOST(-10),这进一步加强了对于业务代码误操作导致主线程降低的推断,最后我们通过对 Process.setThreadPriority(priority) 调用的排查,发现的确有一个地方不小心为主线程设置了 THREAD_PRIORITY_BACKGROUND(10) 的优先级。

写在最后

可以预测,如果不是这次对于卡顿栈的分析,我们不能确定我们还有多久才能发现这个已经存在很久的 bug,我们依然会这样一个小小的失误而承担巨大的成本,因为在后台线程本身就很多主线程的优先级得不到保障的情况下,应用的卡顿是不可避免的,而且可能做再多其他方面的优化,也于事无补,性能检测和监控的价值就在这里,虽然不能马上让应用有质的飞跃,但一点一滴的优化,我们的应用会变得越来越流畅。

参考资料

Comments