最近我们的 APM 上线了应用卡顿的性能检测,我们使用的是和 BlockCanary 同样的方案,通过 Looper Printer 去监控应用的卡顿。在收集到线上数据以后,发现一个比较怪异的现象,大量的卡顿的情况下,当前执行线程(主线程)的执行时间其实并不长,主线程只执行了几毫秒,但是却卡顿1s甚至更长的时间。很明显这个时候是由于主线程没有抢占到CPU导致,为了搞清楚为什么主线程没有抢到CPU,我把 Android 线程调度仔细撸了一遍。
进程是资源管理的最小单位,线程是程序执行的最小单位。在操作系统设计上,从进程演化出线程,最主要的目的就是更好的支持SMP以及减小(进程/线程)上下文切换开销。
无论按照怎样的分法,一个进程至少需要一个线程作为它的指令执行体,进程管理着资源(比如cpu、内存、文件等等),而将线程分配到某个cpu上执行。一个进程当然可以拥有多个线程,此时,如果进程运行在SMP机器上,它就可以同时使用多个cpu来执行各个线程,达到最大程度的并行,以提高效率;同时,即使是在单cpu的机器上,采用多线程模型来设计程序,正如当年采用多进程模型代替单进程模型一样,使设计更简洁、功能更完备,程序的执行效率也更高,例如采用多个线程响应多个输入,而此时多线程模型所实现的功能实际上也可以用多进程模型来实现,而与后者相比,线程的上下文切换开销就比进程要小多了,从语义上来说,同时响应多个输入这样的功能,实际上就是共享了除cpu以外的所有资源的。
针对线程模型的两大意义,分别开发出了核心级线程和用户级线程两种线程模型,分类的标准主要是线程的调度者在核内还是在核外。前者更利于并发使用多处理器的资源,而后者则更多考虑的是上下文切换开销。
需要理解 Linux 进程与 Android 线程的关系,需要先解释清楚 Linux 中内核线程、用户线程的关系,在 内核线程、轻量级进程、用户线程的区别和联系 中有比较清晰的阐述。可以总结为几点:
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 权限
现在的操作系统都是多任务的,为了能让更多的任务能同时在系统上更好的运行,需要一个管理程序来管理计算机上同时运行的各个任务(也就是进程)。这个管理程序就是调度程序,它的功能说起来很简单:
进程提供了两种优先级,一种是普通的进程优先级,第二个是实时优先级。前者适用 SCHED_NORMAL 调度策略,后者可选 SCHED_FIFO 或 SCHED_RR 调度策略。任何时候,实时进程的优先级都高于普通进程,实时进程只会被更高级的实时进程抢占,同级实时进程之间是按照 FIFO(一次机会做完)或者 RR(多次轮转)规则调度的。
普通进程和实时进程分别用 nice 值和实时优先级(RTPRI)来度量优先级。
Linux 中,使用 nice 值来设定一个普通进程的优先级,系统任务调度器根据 nice 值合理安排调度。
Linux 进程优先级与 nice 值及实时进程优先级的关系:
通过 ps -p 可以看到这几个值之间的对应关系:
除此之外,在执行阶段,调度程序通过增加或减少进程静态优先级的值,来达到奖励IO消耗型或惩罚cpu消耗型的进程,调整后的进程称为动态优先级。与之对应的我们前面提到的优先级的值被称为静态优先级。
优先级,可以决定谁先运行。但是对于调度程序来说,并不是运行一次就结束了,还必须知道间隔多久进行下次调度。于是就有了时间片的概念。时间片是一个数值,表示一个进程被抢占前能持续运行的时间。也可以认为是进程在下次调度发生前运行的时间(除非进程主动放弃CPU,或者有实时进程来抢占CPU)。时间片的大小设置并不简单,设大了,系统响应变慢(调度周期长);设小了,进程频繁切换带来的处理器消耗。默认的时间片一般是10ms。
举个例子说明调度原理的实现1。
假设系统中只有3个进程ProcessA(NI=+10),ProcessB(NI=0),ProcessC(NI=-10),NI表示进程的nice值,时间片=10ms:
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采用的所有进程优先级的比重来计算每个进程的时间片的,是相对的而不是绝对的。
Cgroups是control groups的缩写,是Linux内核提供的一种可以限制、记录、隔离进程组(process groups)所使用的物理资源(如:cpu,memory,IO等等)的机制。最初由google的工程师提出,后来被整合进Linux内核。也是目前轻量级虚拟化技术 lxc (linux container)的基础之一。
Cgroups最初的目标是为资源管理提供的一个统一的框架,既整合现有的cpuset等子系统,也为未来开发新的子系统提供接口。现在的cgroups适用于多种应用场景,从单个进程的资源控制,到实现操作系统层次的虚拟化(OS Level Virtualization)。Cgroups提供了以下功能:
Android中关于 cpu/cpuset/schedtune 三个子系统的应用都是基于进程优先级的。AMS(ActivityManagerService) 和 PMS(PackageManagerService) 等通过 Process 设置进程优先级、调度策略等;android/osProcess JNI通过调用libcutils.so/libutils.so执行getpriority/setpriority/schedsetscheduler/schedgetschedler系统调用或者直接操作CGroup文件节点以达到设置优先级,限制进程CPU资源的目的。
Android 中在从设置进程优先级到最后映射到不同 cgroups 下的过程,有兴趣的可以参考 Android中关于cpu/cpuset/schedtune的应用 这篇文章。我们这里以 cpu 子系统为例介绍一下再 CPU 子系统下是如何控制不同 cgroup 对 CPU 资源的访问。
CPU 子系统连接的 /dev/cpuctl 层级结构下有两个 cgroup,分别是
/
,对应到 Android 的前台进程组。/bg_non_interactive
,对应到 Android 的后台进程组。在 cgroup 下定义了一些参数,来控制不同的 cgroup 在使用 cpu 资源时的配置:
shell@hammerhead:/ $ cat /dev/cpuctl/cpu.shares
1024
shell@hammerhead:/ $ cat /dev/cpuctl/bg_non_interactive/cpu.shares
52
同样我们也可查看 cpu.rt_period_us
与cpu.rt_runtime_us
的时间对比:
shell@hammerhead:/ $ cat /dev/cpuctl/cpu.rt_period_us
1000000
shell@hammerhead:/ $ cat /dev/cpuctl/cpu.rt_runtime_us
800000
即单个逻辑CPU下每一秒内可以获得0.8秒的执行时间。
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秒。
Android 底层对进程分组的操作最后是通过 sched_policy.c
文件中的 set_sched_policy(int tid, SchedPolicy policy)
和 set_cpuset_policy(int tid, SchedPolicy policy)
函数添加到对应的进程组的,调用这两个函数的传递的 SchedPolicy 定义在sched_policy.h
中,定义不同的调度策略:
/* 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 为进程找到不同的进程组,并添加进去。
// 根据不同调度策略选择不同的进程组
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 层级架构:
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\n", 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\n", 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 对线程组的定义:
/**
* 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 开发者应该都知道在系统中进程重要性的划分:
相信大家都很清楚,这里就不做过多的介绍了,不过对于进程重要性是通过哪些操作发生变更的,以及和我们前面讲的 Linux 进程分组又是怎么关联和映射上的,是下面要讲述的重点。
对于每一个运行中的进程,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
(第1档)开始杀进程,如果内存还不足,那么会杀 CACHED_APP_MIN_ADJ
(第2档),不断深入,直到满足内存阈值条件。ProcessRecord中下面这些属性反应了 oom_score_adj 的值:
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
对应的在 ActivityManager 重定义了 process_state 级别的划分,Android 系统会在修改进程状态的同时更新 oom_score_adj 的分级:
在 ProcessRecord 中,记录了和进程状态相关的属性:
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
对应到底层进程分组,除了上面提到的 Process.java 定义的不同线程组的定义,同时还为 Activity manager 定义了一套类似的调度分组,和之前的线程分组定义也存在对应关系:
// 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 中,也记录了和调度组相关的属性:
int curSchedGroup; // Currently desired scheduling class
int setSchedGroup; // Last set to background scheduling class
我们知道影响 Android 应用进程优先级变化的是根据 Android
应用组件的生命周期变化相关。Android进程调度之adj算法 里面罗列了所有会触发进程状态发生变化的事件,主要包括:
位于ActiveServices.java
位于 ActivityManagerService.java
这些事件都会直接或间接调用到 ActivityManagerService.java 中的 updateOomAdjLocked 方法来更新进程的优先级,updateOomAdjLocked 先通过 computeOomAdjLocked 方法负责计算进程的优先级,再通过调用 applyOomAdjLocked 应用进程的优先级。
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,找出其中优先级最高的设置为进程的优先级。
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 |
在当前进程满足
adj > ProcessList.FOREGROUND_APP_ADJ
|| schedGroup == ProcessList.SCHED_GROUP_BACKGROUND
|| procState > ActivityManager.PROCESS_STATE_TOP
这种状态下遍历所有的Service,并且还需要遍历每一个Service的所有连接。然后根据连接的关系确认客户端进程的优先级来确定当前进程的优先级。
这里详细记录了在 bindService 过程中,传递的不同的 FLAG 对于 Service 进程和 Client 进程关联计算 adj 级别。由于涉及的分支判断较多,如果想要仔细研究,最好对着代码一一查看。这里只介绍整个过程中涉及到进程调度组发生的变化:
FLAG |
---|
BIND_WAIVE_PRIORITY |
BIND_NOT_FOREGROUND |
BIND_IMPORTANT_BACKGROUND |
且 client 的 curSchedGroup 大于当前进程的 schedGroup,则需要重新设置当前进程的调度策略;此时,如果有设置 BIND_IMPORTANT 这个 flag,则赋值 client.curSchedGroup 给 schedGroup,否则则将 schedGroup 设置为 SCHED_GROUP_DEFAULT。
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;
}
}
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 的遍历和 Service 的遍历是类似的,在满足
(adj > ProcessList.FOREGROUND_APP_ADJ
|| schedGroup == Process.THREAD_GROUP_BG_NONINTERACTIVE
|| procState > ActivityManager.PROCESS_STATE_TOP)
的条件下进行两次循环遍,其中涉及到进程调度组发生变更的情况:
if (client.curSchedGroup > schedGroup) {
schedGroup = ProcessList.SCHED_GROUP_DEFAULT;
}
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;
}
}
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。
最后会进行一次 adj 和 app.maxAdj 的对比,如果 adj > app.maxAdj 并且 app.maxAdj <= ProcessList.PERCEPTIBLE_APP_ADJ 则将 schedGroup 设置为 SCHED_GROUP_DEFAULT, 然后保存之前计算的 adj、schedGroup 和 procState。
app.curAdj = app.modifyRawOomAdj(adj);
app.curSchedGroup = schedGroup;
app.curProcState = procState;
Android 线程优先级的变化分为两种,一种是根据上面计算的进程优先级的变化,给 Android 线程带来的变化,另一种是开发者可以在代码中手动改变线程的优先级。
我们都知道,在利用 Thread 创建线程或者用 ThreadPoolExecutor 创建线程的时候,我们可以为当前设置的线程设置优先级 setPriority。这个优先级并不是我们之前讲到的 Nice 值,Java 的优先级分为 10 个等级,取值从 1 到 10,根据取值的大小,优先级越来越高,一般 Android 线程默认启动设置的优先级为 NORM_PRIORITY = 5。
虽然 Java 的优先级和 Nice 值不一样,但是它们之间同样存在一定的对应关系,当我们在 Java 层设置优先级的时候,同样会导致 Linux 对应轻量级进程的 Nice 值的变化,它们的对应关系,我们可以在 thread_android.cc 中找到它们之间的对应关系:
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 |
在 Android 中为线程设置优先级,一般鼓励通过 Process 类进行设置,Process 中 setThreadPriority(int priority) 优先级参数和底层Linux 的 Nice 值有一致的对应关系,而且 Process 还提供设置线程组的方法。
不过需要特别说明的一点是,当我们通过 Process 进行线程优先级设置的以后,并不会改变 Thread 对象里面优先级的值,这从某种角度上来说,是系统的一个 bug。
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 |
// 进程调度组发生变化
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.cpp 的 android_os_Process_setProcessGroup(JNIEnv* env, jobject clazz, int pid, jint grp) 方法中:
这里通过 SchedPolicy sp = (SchedPolicy) grp; 将前文说的 Process 进程组和 SchedPolicy 进程调度组进行对应转化。
// 打开进程所有 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'\n", 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_adj、procState、schedGroup 等进程状态的重新计算和设置,从而改变进程的优先级和调度策略,帮助系统进行更合理资源分配和资源回收。
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,我们依然会这样一个小小的失误而承担巨大的成本,因为在后台线程本身就很多主线程的优先级得不到保障的情况下,应用的卡顿是不可避免的,而且可能做再多其他方面的优化,也于事无补,性能检测和监控的价值就在这里,虽然不能马上让应用有质的飞跃,但一点一滴的优化,我们的应用会变得越来越流畅。
转载:https://zhuanlan.zhihu.com/p/34799829