早期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,持续运行了100+ms,而音频相关的软中断恰好也在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的问题。另外,内核为了确保实时任务的低延迟,通过一个调度配置RT_RUNTIME_SHARE来开启各个CPU的时间片共享,就是说,如果当前实时任务队列的时间片用完后,可以向其他CPU借用时间片,从而保证该实时任务能执行,而不至于被其他常规的任务抢占CPU被阻塞。
总结
对Linux这种通用的操作系统来说,进程的调度要考虑的因素非常多:既要考虑到低延迟的任务处理,降低响应延时,比如音频、UI的渲染,同时要考虑并发处理多个任务,保持系统的高吞吐量,这两个目标通常是相互冲突的,需要在仔细权衡。在使用实时调度策略的时候,我们还是要谨慎处理,避免实时任务竞争CPU引起的问题。

