55

Android系统上的进程管理:进程的调度

 4 years ago
source link: https://paul.pub/android-process-schedule/?
Go to the source link to view the article. You can view the picture content, updated content and better typesetting reading experience. If the link is broken, please click the button below to view the snapshot at that time.

Android系统上的进程管理:进程的调度

Loading [MathJax]/jax/output/HTML-CSS/jax.js

之前我写过一些文章讲解Android系统上的进程管理,那几篇文章主要是从ActivityManagerService的角度来讲解。而这篇文章,将从更底层,从Linux内核层的角度讲解Android系统对于进程的调度管理。

bg.JPG

之前我写过几篇文章讲解Android系统上的进程管理,包括:

那几篇文章主要是从ActivityManagerService的角度讲解。在这篇文章中,我们更深入一些。结合Linux内核,来看看Android系统对于进程的调度管理。

为了便于讲解,下文在相关内容中会贴出相应的源码,源码的版本如下:

进程调度是操作系统最核心的功能之一。

在现代的操作系统中(无论是个人电脑还是手机),同一时刻会有几十个甚至几百个进程同时运行。但CPU的数量远没有进程那么多,而进程调度算法就是需要确定:有限的CPU资源该如何分配给进程。

进程的调度算法对整个系统的运行状态有深远的影响。好的调度算法至少需要综合考虑下面四个因素:

  • 快速的进程响应时间,这对于终端系统(例如手机)尤其重要。
  • 后台作业能有较大的吞吐量。
  • 避免进程饥饿。饥饿(starvation)是指进程一直处于等待状态,永远得不到执行的机会。
  • 能够很好的协调高优先级和低优先级进程。

Linux的进程调度是基于时间片(timeslice)的技术,即:将CPU时间划分成一个个小段,然后将这些小段分配给进程。如果某个进程的时间片用完,则强制切换到另外一个进程。

这称之为抢占式(Preemption)多任务。

Linux内核在将CPU时间片分配给进程之前会考虑进程的优先级。并且,每个进程的优先级是动态计算出来的。

需要注意的是,CPU时间片的取值既不能过大,也不能过小。时间片过大会导致每个进程的等待时间过长,整个系统的响应速度变慢。而时间片过小,会导致大部分的时间都消耗在进程的上下文切换上,无效耗费的时间太多。

在讨论进程调度的时候,常常会对进程做一些分类。通常的分类有两种方式。

方式一将进程分为:

  • I/O密集型进程:这类进程有大量时间都在等待输入输出,例如:接受用户的输入事件,或者进行文件IO。
  • CPU密集型进程:这类进程大部分时间都在利用CPU执行计算任务。

方式二将进程分为:

  • 交互式进程:长时间与用户进行交互,例如应用程序的界面部分。
  • 批处理进程:这类进程不与用户交互,一直在后台执行任务。例如编译器,数据库引擎等。
  • 实时进程:这类进程有非常高的实时要求,这通常是控制硬件相关的进程。

实时任务指的是:必须保证任务的完成在规定的时间范围内。但实际上,Linux本身并非真正的实时操作系统,而仅仅是一个软实时(soft real-time)的系统。它只是尽可能的保证任务的完成时间,但使用者如果将其用在可能导致致命的系统上(例如:自动驾驶)就可能会出现问题。

Linux调度器

目前的Linux内核中,是一套调度框架中包含了几个调度器来共同完全调度任务。

每个调度器都有一个优先级,调度框架根据优先级来选择调度器,然后再由调度器来选择进程。

调度器的实现位于 /kernel/sched 目录下。这其中几个核心文件说明如下:

文件 说明 调度策略
core.c 调度框架核心逻辑 -
fair.c CFS实现 SCHED_NORMAL
rt.c 实时调度器 SCHED_FIFO,SCHED_RR
deadline.c Deadline调度器 SCHED_DEADLINE

CFS全称是Completely Fair Scheduler。这是普通进程的默认调度器,也是整个调度框架的核心。

这里的调度策略会在下文中讲解。

Linux调度器的发展历史

Linux内核经过了近30年的开发,其中的进程调度器自然也经历了很多次改进。

A complete guide to Linux process scheduling》这篇论文中详细描述了每个版本的调度器实现,因此这里仅仅做一个简要的对比。

调度器名称 版本 时间 介绍
初始版本 0.01 1991年 只有一个进程队列,每次调度遍历以选择执行的进程
O(n)调度器 2.4 2001年 与初始版本类似,但引入了goodness来描述进程优先级
O(1)调度器 2.6.8.1 2004年 基于全局的优先队列完成调度选择,无需遍历所有进程
实时调度器 2.6.21 2007年 实现了SCHED_FIFO,SCHED_RR两个策略。
CFS 2.6.23 2007年 基于红黑树数据结构,实现“公平”调度。
Deadline调度器 3.14 2014年 实时调度器,实现了SCHED_DEADLINE策略。

除了上面这些调度器,Con Kolivas 开发的Staircase调度器RSDL调度器,以及BFS调度器也值得了解。事实上,他的算法直接影响了CFS调度器。

Linux上的进程与线程

进程是运行中的程序。内核需要记录和管理进程运行中的相关信息,包括:地址空间,内存映射,打开的文件,进程状态以及其中包含的线程等。

在Linux中,线程又称做轻量级进程。线程包含了一个执行的上下文。一个进程中可以包含一个或多个线程,每个线程有自己的线程id(tid),程序计数器,程序栈以及寄存器。同一个进程中的多个线程共享进程的地址空间,这使得线程之间共享数据非常方便。

在Linux中,通常通过下面的方式创建进程:

clone(SIGCHLD, 0);

SIGCHLD意味着当子进程退出时,需要发送这个信号给父进程。

而创建线程的方法如下:

clone(CLONE_VM | CLONE_FS | CLONE_FILES | CLONE_SIGHAND, 0);

