早期Linux内核的调度更多考虑的是系统调度的公平与吞吐量,对于实时性的支持并不友好。为了改善系统的响应时间,降低某些场景下实时任务的调度延迟,从2.6
版本开始支持了实时调度与抢占功能,开发人员为此专门建立了一个实时Linux的网站,上面提供了实时内核的一些历史状态与补丁信息。实时调度对于音视频、UI渲染等对时间非常敏感的任务来说,非常必要。比如对于Android
平台,会将音频、渲染相关的一些核心任务的调度策略设置为实时调度,这样可以减少系统调度延迟与任务抢占带来的延时。Linux内核中的实时调度主要有两种调度策略:
SCHED_FIFO
: 先入先出,即优先级高的任务优先执行,不会被其他任务抢占,直到对应的任务阻塞或者主动释放CPUSCHED_RR
: 轮询(也称随机轮盘)调度,相同优先级的任务轮流执行相同的时间片,时间片用完后会调度其他的任务
本文基于Linux内核5.10版本分析
我们可以通过top -H
命令查看系统实时任务的情况,其中PR
列为RT
的即为实时调度的任务。
1 |
|
尽管实时调度对于一些时间敏感的任务来说非常合适,但是对于一个多任务系统来说,如果系统中存在很多的进程,负载比较高,有可能会出现一些负面的效应;比较常见的问题有如下两类:
- 实时进程会抢占其他非实时任务的CPU,长时间占据CPU,导致系统吞吐量下降,引起性能问题
- 由于优先级设置不当,高优先级的实时任务会抢占低优先级实时任务的CPU,导致某些任务处理延迟
接下来我们就一起来看看这两类问题的表现,以及如何在实际开发中避免。在此之前,首先来简要的看一看Linux内核中实时调度策略的实现。
Linux内核的实时调度
除了常规的公平调度CFS(Complete Fair Scheduling)
之外,Linux内核还支持两类实时调度类型:
- 随机轮盘调度(
Round-robin
,SCHED_RR
):该调度策略的事实任务有固定的时间片(默认是100ms
),任务执行完一段时间后,时间片减少;时间片用完后,进程换出,会放入到运行队列末尾,等待下一轮调度;这样确保相同优先级的任务可以轮流执行 - 先进先出调度(
First-In, First-Out
,SCHED_FIFO
):该调度策略没有时间片的限制,一旦调度执行会一直占用CPU;如果该任务的代码有问题导致阻塞,就可能出现CPU被长时间占用而无法换出的问题。
从内核代码可以看到,内核执行任务调度时,从高优先级调度类开始选择任务,再到低优先级调度类-实时调度类rt_sched_class
高于公平调度类(SCHED_NORMAL
)fair_sched_class
:
1 |
|
Linux内核调度为每个调度类型都提供了一个关键的调度类,实时调度类如下:
1 |
|
实时调度的调度队列是一个双向链表,所有优先级相同的任务都放入到active.queue[prio]
这个队列里(实时调度的最大优先级MAX_RT_PRIO
),active.bitmap
用于记录哪个优先级对应的队列有任务;对实时调度实现原理感兴趣的可以研究下内核的代码kernel/sched/rt.c
。
1 |
|
实时调度坑之一-实时任务长时间占据CPU
之前在一个项目开发过程中碰到一个问题:系统中一个跟摄像头相关的实时任务长时间占用了CPU0
,持续运行了100ms+
,而音频相关的软中断恰好也在CPU0
上处理(物理中断默认绑定在CPU0
上,对应的软中断会跟物理中断在同一个CPU上处理),导致音频的软中断无法抢占到CPU
,发生响应延迟,导致音频卡顿。
那么,软中断为啥没能竞争过用户空间的实时任务了?根因在于内核中的软中断softirqd
线程创建时默认使用SCHED_NORMAL
公平调度策略,因此优先级是低于实时调度(RT
)的,这也能解释为为什么软中断无法抢占到CPU
,导致音频卡顿。
1 |
|
这类问题要解决有两个方法,一个是直接将用户空间的线程调度策略设置为普通公平调度,一个是开启物理中断的CPU
亲和性,确保软中断处理不绑定到特定的CPU
上,从而错开与实时调度任务的执行,也可以将软中断设置为实时调度策略(这个影响较大,不推荐)。
实时调度坑之二-优先级设置不当
与问题一不一样的是,问题二是两个实时任务的竞争引起的音频卡顿的问题(Android
中大部分的实时调度任务都是音频):一个应用进入前台后(Android中前台进程是top-app
,绑定CPU0~3
,我们开启了Android的一个特定sys.use_fifo_ui
,会使得应用的UI
线程使用实时调度策略),会偶现音频播放出现杂音。通过复现抓到的trace
可以看到,内核音频线程(297
)有好几处长时间的(大于13ms
以上)休眠,此时同一个CPU0
上运行的就是前台的任务的主线程6600
,可以看到只有等主线程执行完成释放CPU0
,音频内核线程297
才会唤醒,而此时音频可能已经出现了丢帧,从而出现杂音的问题。
那么,为啥同样是实时调度的任务,内核的音频线程没法抢占到CPU
呢?从实时调度的原理来看,可以推测是内核实时线程的优先级低于前台任务的主线程,实际在设备确认发现,音频线程的优先级与主线程优先级恰好相等,都是98
(实时线程的最大优先级是100
)。
与上一个问题类似,要解决问题二,要么关闭Android
的主线程优化sys.use_fifo_ui
,将其设置为0
,从而避开与内核音频线程的竞争;要么提高内核音频线程的优先级,在创建线程时将内核线程的nice
值降低(优先级提高)。实测发现方案二有效,最终我们也采用了方案二来解决问题。
如何限制实时任务的执行时间
从实时调度的调度策略来看,如果实时进程的代码存在问题,就很有可能导致CPU长时间被占用,系统卡住。内核为了解决该问题,针对实时任务的执行时间进行限定。在proc
目录下有两个参数:
sched_rt_period_us
: 表示最大的调度时长(可以理解为100%的CPU带宽),大小范围从-1
到INT_MAX-1
,默认是1s,sched_rt_runtime_us
: 表示实时任务最大可运行时长,默认是0.95s,表示实时任务可以使用0.95s的CPU时间,而其他调度类的进程可以使用余下的0.05s
1 |
|
通过调节这两个参数,我们可以限制实时类调度任务的时间片分配,从而确保其他任务可以执行。除此之外,通过设定cgroup
的配置CONFIG_RT_GROUP_SCHED
,也可以通过控制分组来限定某些分组的实时任务占用的时间片;启动该配置后,在对应的cgroup目录可以看到如下配置:
1 |
|
通过设定该参数,可以在不同的分组采用不一样的实时任务分配策略,确保某些分组比如后台的实时任务不长时间占用CPU,从而解决其他任务无法抢占到CPU的问题
总结
对Linux
这种通用的操作系统来说,进程的调度要考虑的因素非常多:既要考虑到低延迟的任务处理,降低响应延时,比如音频、UI的渲染,同时要考虑并发处理多个任务,保持系统的高吞吐量,这两个目标通常是相互冲突的,需要在仔细权衡。在使用实时调度策略的时候,我们还是要谨慎处理,避免实时任务竞争CPU
引起的问题。