这是一篇非常好的关于线程调度的资料,翻译自shed
从Linux 2.6.23开始,默认的调度器为CFS,即"完全公平调度器"(Completely Fair Scheduler)。CFS调度器取代了之前的"O(1)"调度器。
CFS的实现细节可以参见sched-design-CFS。cgroup的CPU调度也属于CFS扩展的一部分。
内核模块使用调度器来决定下一个CPU时钟周期执行的线程。每个线程都包含一个调度策略以及一个静态的调度优先级sched_priority
,调度器根据系统上所有线程的调度策略和静态优先级来决定如何进行调度。
对于使用普通调度策略(SCHED_OTHER
, SCHED_IDLE
, SCHED_BATCH
)的线程来说,sched_priority
并不会影响调度结果,且必须设置为0。
对于使用实时策略(SCHED_FIFO
,SCHED_RR
)的进程,其sched_priority
取值为1到99(1为最低值)。实时线程的调度优先级总是高于普通线程。注:POSIX.1的系统在实现中,会要求实时调度策略有32个优先级设置,因此,为了可移植性,可以使用sched_get_priority_min和sched_get_priority_max来查找调度策略所支持的优先级范围。
调度器会为每个sched_priority
值维护一个可运行的线程列表。调度器通过查看非空且静态优先级最高的列表,并选择该列表首部的元素作为下一个运行的线程。
线程的调度策略决定了如何根据静态优先级来将一个线程插入到同静态优先级的线程列表(list of runnable threads)中,以及如何在该列表中调整线程的位置。
所有的调度都具有抢占性:如果一个具有更高静态优先级的线程准备运行,当前运行的线程会被抢占并返回到其静态优先级对应的等待列表中。调度策略仅根据具有相同静态优先级的可运行线程列表来决定调度顺序。
进程调度中使用了2个队列:进程一开始会进入ready队列等待调度;当进程执行中遇到I/O阻塞,等待子进程结束或软中断等原因会进入wait队列,等阻塞结束后会返回到ready队列
SCHED_FIFO
仅适用于静态优先级大于0的线程,即当一个SCHED_FIFO
的线程变为可运行(runnable)状态时,它会立即抢占所有当前运行的SCHED_OTHER
, SCHED_BATCH
或SCHED_IDLE
线程。SCHED_FIFO
不使用时间片进行调度,所有使用SCHED_FIFO
调度策略的线程应该遵守如下规则:
SCHED_FIFO
线程被其他有更高优先级的线程抢占后,该线程会返回到其优先级对应的列表的首部,当所有更高优先级的线程阻塞后,该线程将会立即恢复运行;SCHED_FIFO
线程变为可运行状态时,该线程会返回到其优先级对应的列表末尾;SCHED_FIFO
线程的优先级时,该线程在列表中的位置取决于优先级的变动:SCHED_FIFO
线程将会一直运行,直到被更高优先级的线程抢占,或调用了sched_yield(2) 。
SCHED_RR
对SCHED_FIFO
做了简单增强。除每个线程仅允许运行在一个最大时间段下外,SCHED_FIFO
中的所有规则都适用于SCHED_RR
。如果一个SCHED_RR
线程已经运行了等于或大于该最大时间段时,该线程会被放置到其优先级列表的末尾。当一个SCHED_RR
线程被更高优先级的线程抢占,并在后续恢复运行后,会在先前未过期的时间段下运行。最大时间段可以通过sched_rr_get_interval(2)获得。
3.14版本之后的Linux提供了一个新的调度策略SCHED_DEADLINE
。该策略结合了GEDF(Global Earliest Deadline First)和 CBS (Constant Bandwidth Server)。必须通sched_setattr(2)和sched_getattr(2)来设置和获取该策略。
一个Sporadic task被定义为一系列任务,且每个任务每次仅激活一次。每个任务都有一个relative deadline
(该任务应该在该相对时间前停止运行),以及一个computation time
(执行该任务需要的CPU时间,对应下图的comp. time
)。一个新的任务开始执行时会唤醒(wakeup)一个Sporadic task,该时间点被称为arrival time
,start time
为一个任务开始执行的时间,absolute deadline
(绝对截止时间)为arrival time
加上relative deadline
的时间点。
arrival/wakeup absolute deadline
| start time |
| | |
v v v
-----x--------xooooooooooooooooo--------x--------x---
|<- comp. time ->|
|<------- relative deadline ------>|
|<-------------- period ------------------->|
当使用sched_setattr(2)给一个线程设置SCHED_DEADLINE
策略时,可以设置3个参数:Runtime
, Dead‐line
和Period
,对于上面提到的场景来说,通常的做法是将Runtime
设置为大于平均计算时间的值(或更坏的场景下,设置为硬实时任务的执行时间);将Deadline
设置为对应的dead-line,将Period
设置为任务的周期,此时对于SCHED_DEADLINE
的调度如下:
Runtime
对应上图中的comp.time
,Dead-line
对应上图的relative deadline
arrival/wakeup absolute deadline
| start time |
| | |
v v v
-----x--------xooooooooooooooooo--------x--------x---
|<-- Runtime ------->|
|<----------- Deadline ----------->|
|<-------------- Period ------------------->|
3个deadline调度参数对应sched_attr
结构体中的sched_run‐time
, sched_deadline
, 和sched_period
字段,参见sched_setattr(2)。这些字段的单位为纳秒。如果sched_period
的值为0,则它与sched_deadline
相同。
内核要求:
sched_runtime <= sched_deadline <= sched_period
此外,在当前实现中,所有参数的值至少为1024(即,大于1微秒),小于2^63。如果有效性校验失败,sched_setattr(2)返回EINVAL错误。
CBS通过阻止线程超出其运行时间Runtime
来保证任务间不互相干扰。
为了确保deadline调度,当SCHED_DEADLINE
线程在给定的条件下不可运行时,此时内核必须阻止这些线程的运行。内核必须在设置或修改SCHED_DEADLINE
策略和属性时执行准入测试。准入测试用于计算这些修改是否可行,如果不可行,sched_setattr(2)将返回EBUSY错误。
例如,总的CPU利用率应该小于或等于总的可用的CPU。由于每个线程可以在每个Period
中最大运行Runtime
时间,线程的CPU时间片使用率为Runtime
除以Period
。
为了满足SCHED_DEADLINE
的条件,使用SCHED_DEADLINE策略的线程的优先级是系统中最高的。当一个SCHED_DEADLINE
线程运行时,该线程会抢占其他策略下调度的线程。
对SCHED_DEADLINE
策略调度的线程调用fork(2)会返回EAGAIN错误(除非该线程设置了reset-on-fork标记)。
当一个SCHED_DEADLINE
线程调用了sched_yield(2)将会停止当前任务,并等待新的周期。
SCHED_OTHER
只能在静态优先级为0时使用(普通线程)。SCHED_OTHER
是标准的Linux分时调度策略(不需要实时机制)。
如何从静态优先级为0的列表中选择运行的线程取决于列表中的dynamic
优先级。dynamic
优先级基于nice值,且在每次线程准备运行时增加。这种机制保证公平处理所有的SCHED_OTHER
线程。
在Linux内核源码中,SCHED_OTHER
被称为SCHED_NORMAL
。
nice值用于影响CPU调度器对进程的调度偏好。适用于SCHED_OTHER和SCHED_BATCH调度处理。可以通过nice(2),setpriority(2)或sched_setattr(2)修改nice值。
根据POSIX.1,nice值是一个单进程属性,即进程中的所有线程共享一个nice值。然而,在Linux中,nice值是一个单线程属性,相同进程中的不同线程可能使用不同的nice值。
nice值的取值范围根据UNIX系统的不同而不同。在现代Linux系统中,取值为-20(高优先级)到+19(低优先级),而一些系统中的取值为-20..20。在一些非常早期的Linux 内核(Linux 2.0之前)中的取值为-infinity..15。
nice值对相应的SCHED_OTHER
进程的影响根据UNIX系统和Linux内核版本的不同而不同。
2.6.23版本的Linux内核中引入了CFS调度,并采用了一种能根据nice的差值产生更显著影响的算法。在当前的实现下,两个进程的nice差值中,每单位的nice差值对CFS调度的影响因子为1.25 (参见how-is-nice-working,CFS根据vruntime进行CPU调度:vruntime = 实际运行时间 * 1024 / 进程权重
,进程权重
为1.25 ^ nice_value
)。这种算法使得在有高优先级负载运行的情况下,只能给低nice值(+19)的负载提供很小的CPU;而为高nice值(-20)的负载提供其运行应用需要的绝大部分CPU(如音频应用)。
/proc//sched
中的se.vruntime
选项查看:grep vruntime /proc//sched
。vruntime的增加与进程占用的CPU有关,如果一个线程一直处于sleep状态,其vruntime是不会增加的。那么如果一个sleep的线程被唤醒之后,是否会立即抢占vruntime比它大的线程?答案是否定的,其最少需要在cpu的run队列中等待sched_min_granularity_ns的时间,sched_min_granularity_ns的计算方式如下:If number of runnable tasks does not exceed sched_latency_ns/sched_min_granularity_ns
scheduler period = sched_latency_ns
else
scheduler period = number_of_running_tasks * sched_min_granularity_ns
top
命令查看系统上的nice值和优先级。如下PR
表示优先级,NI
表示nice值,前者为内核角度看的进程的实际优先级,后者为用户空间看到的进程的nice值。两者的关系为:PR = 20 + NI
可以使用top命令查看系统上的nice值和优先级。如下PR表示优先级,NI表示nice值,前者为内核角度看的进程的实际优先级,后者为用户空间看到的进程的nice值。两者的关系为:PR = 20 + NI
PID USER PR NI VIRT RES SHR S %CPU %MEM TIME+ COMMAND
1145 root 20 0 602236 60888 25096 S 0.7 1.2 0:09.70 containerd
61 root 20 0 0 0 0 I 0.3 0.0 0:02.91 kworker/1:1-eve
1 root 20 0 193736 8288 5644 S 0.0 0.2 0:01.15 systemd
2 root 20 0 0 0 0 S 0.0 0.0 0:00.01 kthreadd
3 root 0 -20 0 0 0 I 0.0 0.0 0:00.00 rcu_gp
4 root 0 -20 0 0 0 I 0.0 0.0 0:00.00 rcu_par_gp
5 root 20 0 0 0 0 I 0.0 0.0 0:00.23 kworker/0:0-ata
6 root 0 -20 0 0 0 I 0.0 0.0 0:00.00 kworker/0:0H-ev
7 root 20 0 0 0 0 I 0.0 0.0 0:00.17 kworker/u256:0-
8 root 0 -20 0 0 0 I 0.0 0.0 0:00.00 mm_percpu_wq
9 root 20 0 0 0 0 S 0.0 0.0 0:00.00 ksoftirqd/0
10 root 20 0 0 0 0 I 0.0 0.0 0:00.42 rcu_sched
11 root rt 0 0 0 0 S 0.0 0.0 0:00.00 migration/0
...
Linux系统可以使用RLIMIT_NICE资源来限制非特权进程的nice值的上限,参见setrlimit(2)。
更多nice值的用法,参见下面的autogroup。
从Linux 2.6.16开始,SCHED_BATCH
可以用于静态优先级为0的线程。该策略类似SCHED_OTHER
,并根据动态优先级(nice值)进行调度。区别是使用该策略时,调度器会假设线程是CPU密集型的,因此,该调度器会根据线程的唤醒行为施加调度惩罚,因此这种调度策略比较不受欢迎。
该策略比较适用于非交互且不期望降低nice值的负载,以及需要不因为交互而(在负载之间)造成额外抢占的调度策略的负载。下面引用自PHP-FPM on Linux, SCHED_BATCH or SCHED_OTHER?,更多参见[batch/idle priority scheduling, SCHED_BATCH](batch/idle priority scheduling, SCHED_BATCH)
SCHED_BATCH was clearly designed for very long running (hours or even days) compute-intensive jobs. Your jobs are only compute-intensive for seconds or fractions of seconds.
This pretty much makes it a no-go for a web server. And it would be worse if the database is on the same machine, as they might contend for one of those extra-long timeslices.
从Linux 2.6.23开始,SCHED_IDLE
可以用于静态优先级为0的线程。nice值不会影响该策略。
该策略用于运行非常低优先级的任务(低于nice值为+19的SCHED_OTHER
或SCHED_BATCH
策略)。
每个线程都有一个reset-on-fork调度标识。当设置该标识后,使用fork(2)创建的子进程不会继承特权调度策略。可以通过如下方式设置reset-on-fork:
policy
参数,或attr.sched_flags
注意上面两个函数的常量名称不一样。使用sched_getscheduler(2)和sched_getattr(2)获取reset-on-fork状态的用法与上面类似。
reset-on-fork特性用于媒体播放的应用,可以防止应用在创建多个子进程时规避RLIMIT_RTTIME设置的资源限制。
更精确地讲,如果设置了reset-on-fork,后续创建地子进程会遵循下面规则:
SCHED_FIFO
或SCHED_RR
调度策略,子进程地策略或被设置为SCHED_OTHER
;在设置reset-on-fork之后,只有线程拥有CAP_SYS_NICE的capability时才能重置reset-on-fork。使用fork(2)创建的子进程会disable reset-on-fork标识。
在Linux 2.6.12之前,只有拥有特权(CAP_SYS_NICE)的线程才能设置非0的静态优先级(即设置实时调度策略)。后续版本对如下实现进行了修改:非特权的线程仅在调用者的effective user ID(EID)与目标线程的real或effective user ID相同的情况下才能且仅能设置SCHED_OTHER策略。
为了设置或修改SCHED_DEADLINE
策略。线程必须是特权(CAP_SYS_NICE)的。
从Linux 2.6.12开始,RLIMIT_RTPRIO(可以使用ulimit -e设置)资源限制定义了非特权线程设置SCHED_RR 和SCHED_FIFIO策略的静态优先级的上限。修改调度策略和优先级的规则如下:
特权(CAP_SYS_NICE)线程会忽略RLIMIT_RTPRIO限制。在一些老的内核中,特权线程可以任意修改策略和优先级。参见 getrlimit(2)获取更多信息。
SCHED_FIFO, SCHED_RR或SCHED_DEADLINE策略下调度的线程中的非阻塞无限循环处理可能会阻塞其他线程获取CPU。在Linux 2.6.25之前,阻止实时进程冻结系统的唯一方式是通过shell启动一个静态优先级更高的程序,如通过这种方式来停止实施程序,并释放CPU资源。
从Linux 2.6.25开始,引进了其他技术手段来处理实时(SCHED_FIFO,SCHED_RR)和deadline(SCHED_DEADLINE)进程。一种方式是通过RLIMIT_RTTIME 来限制实时进程可能使用到的CPU的上限。参见 getrlimit(2)获取更多信息。
从Linux 2.6.25开始,Linux提供了2个/proc
文件来为非实时进程保留CPU时间。保留的CPU也可以为shell预留资源来停止正在允许的进程。两个文件中的值对应的单位为微秒:
/proc/sys/kernel/sched_rt_period_us
/proc/sys/kernel/sched_rt_runtime_us
sched_rt_period_us
使用一个阻塞的高优先级的线程(在调度前)等待I/O时会有一个确定的响应时间。设备驱动作者可以使用"slow interrupt"中断句柄来减少响应时间
子进程会通过fork(2)继承调度策略和参数。可以使用execve(2)来保存调度策略和参数。
实时进程通常会使用memory locking特性来防止内存页的延迟。可以使用mlock(2) 或mlockall(2)设置memory locking。
从Linux 2.6.38开始,内核提供了一种被称为autogrouping的特性来为多进程和CPU密集型负载(如Linux内核中的大量并行进程)提升交互式桌面性能。
该特性结合CFS调度策略,需要内核设置CONFIG_SCHED_AUTOGROUP
。在一个运行的系统中,该特性可以通过文件/proc/sys/kernel/sched_autogroup_enabled
使能或去使能,值0表示去使能,1表示使能。默认值为1(除非内核使用noautogroup
参数启动内核)。
当通过setsid(2) (setsid会将一个进程脱离父进程)创建一个新的会话时会创建一个新的autogroup,这种情况可能发生在一个新的终端窗口启动时。使用fork(2)创建的进程会继承父辈的autogroup成员。因此,一个会话中的所有进程都属于同一个autogroup。当最后一个进程结束后,autogroup会被自动销毁。
当使能autogrouping时,一个autogroup中的所有成员都属于同一个内核调度器"任务组"。CFS调度器使用了在任务组间均衡分配CPU时钟周期的算法。可以使用下面例子进行展示提升交互式桌面性能的好处。
假设有2个竞争相同CPU的autogroup(即,单核系统或使用taskset设置所有SMP系统的进程使用相同的CPU),第一个group包含10个用于构建内核的CPU密集型进程make -j10CPU
;另外一个包含一个CPU密集型的视频播放器进程。autogrouping的影响为:每个group各自分配到一半的CPU时钟周期,即视频播放器会分配到50%的CPU时钟周期,而非9%的时钟周期(该情况下可能会导致降低视频播放质量)。在SMP系统上会更加复杂,但整体的表现是一样的:调度器会在任务组之间分配CPU时钟周期,包含大量CPU密集型进程的autogroup并不会以牺牲系统上的其他任务为代价占用CPU周期。
进程的autogroup成员可以通过/proc/[pid]/autogroup
查看:(下面进程隶属于autogroup-1,autogroup-1的nice值为0)
$ cat /proc/1/autogroup
/autogroup-1 nice 0
该文件可以通过为autogroup设置nice
值来修改分配给一个autogroup的CPU带宽(bandwidth,即可使用的CPU时间),nice值范围为+19(低优先级)到-20(高优先级),设置越界的值会导致write(2)返回EINVAL错误。
autogroup的nice值的意义与进程的nice值意义相同,区别是前者为将autogroup作为一个整体,并基于相对其他autogroups设置的nice值来分配CPU时钟周期。对于一个autogroup内的进程,其CPU时钟周期为autogroup(相对于其他autogroups)的nice值和进程的nice值(相对于其他进程)的产物(即首先根据autogroup的nice值计算该autogroup所占用的CPU,然后根据进程的nice值计算该进程所占用的(属于其autogroup的)CPU)。
可以使用cgroups(7) CPU控制器来设置(非root CPU cgroup的)cgroups中的进程所占用的CPU,该设置会覆盖掉autogrouping。
所有的cgroup都由可选择的内核配置CONFIG_CGROUPS控制。在Linux 3.2引入了CPU带宽控制(bandwidth control)。cgroup对CFS的扩展有如下三种:
CONFIG_CGROUP_SCHED :运行任务以组的方式(在组之间)公平使用CPU
CONFIG_RT_GROUP_SCHED :用于支持实时任务组(SCHED_FIFO和SCHED_RR)
CONFIG_FAIR_GROUP_SCHED :用于支持CFS任务组(SCHED_NORMAL和SCHED_BATCH)
可以用如下方式查看是否启用了cgroup
# zgrep -i cgroup /boot/config-3.10.0-693.el7.x86_64
CONFIG_CGROUPS=y
# CONFIG_CGROUP_DEBUG is not set
CONFIG_CGROUP_FREEZER=y
CONFIG_CGROUP_PIDS=y
CONFIG_CGROUP_DEVICE=y
CONFIG_CGROUP_CPUACCT=y
CONFIG_CGROUP_HUGETLB=y
CONFIG_CGROUP_PERF=y
CONFIG_CGROUP_SCHED=y
CONFIG_BLK_CGROUP=y
...
autogroup特性仅用于非实时调度策略(SCHED_OTHER, SCHED_BATCH和SCHED_IDLE)。它不会为实时和deadline策略分组。
当调度非实时进程时,CFS调度器会使用一种称为"group scheduling"的技术(如果内核设置了CONFIG_FAIR_GROUP_SCHED
选项)
在group scheduling下,进程以"任务组"方式进行调度。任务组间有继承关系,会继承系统上被称为"root任务组"的初始化任务组。任务组遵循以下条件(按顺序):
在group调度下,线程的nice值仅会影响到相同任务组的其他线程的调度。这会在一些使用传统nice语义的UNIX系统上会导致惊人的后果。实践中,如果使能了autogrouping,则会使用setpriority(2)或nice(1)来影响相同会话(通常为相同的终端窗口)中的一个进程相对于其他进程的调度。
相反的,对于不同会话(如,不同的终端窗口,这些任务都绑定到不同的autogroups)中绑定了唯一的CPU的2个进程,修改一个会话中的进程的nice值不会影响其他会话中的进程的调度。使用如下命令可以修改一个终端会话中所有进程对于的autogroup nice值。
$ echo 10 > /proc/self/autogroup
autogroup和进程都有一个nice值,autogroup的nice值用于在autogroup之间分配CPU;autogroup内的进程的nice值用于在进程间分配autogroup的CPU。cgroup的配置会覆盖autogroup
从Linux 2.6.18开始,Linux逐渐具备实时功能,其中大部分来源于realtime-preempt
补丁集。在这些补丁最终合并到内核主线之前,它们必须通过安装才能达到实时性能。这些补丁命名为:
patch-kernelversion-rtpatchversion
如果没有补丁且在这些补丁完全合并到主线之前的内核提供了3中抢占类,CONFIG_PREEMPT_NONE,CONFIG_PREEMPT_VOLUNTARY和CONFIG_PREEMPT_DESKTOP,分别表示没有,部分,和考虑降低最坏情况下的调度延迟。
如果没有补丁且在这些补丁完全合并到主线之前的内核还提供了额外的配置表项CONFIG_PREEMPT_RT,如果选择该表项,Linux会转变为正式的实时操作系统。FIFO和RR调度策略会用于运行具有实时优先级且最小调度延迟的线程。
ps -eLfc
可以在CLS
一栏中查看进程的调度策略,最下面为最新内核中定义的调度策略(5.5)TS SCHED_OTHER
FF SCHED_FIFO
RR SCHED_RR
B SCHED_BATCH
ISO SCHED_ISO
IDL SCHED_IDLE
/*
* Scheduling policies
*/
#define SCHED_NORMAL 0
#define SCHED_FIFO 1
#define SCHED_RR 2
#define SCHED_BATCH 3
/* SCHED_ISO: reserved but not implemented yet */
#define SCHED_IDLE 5
#define SCHED_DEADLINE 6
可以在/proc/sched_debug中查看各个cpu core的调度情况,其中包含每个cgroup 服务的调度情况,如下面为docker服务在core1上的调度情况
cfs_rq[1]:/system.slice/docker.service
.exec_clock : 0.000000
.MIN_vruntime : 0.000001
.min_vruntime : 2723538.752185
.max_vruntime : 0.000001
.spread : 0.000000
.spread0 : -780278343.308552
.nr_spread_over : 0
.nr_running : 0
.load : 0
.runnable_load_avg : 0
.blocked_load_avg : 1
.tg_load_avg : 1
.tg_load_contrib : 1
.tg_runnable_contrib : 3
.tg->runnable_avg : 8
.tg->cfs_bandwidth.timer_active: 0
.throttled : 0
.throttle_count : 0
.se->exec_start : 7432565479.124290
.se->vruntime : 560032308.234830
.se->sum_exec_runtime : 7762399.141979
.se->load.weight : 2
.se->avg.runnable_avg_sum : 147
.se->avg.runnable_avg_period : 47729
.se->avg.load_avg_contrib : 1
.se->avg.decay_count : 7088246803
可以在/proc/$pid/sched中查看特定进程的调度情况
# cat sched
docker-proxy-cu (77992, #threads: 8)
-------------------------------------------------------------------
se.exec_start : 7179182946.125343
se.vruntime : 1843988.364695
se.sum_exec_runtime : 6.017643
se.nr_migrations : 2
nr_switches : 6
nr_voluntary_switches : 4
nr_involuntary_switches : 2
se.load.weight : 1024
policy : 0
prio : 120
clock-delta : 34
mm->numa_scan_seq : 0
numa_migrations, 0
numa_faults_memory, 0, 0, 1, 0, -1
numa_faults_memory, 1, 0, 0, 0, -1
voluntary_ctxt_switches: 1
nonvoluntary_ctxt_switches: 10
cpu.cfs_period_us
定义,默认100m。意味着,如果cpu.cfs_period_us
为100m,则1CPU就是100ms,而2500m CPU就是250ms,CFS会将一个周期切分为多个时间片,最小时间片由sched_cfs_bandwidth_slice_us
定义,默认5ms。原文作者:charlieroro
原文地址:linux线程调度策略 - charlieroro - 博客园(版权归原文作者所有,侵权留言联系删除)