可以看出,这与创建进程非常的相似,区别在于:这里同时复制了当前进程的地址空间(CLONE_VM),文件系统(CLONE_FS),打开文件(CLONE_FILES)以及信号量处理器(CLONE_SIGHAND)。

Linux内核提供了以下的系统调用让程序来参与系统的调度策略:

系统调度 说明
nice 设置当前线程的nice值,下文会详细讲解nice值
getpriority 查询某个线程,或者进程组的nice值
setpriority 设置线程或者进程组的nice值
sched_setscheduler 设置指定线程的调度策略和参数
sched_getscheduler 查询指定线程的调度策略和参数
sched_setparam 设置指定线程的调度参数
sched_getparam 查询指定线程的调度参数
sched_get_priority_max 查询特定调度策略的最大优先级值
sched_get_priority_min 查询特定调度策略的最小优先级值
sched_rr_get_interval 查询round-robin策略下线程的定量
sched_yield 使得当前线程让出CPU给其他线程使用
sched_setaffinity 设置指定线程的CPU掩码
sched_getaffinity 查询指定线程的CPU掩码
sched_setattr 设置指定线程的调度策略和调度参数
sched_getattr 查询指定线程的调度策略和调度参数

通过nicesetpriority,和sched_setattr三个系统调度可以设置进程/线程的nice值。

nice值指的是自身相对于其他人的“友好”程度,因此:nice值越大,优先级越低。反之则反。

在POSIX标准中,nice值是一个进程相关的值,因此进程中所有线程的nice值是一样的。但在Linux中,nice值是一个线程相关的值,因此同一个进程的不同线程可以设置不同的nice值。

Linux上,nice值的范围是[−20,19]。其中 -20 是最高优先级,19 是最低优先级。

特权与资源限制

上面这些系统调用可能会影响整个系统的调度情况,因此为了保证系统的稳定,并非所有进程都能随意进行设置。对于这些系统调用的限制如下:

在Linux 2.6.12之前的版本上,只有特权线程可以设置非0的静态优先级(使用实时调度策略)。非特权线程只允许设置使用SCHED_NORMAL策略,并且这个改动还要求调用者的Effective user ID和目标线程的Real user ID或者Effective user ID一致。

只有具有CAP_SYS_NICE特权的线程允许设置或者更改SCHED_DEADLINE策略。

从Linux 2.6.12开始,RLIMIT_RTPRIO定义了非特权线程对于SCHED_RRSCHED_FIFO静态优先级设置的上限。相关规则如下:

  • 如果非特权线程具有非零的RLIMIT_RTPRIO软限制,则它可以更改其调度策略和优先级,但其优先级不能设置为超过当前优先级的最大值以及其RLIMIT_RTPRIO限制。

  • 如果RLIMIT_RTPRIO软限制为0,则只允许降低优先级,或切换到非实时策略。

  • 对于修改其他线程的线程来说,遵循相同的策略。并且需要调用者的Effective user ID和目标线程的Real user ID或者Effective user ID一致。

  • SCHED_IDLE使用不一样的策略。在Linux 2.6.39之前,一个使用此策略的非特权线程始终无法更改其调度策略,无论其RLIMIT_RTPRIO值是什么。在Linux 2.6.39之后的版本中,非特权的线程可以切换到SCHED_BATCHSCHED_NORMAL策略,只要其值在RLIMIT_NICE所允许的范围即可。

另外,特权(CAP_SYS_NICE)线程不受RLIMIT_RTPRIO限制;

内核中的数据结构

与调度相关的核心结构和常量定义在下面两个头文件中:

其中最重要的就是描述进程的数据结构task_struct

这个结构体非常的大,里面包含了非常多的用来描述进程的字段。这里我们只关心与进程调度相关的内容,它们如下所示:

struct task_struct {
  int prio;
  int static_prio;
  int normal_prio;
  unsigned int rt_priority;

  const struct sched_class *sched_class;
  struct sched_entity se;
  struct sched_rt_entity rt;
  ...

  unsigned int policy;
  cpumask_t cpus_allowed;
  ...
  pid_t pid;
}

这些字段说明如下:

  • priostatic_prionormal_prio:描述了进程的优先级,它们之间存在互相的关联关系。
  • rt_priority:实时进程使用,描述进程的实时优先级。
  • sched_class:进程所属的调度器类。
  • se:调度的实体,既可能是一个线程,也可以是一组进程。
  • policy:所使用的调度策略,见下文。
  • cpus_allowed:允许运行的CPU。可以通过sched_setaffinity来设置。
  • pid:进程的id。

在目前的Linux内核中,一共有六种调度策略,它们可以分为三类:

  • 普通调度策略:SCHED_NORMAL, SCHED_BATCHSCHED_IDLE
  • 实时调度策略:SCHED_FIFO, SCHED_RR
  • Deadline调度策略:SCHED_DEADLINE

六种调度策略说明如下:

  • SCHED_NORMAL:也叫SCHED_OTHER。这是进程的默认调度策略,也就是时间共享策略。绝大部分进程都使用这个调度策略。
  • SCHED_BATCH:与SCHED_NORMAL类似。不同的是,内核会认为该进程是CPU密集型,因此在调度会有小的惩罚。这种策略适用于那些非交互的后台进程。
  • SCHED_IDLE:最低优先级的调度策略,nice值不被考虑。因此它的调度将低于SCHED_NORMALSCHED_BATCH
  • SCHED_FIFO:FIFO全称是First in-first out。这种策略不会使用时间片算法,在同优先级的情况下,会按照先进先出的方法按顺序执行。作为一种实时调度策略,属于该调度策略的进程会一直执行直到被IO阻塞或者被更高优先级的进程抢占。
  • SCHED_RR:RR的全称是Round-robin。这是对于SCHED_FIFO增强的实时策略,它使用了时间片共享的方式来调度进程。
  • SCHED_DEADLINE:指定了预计完成时间的调度策略,它拥有超过所有其他策略的最高优先级。

