您当前的位置: 首页 >  linux
  • 0浏览

    0关注

    674博文

    0收益

  • 0浏览

    0点赞

    0打赏

    0留言

私信
关注
热门博文

从 Linux 进程调度到 Android 线程管理

沙漠一只雕得儿得儿 发布时间:2021-12-05 09:25:33 ,浏览量:0

从 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_FIFO或 SCHED_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/schedsetscheduler/schedgetschedler系统调用或者直接操作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 利用率。 Bash

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的时间对比: Bash

shell@hammerhead:/ $ cat /dev/cpuctl/cpu.rt_period_us
1000000
shell@hammerhead:/ $ cat /dev/cpuctl/cpu.rt_runtime_us                                          
800000

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

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 中,定义不同的调度策略: C

/* 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 为进程找到不同的进程组,并添加进去。 C

// 根据不同调度策略选择不同的进程组
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 层级架构: C

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 对线程组的定义: 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_DEFAULTSP_DEFAULTTHREAD_GROUP_BG_NONINTERACTIVESP_BACKGROUNDTHREAD_GROUP_FOREGROUNDSP_FOREGROUNDTHREAD_GROUP_SYSTEMSP_SYSTEMTHREAD_GROUP_AUDIO_APPSP_AUDIO_APPTHREAD_GROUP_AUDIO_SYSSP_AUDIO_SYSTHREAD_GROUP_TOP_APPSP_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 的值: Java

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 中,记录了和进程状态相关的属性: Java

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 定义了一套类似的调度分组,和之前的线程分组定义也存在对应关系: Java

// 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 中,也记录了和调度组相关的属性: Java

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 =FOREGROUND_APP_ADJ)PROCESS_STATE_CACHED_EMPTY

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

非前台 Activity

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

Case

schedGroup

adj

procState

activity可见SCHED_GROUP_DEFAULT
关注
打赏
1657159701
查看更多评论
0.0421s