之前我写过一些文章讲解Android系统上的进程管理,那几篇文章主要是从ActivityManagerService的角度来讲解。而这篇文章,将从更底层,从Linux内核层的角度讲解Android系统对于进程的调度管理。
前言
之前我写过几篇文章讲解Android系统上的进程管理,包括:
那几篇文章主要是从ActivityManagerService的角度讲解。在这篇文章中,我们更深入一些。结合Linux内核,来看看Android系统对于进程的调度管理。
为了便于讲解,下文在相关内容中会贴出相应的源码,源码的版本如下:
- Linux内核的源码版本是:linux-5.1.12
- Android系统的源码取自:2019年6月25日AOSP最新代码
进程调度
进程调度是操作系统最核心的功能之一。
在现代的操作系统中(无论是个人电脑还是手机),同一时刻会有几十个甚至几百个进程同时运行。但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 | 查询指定线程的调度策略和调度参数 |
通过nice
,setpriority
,和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_RR
和SCHED_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_BATCH
或SCHED_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;
}
这些字段说明如下:
prio
,static_prio
,normal_prio
:描述了进程的优先级,它们之间存在互相的关联关系。rt_priority
:实时进程使用,描述进程的实时优先级。sched_class
:进程所属的调度器类。se
:调度的实体,既可能是一个线程,也可以是一组进程。policy
:所使用的调度策略,见下文。cpus_allowed
:允许运行的CPU。可以通过sched_setaffinity
来设置。pid
:进程的id。
调度策略
在目前的Linux内核中,一共有六种调度策略,它们可以分为三类:
- 普通调度策略:
SCHED_NORMAL
,SCHED_BATCH
,SCHED_IDLE
。 - 实时调度策略:
SCHED_FIFO
,SCHED_RR
。 - Deadline调度策略:
SCHED_DEADLINE
。
六种调度策略说明如下:
SCHED_NORMAL
:也叫SCHED_OTHER
。这是进程的默认调度策略,也就是时间共享策略。绝大部分进程都使用这个调度策略。SCHED_BATCH
:与SCHED_NORMAL
类似。不同的是,内核会认为该进程是CPU密集型,因此在调度会有小的惩罚。这种策略适用于那些非交互的后台进程。SCHED_IDLE
:最低优先级的调度策略,nice值不被考虑。因此它的调度将低于SCHED_NORMAL
和SCHED_BATCH
。SCHED_FIFO
:FIFO全称是First in-first out。这种策略不会使用时间片算法,在同优先级的情况下,会按照先进先出的方法按顺序执行。作为一种实时调度策略,属于该调度策略的进程会一直执行直到被IO阻塞或者被更高优先级的进程抢占。SCHED_RR
:RR的全称是Round-robin。这是对于SCHED_FIFO增强的实时策略,它使用了时间片共享的方式来调度进程。SCHED_DEADLINE
:指定了预计完成时间的调度策略,它拥有超过所有其他策略的最高优先级。
实时调度策略(SCHED_FIFO
,SCHED_RR
)的优先级始终高于普通调度策略(SCHED_NORMAL
, SCHED_BATCH
,SCHED_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]的范围内。
图示如下:
查看进程的调度信息
通过ps
命令,我们可以查看进程的调度信息。
不过在不同的系统上,该命令的参数不一样。
在Linux系统上,可以通过下面这条命令查看:
ps ax -o uname,pid,cls,pri,rtprio,cmd
-o
指定了输出的列。列名说明如下:
uname
: 进程所属的用户名称。pid
: 进程id。cls
:进程的调度策略。TS
表示SCHED_OTHER
,FF
表示SCHED_FIFO
,RR
表示SCHED_RR
,B
表示SCHED_BATCH
,IDL
表示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
子系统为例,其操作步骤如下:
mount -t tmpfs cgroup_root /sys/fs/cgroup
挂在cgroup根系统mkdir /sys/fs/cgroup/cpuset
创建cpuset子系统目录mount -t cgroup -o cpuset cpuset /sys/fs/cgroup/cpuset
挂载cpuset子系统- 启动任务的根进程
echo the_pid > /sys/fs/cgroup/cpuset/tasks
将任务根进程的pid写入子系统中- 由根进程执行任务或者
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的差异点包括下面这些:
- v2为所有挂载的控制器提供了一个统一的层次结构。
- 不允许出现”内部”进程。除了根cgroup以外,所有进程只允许出现在叶子节点上。
- 必须通过
cgroup.controllers
和cgroup.subtree_control
文件激活cgroup。 tasks
被移除。cpuset
控制器使用的cgroup.clone_children
文件也被移除了。- 改进的空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 你的设备。
我手上这台设备的版本信息如下图所示:
首先我们通过下面的命令来确认其使用的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版本。
- 使用了
cpuacct
,schedtune
,cpu
,cpuset
四个控制器。它们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
可以看到,这里为rt
和 top-app
两个进程组设置了处理器提速。
schedtune.prefer_idle
是一个标志位,它向调度器指示用户空间希望调度器更关注功耗或者更关注性能。当这个值设为1,表示希望调度器尽可能减少改组中进程唤醒延迟(倾向于性能)。
对SchedTune感兴趣的读者可以以下面的链接为起点继续探索:
- LKML: sched: Central, scheduler-driven, power-perfomance control
- Energy Aware Scheduling on Android
- sched/tune: add initial support for CGroups based boosting
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系统中,进程调度主要涉及三层:
- 系统服务层
init
进程是Android上所有其他进程的祖先。它负责cgroup的初始化和配置工作。ActivityManagerService
负责管理所有应用进程的调度工作。
- 共享库层
Process
类中包含了接口用来调整某个进程的调度策略。libprocessgroup
库提供了针对cgroup的接口。
- 内核层:Linux内核提供了最底层调度策略和cgroup的实现。
参考资料与推荐读物
- Linux Kernel Development
- Understanding the Linux Kernel
- Nikita Ishkov: A complete guide to Linux process scheduling
- $SCHED(7)$ Linux Programmer’s Manual
- Real-Time Linux Kernel Scheduler
- Modular Scheduler Core and Completely Fair Scheduler
- CS@CU: Linux Scheduler
- Better compute control for Android using SchedTune and SCHED_DEADLINE
- kernel doc » cgroup-v1
- Linux Kernel Doc » Control Group v2
- AOSP: Optimizing Boot Times
- CGROUPS(7) Linux Programmer’s Manual
- Understanding the new control groups API
- systemd.exec — Execution environment configuration
- Rami Rosen: Namespaces and cgroups, the Building Blocks of Linux containers
- LKML: sched: Central, scheduler-driven, power-perfomance control
- Energy Aware Scheduling on Android
- sched/tune: add initial support for CGroups based boosting
- Scheduling for Android devices
- ARM Community: cpufreq(DVFS)
- Linux的cgroup功能(二):资源限制cgroup v1和cgroup v2的详细介绍
- Cgroups 与 Systemd
- PS(1) User Commands