实时调度策略(SCHED_FIFOSCHED_RR)的优先级始终高于普通调度策略(SCHED_NORMALSCHED_BATCHSCHED_IDLE),整体来说,六种调度策略的优先级排列如下:

SCHED_DEADLINE > SCHED_FIFO = SCHED_RR > SCHED_NORMAL > SCHED_BATCH > SCHED_IDLE

进程优先级

include/linux/sched/prio.h 文件中,定义了一系列宏用来描述进程的优先级:

#define MAX_NICE             19
#define MIN_NICE             -20
#define NICE_WIDTH           (MAX_NICE - MIN_NICE + 1)

#define MAX_USER_RT_PRIO     100
#define MAX_RT_PRIO          MAX_USER_RT_PRIO
#define MAX_PRIO             (MAX_RT_PRIO + NICE_WIDTH)
#define DEFAULT_PRIO         (MAX_RT_PRIO + NICE_WIDTH / 2)

#define NICE_TO_PRIO(nice)   ((nice) + DEFAULT_PRIO)
#define PRIO_TO_NICE(prio)   ((prio) - DEFAULT_PRIO)

#define USER_PRIO(p)         ((p)-MAX_RT_PRIO)
#define TASK_USER_PRIO(p)    USER_PRIO((p)->static_prio)
#define MAX_USER_PRIO        (USER_PRIO(MAX_PRIO))

整体来说,Linux中进程的优先级范围是:[0, 139]。值越小,优先级越高

其中,实时进程占用了[0, 99]的范围。普通进程占用了[100, 139]的范围。nice值最终会映射到 [100, 139]的范围内。

图示如下:

priority.png

查看进程的调度信息

通过ps命令,我们可以查看进程的调度信息。

不过在不同的系统上,该命令的参数不一样。

在Linux系统上,可以通过下面这条命令查看:

ps ax -o uname,pid,cls,pri,rtprio,cmd

-o指定了输出的列。列名说明如下:

  • uname: 进程所属的用户名称。
  • pid: 进程id。
  • cls:进程的调度策略。TS表示SCHED_OTHERFF表示SCHED_FIFORR表示SCHED_RRB表示SCHED_BATCHIDL表示SCHED_IDLE
  • pri:进程的优先级。
  • rtprio:进程的实时优先级。
  • cmd:进程的可执行文件名称。

如果是希望查看所有线程,可以使用这样的参数

ps ax -L -o uname,pid,tid,cls,pri,rtprio,cmd,comm

tid即线程id。

在Android系统上,可以通过下面这条命令查看相关信息:

ps -A -o PID,TID,SCHED,PRI,RTPRIO,NICE,PCY,NAME,CMD

这里的列名说明如下:

  • PID:进程id。
  • TID:线程id。
  • SCHED:进程/线程的调度器:0=other, 1=fifo, 2=rr, 3=batch, 4=iso, 5=idle。
  • PRI:进程/线程的优先级。值越大,优先级越高。
  • RTPRIO:进程/线程的实时优先级。
  • NICE:进程/线程的nice值。
  • PCY:进程/线程的Android调度策略,可能是fg(前台)或者bg(后台)。
  • NAME:进程名称。
  • CMD:线程名称。

下面是一个输出样例:

 PID   TID  SCH PRI RTPRIO  NI PCY NAME                        CMD
  ...                       
 1095  1095   0  21      -  -2  fg system_server               system_server
 1095  1102   0  39      - -20  fg system_server               Jit thread pool
 1095  1103   0  39      - -20  fg system_server               Runtime worker 
 1095  1104   0  39      - -20  fg system_server               Runtime worker 
 1095  1105   0  39      - -20  fg system_server               Runtime worker 
 1095  1106   0  39      - -20  fg system_server               Runtime worker 
 1095  1107   0  39      - -20  fg system_server               Signal Catcher
 1095  1108   0  15      -   4  fg system_server               HeapTaskDaemon
 1095  1109   0  15      -   4  fg system_server               ReferenceQueueD
 1095  1110   0  15      -   4  fg system_server               FinalizerDaemon
 1095  1111   0  15      -   4  fg system_server               FinalizerWatchd
 1095  1112   0  19      -   0  fg system_server               Binder:1095_1
 1095  1113   0  19      -   0  fg system_server               Binder:1095_2
 1095  1143   0  19      -   0  fg system_server               android.fg
 1095  1144   0  21      -  -2  ta system_server               android.ui
 1095  1145   0  19      -   0  fg system_server               android.io
 ...

cgroup

cgroup是control group的缩写。这是一个Linux内核的特性。用来对进程所使用的资源(如CPU、内存、磁盘输入输出等)进行限制、统计与隔离。

cgroup 单数形式用于指定整个特性,也用作“cgroup控制器”中的限定符。 当明确指代多个单独的控制组时,使用复数形式“cgroups”。

这个项目最早是由Google的工程师(主要是Paul Menage和Rohit Seth)在2006年发起。该功能于2008年(Android的发布也是在这一年)合入到Linux 2.6.24版本中。这是cgroup的第一个版本。

后来cgroup由Tejun Heo维护。他重新设计并重写了cgroup,第二个版本的cgroup在2016年Linux 4.5版本中发布。

实现 cgroup 的主要目的是为不同用户层面的资源管理提供一个统一的接口。从单个任务的资源控制到操作系统层面的虚拟化,cgroup 提供了四大功能:

  • 资源限制:cgroup 可以对任务是要的资源总额进行限制。比如设定任务运行时使用的内存上限,一旦超出就发 OOM。
  • 优先级分配:通过分配的 CPU 时间片数量和磁盘 IO 带宽,实际上就等同于控制了任务运行的优先级。
  • 资源统计:cgoup 可以统计系统的资源使用量,比如 CPU 使用时长、内存用量等。这个功能非常适合当前云端产品按使用量计费的方式。
  • 任务控制:cgroup 可以对任务执行挂起、恢复等操作。

cgroup主要由两个部分组成:

  • 核心部分:主要负责按层级组织管理进程。
  • 控制器:cgroup包含了多个控制器,一个控制器负责一类特定系统资源的管理。控制器也可以称之为子系统。例如 CPU 子系统可以控制 CPU 的时间分配,内存子系统可以限制内存的使用量。

cgroup以树型结构进行组织,系统中的每个进程都属于且只属于一个cgroup。创建时,所有进程都放在父进程所属的cgroup中。 进程可以迁移到另一个cgroup。 迁移进程不会影响后代进程。

在一些结构性约束下,可以在cgroup上选择性地启用或禁用某个控制器。 所有控制器行为都是分层的 - 如果在某个cgroup上启用了控制器,则它会影响属于该cgroup子层次的所有进程。 在嵌套的cgroup上启用控制器时,它始终会进一步限制资源使用。

Version 1

cgroup第一版本的官方文档见这里:cgroup-v1

cgroup的第一个版本目前包含了下面13个子系统:

名称 起始Linux版本 说明
cpu 2.6.24 限制 CPU 时间片的分配,与 cpuacct 挂载在同一目录。
cpuacct 2.6.24 生成 cgroup 中的任务占用 CPU 资源的报告,与 cpu 挂载在同一目录。
cpuset 2.6.24 给 cgroup 中的任务分配独立的 CPU(多处理器系统)和内存节点。
memory 2.6.25 对 cgroup 中的任务的可用内存进行限制,并自动生成资源占用报告。
devices 2.6.26 允许或禁止 cgroup 中的任务访问设备。
freezer 2.6.28 暂停/恢复 cgroup 中的任务。
net_cls 2.6.29 对cgroup中的任务进行网络限制。
blkio 2.6.33 对块设备的 IO 进行限制。
perf_event 2.6.39 允许使用 perf 工具来监控 cgroup。
net_prio 3.3 允许基于 cgroup 设置网络流量(netowork traffic)的优先级。
hugetlb 3.5 制使用的内存页数量。
pids 4.3 限制任务的数量。
rdma 4.11 限制RDMA/IB相关资源

可以通过下面这条命令来挂载 cgroup 系统并使能所有子系统:

mount -t cgroup xxx /sys/fs/cgroup

cgroup不会处理这里的”xxx”,但它会出现在/proc/mounts文件中以便辨识。

如果只想使用部分子系统,可以通过下面的方法:

mount -t tmpfs cgroup_root /sys/fs/cgroup
mkdir /sys/fs/cgroup/rg1
mount -t cgroup -o cpuset,memory hier1 /sys/fs/cgroup/rg1

对于每一个子系统都会包含一个cgroup.procs文件和一个tasks文件。前者记录了属于该子系统的进程id,而后者是线程id

对于cgroup的使用就是将进程或线程的id写入到这个文件中。

/bin/echo PID_n > cgroup.procs

需要注意的是,这里一次只能写入一个pid,不能写入多个。

以启用cgroup,并且使用cpuset子系统为例,其操作步骤如下:

  1. mount -t tmpfs cgroup_root /sys/fs/cgroup 挂在cgroup根系统
  2. mkdir /sys/fs/cgroup/cpuset 创建cpuset子系统目录
  3. mount -t cgroup -o cpuset cpuset /sys/fs/cgroup/cpuset 挂载cpuset子系统
  4. 启动任务的根进程
  5. echo the_pid > /sys/fs/cgroup/cpuset/tasks 将任务根进程的pid写入子系统中
  6. 由根进程执行任务或者fork子进程来执行任务

可以通过下面这条命令查看系统中cgroup的挂载情况:

cat /proc/mounts  | grep cgroup

对于某个具体的进程,可以根据pid查看其proc文件系统下的文件以确认其cgroup的使用情况:

cat /proc/[pid]/cgroup

下面是一个示例:

$ cat /proc/2924/cgroup                                                                                                                                                                               
4:cpuset:/restricted
3:cpu:/
2:schedtune:/top-app
1:cpuacct:/uid_10009/pid_2924

这里是Android上的一个进程,对于具体的配置在下文中会讲解。

Version 2

cgroup第二版本的官方文档见这里:cgroup-v2

随着时间的推移,cgroup添加了各种控制器。然而这些控制器的开发在很大程度上是不协调的,其结果是控制器之间出现了许多不一致,导致cgroup层次结构的管理变得相当复杂。

于是就出现了第二个版本。虽然说v2是用来替代v1的。但是旧的系统仍然存在,出于兼容性的考虑,也不太可能被移除。当前,v2版本只实现了v1的部分控制器。并且v1和v2的控制器可以同时在一个系统上使用。例如:可以使用那些v2支持的控制器,同时也使用v2尚不支持的v1控制器。不过需要注意的是:同一个控制器不能同时用于v1层次结构和v2层次结构中。

v2与v1的差异点包括下面这些:

  1. v2为所有挂载的控制器提供了一个统一的层次结构。
  2. 不允许出现”内部”进程。除了根cgroup以外,所有进程只允许出现在叶子节点上。
  3. 必须通过cgroup.controllerscgroup.subtree_control文件激活cgroup。
  4. tasks被移除。cpuset控制器使用的cgroup.clone_children文件也被移除了。
  5. 改进的空cgroup通知通过cgroup.events文件提供。

cgroup的v2包含的控制器:

名称 起始Linux版本 说明
io 4.5 v1 blkio 控制器的后继版本
memory 4.5 v1 memory 控制器的后继版本
pids 4.5 和v1 pids 控制器一样
perf_event 4.11 和v1 perf_event 控制器一样
rdma 4.11 和v1 rdma控制器一样
cpu 4.15 v1 cpu和cpuacct控制器的后继版本

systemd 与 cgroup

systemd 是Linux上新的初始化系统。

当前绝大多数的Linux发行版都已采用systemd,包括下面这些:

  • Fedora 15及后续版本
  • Mageia 2
  • Mandriva 2011
  • openSUSE 12.1 及后续版本
  • Red Hat Enterprise Linux 7及后续版本,包括其派生品CentOS、Scientific Linux、Oracle Linux等
  • Chakra GNU/Linux,在2012.10的光盘映像档发布后默认使用systemd
  • Debian GNU/Linux,在2014年的技术委员会的init系统投票中决定在Debian 8“Jessie”中以Linux为核心的版本转换到systemd
  • Ubuntu 15.04及后续版本

systemd中包含了对于cgroup的支持。在系统的开机阶段,systemd 会把支持的 控制器(subsystem 子系统)挂载到默认的 /sys/fs/cgroup/ 目录下面。

因此如果你使用了systemd,你不需要自己初始化cgroup了。

以Ubuntu 18.04为例,这个系统上的cgroup启用情况如下:

tmpfs /sys/fs/cgroup tmpfs ro,nosuid,nodev,noexec,mode=755 0 0
cgroup /sys/fs/cgroup/unified cgroup2 rw,nosuid,nodev,noexec,relatime,nsdelegate 0 0
cgroup /sys/fs/cgroup/systemd cgroup rw,nosuid,nodev,noexec,relatime,xattr,name=systemd 0 0
cgroup /sys/fs/cgroup/net_cls,net_prio cgroup rw,nosuid,nodev,noexec,relatime,net_cls,net_prio 0 0
cgroup /sys/fs/cgroup/pids cgroup rw,nosuid,nodev,noexec,relatime,pids 0 0
cgroup /sys/fs/cgroup/cpuset cgroup rw,nosuid,nodev,noexec,relatime,cpuset 0 0
cgroup /sys/fs/cgroup/cpu,cpuacct cgroup rw,nosuid,nodev,noexec,relatime,cpu,cpuacct 0 0
cgroup /sys/fs/cgroup/perf_event cgroup rw,nosuid,nodev,noexec,relatime,perf_event 0 0
cgroup /sys/fs/cgroup/hugetlb cgroup rw,nosuid,nodev,noexec,relatime,hugetlb 0 0
cgroup /sys/fs/cgroup/rdma cgroup rw,nosuid,nodev,noexec,relatime,rdma 0 0
cgroup /sys/fs/cgroup/memory cgroup rw,nosuid,nodev,noexec,relatime,memory 0 0
cgroup /sys/fs/cgroup/devices cgroup rw,nosuid,nodev,noexec,relatime,devices 0 0
cgroup /sys/fs/cgroup/blkio cgroup rw,nosuid,nodev,noexec,relatime,blkio 0 0
cgroup /sys/fs/cgroup/freezer cgroup rw,nosuid,nodev,noexec,relatime,freezer 0 0

针对systemd和cgroup,有以下两个命令会很有用:

  • systemd-cgls :以树状结构显示cgroup的详细状况。
  • systemd-cgtop:按资源使用情况显示top控制组。

很自然,systemd中提供了配置项用来设置cgroup资源,相关内容可以参阅这里:systemd.resource-control

Android的进程调度

Android是基于Linux内核的操作系统,因此其进程调度自然会使用上面提到的这些机制。

下面我们结合具体的设备和源码来分析一下。

以下内容以我手上的 Pixel XL 手机为例来进行分析。

只要是原生Android的系统,其基本机制就是一致的,不同的仅仅是参数的配置不一样而已。因此如果你拥有的是其他设备,也可以用同样的方法来分析。

当然你可以对比一下你设备的参数与Pixel XL有什么不同,以及为什么那样配置。想要更深入理解一项技术,我们不仅需要知道是什么,更需要知道为什么。

以下这些信息涉及系统最底层的配置,出于安全的考虑,系统有些信息是不允许普通用户查看的。因此,想要进行这些分析,你可能需要先 root 你的设备。

我手上这台设备的版本信息如下图所示:

pixel_xl.png

首先我们通过下面的命令来确认其使用的cgroup情况:

marlin:/ $ cat /proc/mounts  | grep cgroup                                                                                                                       
none /acct cgroup rw,nosuid,nodev,noexec,relatime,cpuacct 0 0
none /dev/stune cgroup rw,nosuid,nodev,noexec,relatime,schedtune 0 0
none /dev/cpuctl cgroup rw,nosuid,nodev,noexec,relatime,cpu 0 0
none /dev/cpuset cgroup rw,nosuid,nodev,noexec,relatime,cpuset,noprefix,release_agent=/sbin/cpuset_release_agent 0 0

从这个输出可以看出,这个设备的系统上:

  • 使用的是cgroup v1版本。
  • 使用了cpuacctschedtunecpucpuset四个控制器。它们mount的地址分别是:/acct/dev/stune/dev/cpuctl/dev/cpuset

这里的schedtune我们下面会提到,其他三个控制器前面已经都提到过。

除了上面这个命令,还可以通过cat /proc/cgroups来了解cgroup的情况。

marlin:/ # cat /proc/cgroups                                                                                                                                                                                                  
#subsys_name  hierarchy num_cgroups enabled
cpuset        4         14          1
cpu           3         1           1
cpuacct       1         249         1
schedtune     2         5           1
freezer       0         1           1
debug         0         1           1

对于这个文件的解释请参考这里:cgroups(7)

初始化cgroup

我们已经知道,在Android系统上,init进程负责了整个系统的启动逻辑。很自然的,init进程就要负责cgroup的初始化工作。

在根目录下的 /init.rc 文件包含了这部分配置:

如果你不理解这里的内容,请熟悉一下 Android Init Language,或者阅读我之前写过的文章:Android系统启动:init进程与init语言

on early-init
    # Mount cgroup mount point for cpu accounting
    mount cgroup none /acct nodev noexec nosuid cpuacct
    mkdir /acct/uid

    # root memory control cgroup, used by lmkd
    mkdir /dev/memcg 0700 root system
    mount cgroup none /dev/memcg nodev noexec nosuid memory
    # app mem cgroups, used by activity manager, lmkd and zygote
    mkdir /dev/memcg/apps/ 0755 system system
    # cgroup for system_server and surfaceflinger
    mkdir /dev/memcg/system 0550 system system
    ...
on init
    # Create energy-aware scheduler tuning nodes
    mkdir /dev/stune
    mount cgroup none /dev/stune nodev noexec nosuid schedtune
    mkdir /dev/stune/foreground
    mkdir /dev/stune/background
    mkdir /dev/stune/top-app
    mkdir /dev/stune/rt
    ...
    # Create cgroup mount points for process groups
    mkdir /dev/cpuctl
    mount cgroup none /dev/cpuctl nodev noexec nosuid cpu
    chown system system /dev/cpuctl
    chown system system /dev/cpuctl/tasks
    chmod 0666 /dev/cpuctl/tasks
    write /dev/cpuctl/cpu.rt_period_us 1000000
    write /dev/cpuctl/cpu.rt_runtime_us 950000

    # sets up initial cpusets for ActivityManager
    mkdir /dev/cpuset
    mount cpuset none /dev/cpuset nodev noexec nosuid
    
    # this ensures that the cpusets are present and usable, but the device's
    # init.rc must actually set the correct cpus
    mkdir /dev/cpuset/foreground
    copy /dev/cpuset/cpus /dev/cpuset/foreground/cpus
    copy /dev/cpuset/mems /dev/cpuset/foreground/mems
    mkdir /dev/cpuset/background
    copy /dev/cpuset/cpus /dev/cpuset/background/cpus
    copy /dev/cpuset/mems /dev/cpuset/background/mems

    # system-background is for system tasks that should only run on
    # little cores, not on bigs
    # to be used only by init, so don't change system-bg permissions
    mkdir /dev/cpuset/system-background
    copy /dev/cpuset/cpus /dev/cpuset/system-background/cpus
    copy /dev/cpuset/mems /dev/cpuset/system-background/mems

    # restricted is for system tasks that are being throttled
    # due to screen off.
    mkdir /dev/cpuset/restricted
    copy /dev/cpuset/cpus /dev/cpuset/restricted/cpus
    copy /dev/cpuset/mems /dev/cpuset/restricted/mems

    mkdir /dev/cpuset/top-app
    copy /dev/cpuset/cpus /dev/cpuset/top-app/cpus
    copy /dev/cpuset/mems /dev/cpuset/top-app/mems

这里挂载了cgroup子系统。并创建了一些分组。

libprocessgroup

为了将cgroup的处理逻辑集中管理,AOSP源码中,有一个库 libprocessgroup 专门负责相关工作。

这个库主要包含了以下几个功能:

  • 初始化cgroup (见下文)
  • 进程的CPU调度控制
  • cgroup 配置文件的读写
  • cgroup 配置参数调整

这个库主要被init进程和Process类使用。

CgroupSetup

前面我们已经看到,init.rc 中包含了cgroup的初始化工作。但实际上,libprocessgroup库提供的CgroupSetup接口也完成了相同的工作。

CgroupSetup接口会读取一个叫做cgroups.json的文件,并根据文件中的内容来配置cgroup。但在我的Pixel XL手机上,并没有cgroups.json这个文件,因此这里的初始化工作应该是没有使用到的。笔者觉得这可能是还在开发过程中,即:目前怀疑Android系统维护者计划将cgroup的初始化工作移到libprocessgroup中来完成,以减少init.rc中的配置。(至于这个猜想是否准确,等今后的版本更新就知道了。)

出于好奇心,我们可以大致来看一下这部分内容。

CgroupSetup函数中最主要的逻辑如下:

// load cgroups.json file
if (!ReadDescriptors(&descriptors)) {
    LOG(ERROR) << "Failed to load cgroup description file";
    return false;
}

// setup cgroups
for (auto& [name, descriptor] : descriptors) {
    if (SetupCgroup(descriptor)) {
        descriptor.set_mounted(true);
    } else {
        // issue a warning and proceed with the next cgroup
        LOG(WARNING) << "Failed to setup " << name << " cgroup";
    }
}

// mkdir <CGROUPS_RC_DIR> 0711 system system
if (!Mkdir(android::base::Dirname(CGROUPS_RC_PATH), 0711, "system", "system")) {
    LOG(ERROR) << "Failed to create directory for " << CGROUPS_RC_PATH << " file";
    return false;
}

这段代码应该很容易理解,就是读取配置文件,然后根据配置文件逐个设置每一个group。

AOSP中包含的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": "/dev/cg2_bpf",
    "Mode": "0600",
    "UID": "root",
    "GID": "root"
  }
}

很显然,这个文件既支持cgroup v1的配置,也支持cgroup v2的配置。这为今后cgroup的版本切换做好了准备。

CpuSets

cgroup的cpusets文档参见这里:ocumentation/cgroup-v1/cpusets.txt

在多CPU或者多核CPU的情况下,cpusets限制了进程使用的CPU范围。如果你仔细看了前面 /init.rc 中的配置,你就会发现,那里对cpuset做了一些具体的分组,包括:

  • foreground
  • background
  • top-app
  • system-background
  • restricted

很明显的,这里是在对进程的类型做分类。有了这个分类的基础框架,其他地方就可以将进程放入对应的分类组中,这样就达到的资源合理分配和限制的目的。而这也正是使用cgroup的原因。

不过,/init.rc 是为整个AOSP项目使用的。这里面自然不能包含针对某个具体设备的配置。所以上面这个配置只是创建了这些cgroup,并没有进行设置。而完成这个具体参数设置工作的任务就要让具体的设备厂商来完成,这就是 /vendor/etc/init/init.rc 这个文件的任务了。

我的Pixel XL设备上/vendor/etc/init/init.rc这个文件中的相关内容如下:

on property:sys.boot_completed=1
    ...

    # update cpusets now that boot is complete and we want better load balancing
    write /dev/cpuset/top-app/cpus 0-3
    write /dev/cpuset/foreground/cpus 0-2
    write /dev/cpuset/background/cpus 0
    write /dev/cpuset/system-background/cpus 0-2
    write /dev/cpuset/restricted/cpus 0-1

当然,如果你的设备或者版本和我不一样,这个文件的内容也会不一样。

Pixel XL的CPU是4核的。通过这个配置可以看到,top-app进程可以使用所有的CPU。但是background进程只能使用CPU0。其他类型的进程也有一定的限制。

如果你完整的查看了/vendor/etc/init/init.rc这个文件你就会发现,这个文件中对有cputsets的设置远不止这一处。一方面,在init的启动过程中,包含了好几个阶段,在不同的阶段对于CPU的限制是不一样的。另一方面,除了上面这些分组,系统内部还有一些其他分组,它们也被设置了cpusets。

在Android Framework中,AcitivtyManagerService会根据进程的状态来设置进程组。在Java中,ProcessList类中以下几个常量描述了进程组:

static final int SCHED_GROUP_BACKGROUND = 0;
static final int SCHED_GROUP_RESTRICTED = 1;
static final int SCHED_GROUP_DEFAULT = 2;
static final int SCHED_GROUP_TOP_APP = 3;
static final int SCHED_GROUP_TOP_APP_BOUND = 4;

除了这个类以外,android.os.Process(这个类下文还会提到)类中也包含了类似的常量。它们之间有一定的对应关系,会在不同的场景下使用。

public static final int THREAD_GROUP_BG_NONINTERACTIVE = 0;
private static final int THREAD_GROUP_FOREGROUND = 1;
public static final int THREAD_GROUP_SYSTEM = 2;
public static final int THREAD_GROUP_AUDIO_APP = 3;
public static final int THREAD_GROUP_AUDIO_SYS = 4;
public static final int THREAD_GROUP_TOP_APP = 5;
public static final int THREAD_GROUP_RT_APP = 6;
public static final int THREAD_GROUP_RESTRICTED = 7;

SchedTune

SchedTune是一项与CPU调频相关的性能提升技术,它实现为一个cgroup控制器。

这个控制器提供了一个名称为schedtune.boost的配置参数,运行时系统可以使用它来更改该组中的进程的调度方式。

每当调整这个参数的时候,它会使受影响的进程看起来比实际更重(或更轻)。如果一个组被提升了25%,那么调度程序将期望它使用的CPU时间比它实际上要多25%,并且CPU频率调控器将相应地对处理器提速。因此,以这种方式“提升”进程不会影响其调度优先级,但会影响其最终运行的CPU的速度。

SchedTune扩展仅适用于负载较轻的系统。当系统饱和时,SchedTune应当自动禁用。

Pixel XL上/vendor/etc/init/init.rc文件中的相关配置如下:

# set default schedTune value for foreground/top-app (only affects EAS)
write /dev/stune/foreground/schedtune.prefer_idle 1
write /dev/stune/top-app/schedtune.boost 10
write /dev/stune/top-app/schedtune.prefer_idle 1
write /dev/stune/rt/schedtune.boost 30
write /dev/stune/rt/schedtune.prefer_idle 1

可以看到,这里为rttop-app两个进程组设置了处理器提速。

schedtune.prefer_idle是一个标志位,它向调度器指示用户空间希望调度器更关注功耗或者更关注性能。当这个值设为1,表示希望调度器尽可能减少改组中进程唤醒延迟(倾向于性能)。

对SchedTune感兴趣的读者可以以下面的链接为起点继续探索:

Android系统服务

Android系统中包含了很多的系统服务,这些服务的进程通常是常驻的,对于这些服务进程的调度自然也需要进行管理,而这个管理工作主要由相应的init配置文件来完成。

因为这些进程都是由init进程启动,并且在启动的时候就会将自身放入到对应的进程组中。由于这些系统服务并不像应用进程那样有明显的状态变化,所以通常它们不会频繁的从一个进程组移动到另外一个进程组。

可以通过下面这条命令搜索相关内容,然后对你感兴趣的服务进程查看其配置:

AOSP$ grep -rIn "writepid" system
...
system/vold/vold.rc:6:    writepid /dev/cpuset/foreground/tasks
system/core/logd/logd.rc:11:    writepid /dev/cpuset/system-background/tasks
system/core/logd/logd.rc:18:    writepid /dev/cpuset/system-background/tasks
system/core/debuggerd/tombstoned/tombstoned.rc:11:    writepid /dev/cpuset/system-background/tasks
...
system/core/rootdir/init.zygote32_64.rc:15:    writepid /dev/cpuset/foreground/tasks
system/core/rootdir/init.zygote32_64.rc:25:    writepid /dev/cpuset/foreground/tasks
system/core/rootdir/init.zygote64_32.rc:15:    writepid /dev/cpuset/foreground/tasks
system/core/rootdir/init.zygote64_32.rc:25:    writepid /dev/cpuset/foreground/tasks
system/core/rootdir/init.zygote64.rc:15:    writepid /dev/cpuset/foreground/tasks
system/core/rootdir/init.zygote32.rc:15:    writepid /dev/cpuset/foreground/tasks
system/core/lmkd/lmkd.rc:8:    writepid /dev/cpuset/system-background/tasks
system/core/logcat/logcatd.rc:76:    writepid /dev/cpuset/system-background/tasks
system/core/storaged/storaged.rc:6:    writepid /dev/cpuset/system-background/tasks
system/core/llkd/llkd.rc:45:    writepid /dev/cpuset/system-background/tasks
system/core/llkd/llkd-debuggable.rc:19:    writepid /dev/cpuset/system-background/tasks
system/core/gatekeeperd/gatekeeperd.rc:4:    writepid /dev/cpuset/system-background/tasks
system/security/keystore/keystore.rc:5:    writepid /dev/cpuset/foreground/tasks
system/update_engine/update_engine.rc:5:    writepid /dev/cpuset/system-background/tasks
system/hwservicemanager/hwservicemanager.rc:10:    writepid /dev/cpuset/system-background/tasks
system/iorap/iorapd.rc:19:    writepid /dev/cpuset/system-background/tasks
system/extras/cppreopts/cppreopts.rc:21:    writepid /dev/cpuset/foreground/tasks
system/extras/perfprofd/perfprofd.rc:5:    writepid /dev/cpuset/system-background/tasks

Process类

Android SDK中包含了一个类用来描述系统中的进程,那就是 android.os.Process 。这个类虽然应用开发者也能访问的,但其中很多接口(包括常量)都通过 @hide 注解对应用开发者屏蔽了,因为这些内容是只能系统服务(主要是ActivityManagerService)使用。

这个类中包含了很多描述进程状态的常量。例如:各种类型的uid,线程优先级等。

也包括进程调度器:

public static final int SCHED_OTHER = 0;
public static final int SCHED_FIFO = 1;
public static final int SCHED_RR = 2;
public static final int SCHED_BATCH = 3;
public static final int SCHED_IDLE = 5;

当然,android.os.Process 中包含了调整进程调度策略的接口,如下:

public static final native void setThreadGroup(int tid, int group)
        throws IllegalArgumentException, SecurityException;
    
public static final native void setThreadGroupAndCpuset(int tid, int group)
        throws IllegalArgumentException, SecurityException;

public static final native void setProcessGroup(int pid, int group)
        throws IllegalArgumentException, SecurityException;

public static final native int getProcessGroup(int pid)
        throws IllegalArgumentException, SecurityException;

public static final native int[] getExclusiveCores();
    
public static final native void setThreadPriority(int priority)
        throws IllegalArgumentException, SecurityException;
    
public static final native int getThreadPriority(int tid)
        throws IllegalArgumentException;
    
public static final native int getThreadScheduler(int tid)
        throws IllegalArgumentException;
    
public static final native void setThreadScheduler(int tid, int policy, int priority)
        throws IllegalArgumentException;

这些接口都是native,因为它们的实现都在C++层,在 frameworks/base/core/jni/android_util_Process.cpp 文件中。

这里的实现就会调用到libprocessgroup中的接口。关于这部分内容就不贴出更多代码了,有兴趣的读者可以自行阅读这部分代码。

ActivityManagerService

ActivityManagerService 负责了所有应用进程的管理。

Android系统中的进程管理:进程的优先级 一文中,我们已经看到 ActivityManagerService 对于应用进程的优先级计算逻辑。

简单来说,ActivityManagerService 会根据进程中四大组件的状态来调整进程的优先级。

applyOomAdjLocked 方法中,会根据计算好的调度组进行调度的设置:

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) {
        try {
            // 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);
            }
        } catch (IllegalArgumentException e) {
            Slog.w(TAG,
                    "Failed to set scheduling policy, thread does not exist:\n"
                            + e);
        } catch (SecurityException e) {
            Slog.w(TAG, "Failed to set scheduling policy, not allowed:\n" + e);
        }
    } else {
        // Reset priority for top app UI and render threads
        setThreadPriority(app.pid, 0);
        if (app.renderThreadTid != 0) {
            setThreadPriority(app.renderThreadTid, 0);
        }
    }
}

这里的调用的几个方法都是前面提到的Process类中,ActivityManagerService 对它们进行了静态导入:

import static android.os.Process.setProcessGroup;
import static android.os.Process.setThreadPriority;
import static android.os.Process.setThreadScheduler;

setThreadPriority方法为例,其native方法实现如下:

void android_os_Process_setThreadPriority(JNIEnv* env, jobject clazz,
                                              jint pid, jint pri)
{
#if GUARD_THREAD_PRIORITY
    // if we're putting the current thread into the background, check the TLS
    // to make sure this thread isn't guarded.  If it is, raise an exception.
    if (pri >= ANDROID_PRIORITY_BACKGROUND) {
        if (pid == gettid()) {
            void* bgOk = pthread_getspecific(gBgKey);
            if (bgOk == ((void*)0xbaad)) {
                ALOGE("Thread marked fg-only put self in background!");
                jniThrowException(env, "java/lang/SecurityException", "May not put this thread into background");
                return;
            }
        }
    }
#endif

    int rc = androidSetThreadPriority(pid, pri);
    if (rc != 0) {
        if (rc == INVALID_OPERATION) {
            signalExceptionForPriorityError(env, errno, pid);
        } else {
            signalExceptionForGroupError(env, errno, pid);
        }
    }
}

这里的androidSetThreadPriority实现如下:

int androidSetThreadPriority(pid_t tid, int pri)
{
    int rc = 0;
    int lasterr = 0;

    if (pri >= ANDROID_PRIORITY_BACKGROUND) {
        rc = set_sched_policy(tid, SP_BACKGROUND);
    } else if (getpriority(PRIO_PROCESS, tid) >= ANDROID_PRIORITY_BACKGROUND) {
        rc = set_sched_policy(tid, SP_FOREGROUND);
    }

    if (rc) {
        lasterr = errno;
    }

    if (setpriority(PRIO_PROCESS, tid, pri) < 0) {
        rc = INVALID_OPERATION;
    } else {
        errno = lasterr;
    }

    return rc;
}

这里最终调用了到set_sched_policy,而set_sched_policy方法的实现就位于libprocessgroup中。

最后,我们通过一幅图概括一下这里的调用逻辑。

android-schedule.png

如上图所示,在Android系统中,进程调度主要涉及三层:

  • 系统服务层
    • init 进程是Android上所有其他进程的祖先。它负责cgroup的初始化和配置工作。
    • ActivityManagerService 负责管理所有应用进程的调度工作。
  • 共享库层
    • Process 类中包含了接口用来调整某个进程的调度策略。
    • libprocessgroup 库提供了针对cgroup的接口。
  • 内核层:Linux内核提供了最底层调度策略和cgroup的实现。

参考资料与推荐读物


About Joyk


Aggregate valuable and interesting links.
Joyk means Joy of geeK