JasonWang's Blog

Linux实时调度踩到的哪些坑

字数统计: 2.6k阅读时长: 10 min
2024/11/25

早期Linux内核的调度更多考虑的是系统调度的公平与吞吐量,对于实时性的支持并不友好。为了改善系统的响应时间,降低某些场景下实时任务的调度延迟,从2.6版本开始支持了实时调度与抢占功能,开发人员为此专门建立了一个实时Linux的网站,上面提供了实时内核的一些历史状态与补丁信息。实时调度对于音视频、UI渲染等对时间非常敏感的任务来说,非常必要。比如对于Android平台,会将音频、渲染相关的一些核心任务的调度策略设置为实时调度,这样可以减少系统调度延迟与任务抢占带来的延时。Linux内核中的实时调度主要有两种调度策略:

  • SCHED_FIFO: 先入先出,即优先级高的任务优先执行,不会被其他任务抢占,直到对应的任务阻塞或者主动释放CPU
  • SCHED_RR: 轮询(也称随机轮盘)调度,相同优先级的任务轮流执行相同的时间片,时间片用完后会调度其他的任务

本文基于Linux内核5.10版本分析

我们可以通过top -H命令查看系统实时任务的情况,其中PR列为RT的即为实时调度的任务。

1
2
3
4

1216 audioserver RT 0 119M 62M 10M S 4.6 0.5 0:08.70 DSP00Task0 android.hardware.audio.service
226 root RT 0 0 0 0 S 1.3 0.0 0:03.86 irq/135-asm330l [irq/135-asm330l]

尽管实时调度对于一些时间敏感的任务来说非常合适,但是对于一个多任务系统来说,如果系统中存在很多的进程,负载比较高,有可能会出现一些负面的效应;比较常见的问题有如下两类:

  • 实时进程会抢占其他非实时任务的CPU,长时间占据CPU,导致系统吞吐量下降,引起性能问题
  • 由于优先级设置不当,高优先级的实时任务会抢占低优先级实时任务的CPU,导致某些任务处理延迟

接下来我们就一起来看看这两类问题的表现,以及如何在实际开发中避免。在此之前,首先来简要的看一看Linux内核中实时调度策略的实现。

Linux内核的实时调度

除了常规的公平调度CFS(Complete Fair Scheduling)之外,Linux内核还支持两类实时调度类型:

  • 随机轮盘调度(Round-robinSCHED_RR):该调度策略的事实任务有固定的时间片(默认是100ms),任务执行完一段时间后,时间片减少;时间片用完后,进程换出,会放入到运行队列末尾,等待下一轮调度;这样确保相同优先级的任务可以轮流执行
  • 先进先出调度(First-In, First-Out, SCHED_FIFO):该调度策略没有时间片的限制,一旦调度执行会一直占用CPU;如果该任务的代码有问题导致阻塞,就可能出现CPU被长时间占用而无法换出的问题。

从内核代码可以看到,内核执行任务调度时,从高优先级调度类开始选择任务,再到低优先级调度类-实时调度类rt_sched_class高于公平调度类(SCHED_NORMALfair_sched_class

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24

// vmlinux.lds.h
/*
* The order of the sched class addresses are important, as they are
* used to determine the order of the priority of each sched class in
* relation to each other.
*/
#define SCHED_DATA \
STRUCT_ALIGN(); \
__begin_sched_classes = .; \
*(__idle_sched_class) \
*(__fair_sched_class) \
*(__rt_sched_class) \
*(__dl_sched_class) \
*(__stop_sched_class) \
__end_sched_classes = .;


#define sched_class_highest (__end_sched_classes - 1)
#define sched_class_lowest (__begin_sched_classes - 1)

#define for_each_class(class) \
for_class_range(class, sched_class_highest, sched_class_lowest)

Linux内核调度为每个调度类型都提供了一个关键的调度类,实时调度类如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38

DEFINE_SCHED_CLASS(rt) = {

.enqueue_task = enqueue_task_rt,
.dequeue_task = dequeue_task_rt,
.yield_task = yield_task_rt,

.check_preempt_curr = check_preempt_curr_rt,

.pick_next_task = pick_next_task_rt,
.put_prev_task = put_prev_task_rt,
.set_next_task = set_next_task_rt,

#ifdef CONFIG_SMP
.balance = balance_rt,
.select_task_rq = select_task_rq_rt,
.set_cpus_allowed = set_cpus_allowed_common,
.rq_online = rq_online_rt,
.rq_offline = rq_offline_rt,
.task_woken = task_woken_rt,
.switched_from = switched_from_rt,
.find_lock_rq = find_lock_lowest_rq,
#endif

.task_tick = task_tick_rt,

.get_rr_interval = get_rr_interval_rt,

.prio_changed = prio_changed_rt,
.switched_to = switched_to_rt,

.update_curr = update_curr_rt,

#ifdef CONFIG_UCLAMP_TASK
.uclamp_enabled = 1,
#endif
};

实时调度的调度队列是一个双向链表,所有优先级相同的任务都放入到active.queue[prio]这个队列里(实时调度的最大优先级MAX_RT_PRIO),active.bitmap用于记录哪个优先级对应的队列有任务;对实时调度实现原理感兴趣的可以研究下内核的代码kernel/sched/rt.c

1
2
3
4
5
6
7
8
9
10

struct rt_prio_array {
DECLARE_BITMAP(bitmap, MAX_RT_PRIO+1); /* include 1 bit for delimiter */
struct list_head queue[MAX_RT_PRIO];
};

struct rt_rq {
struct rt_prio_array active;
};

实时调度坑之一-实时任务长时间占据CPU

之前在一个项目开发过程中碰到一个问题:系统中一个跟摄像头相关的实时任务长时间占用了CPU0,持续运行了100ms+,而音频相关的软中断恰好也在CPU0上处理(物理中断默认绑定在CPU0上,对应的软中断会跟物理中断在同一个CPU上处理),导致音频的软中断无法抢占到CPU,发生响应延迟,导致音频卡顿。

RT线程执行时间过长

那么,软中断为啥没能竞争过用户空间的实时任务了?根因在于内核中的软中断softirqd线程创建时默认使用SCHED_NORMAL公平调度策略,因此优先级是低于实时调度(RT)的,这也能解释为为什么软中断无法抢占到CPU,导致音频卡顿。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48

# kthread.c
static int kthread(void *_create)
{
static const struct sched_param param = { .sched_priority = 0 };
/* Copy data: it's on kthread's stack */
struct kthread_create_info *create = _create;
int (*threadfn)(void *data) = create->threadfn;
void *data = create->data;
struct completion *done;
struct kthread *self;
int ret;

self = to_kthread(current);

/* Release the structure when caller killed by a fatal signal. */
done = xchg(&create->done, NULL);
if (!done) {
kfree(create->full_name);
kfree(create);
kthread_exit(-EINTR);
}

self->full_name = create->full_name;
self->threadfn = threadfn;
self->data = data;

/*
* The new thread inherited kthreadd's priority and CPU mask. Reset
* back to default in case they have been changed.
*/
sched_setscheduler_nocheck(current, SCHED_NORMAL, &param);
set_cpus_allowed_ptr(current, housekeeping_cpumask(HK_TYPE_KTHREAD));

/* OK, tell user we're spawned, wait for stop or wakeup */
__set_current_state(TASK_UNINTERRUPTIBLE);
create->result = current;
/*
* Thread is going to call schedule(), do not preempt it,
* or the creator may spend more time in wait_task_inactive().
*/
preempt_disable();
complete(done);
schedule_preempt_disabled();
preempt_enable();
...
}

这类问题要解决有两个方法,一个是直接将用户空间的线程调度策略设置为普通公平调度,一个是开启物理中断的CPU亲和性,确保软中断处理不绑定到特定的CPU上,从而错开与实时调度任务的执行,也可以将软中断设置为实时调度策略(这个影响较大,不推荐)。

实时调度坑之二-优先级设置不当

与问题一不一样的是,问题二是两个实时任务的竞争引起的音频卡顿的问题(Android中大部分的实时调度任务都是音频):一个应用进入前台后(Android中前台进程是top-app,绑定CPU0~3,我们开启了Android的一个特定sys.use_fifo_ui,会使得应用的UI线程使用实时调度策略),会偶现音频播放出现杂音。通过复现抓到的trace可以看到,内核音频线程(297)有好几处长时间的(大于13ms以上)休眠,此时同一个CPU0上运行的就是前台的任务的主线程6600,可以看到只有等主线程执行完成释放CPU0,音频内核线程297才会唤醒,而此时音频可能已经出现了丢帧,从而出现杂音的问题。

RT实时任务竞争CPU

那么,为啥同样是实时调度的任务,内核的音频线程没法抢占到CPU呢?从实时调度的原理来看,可以推测是内核实时线程的优先级低于前台任务的主线程,实际在设备确认发现,音频线程的优先级与主线程优先级恰好相等,都是98(实时线程的最大优先级是100)。

与上一个问题类似,要解决问题二,要么关闭Android的主线程优化sys.use_fifo_ui,将其设置为0,从而避开与内核音频线程的竞争;要么提高内核音频线程的优先级,在创建线程时将内核线程的nice值降低(优先级提高)。实测发现方案二有效,最终我们也采用了方案二来解决问题。

如何限制实时任务的执行时间

从实时调度的调度策略来看,如果实时进程的代码存在问题,就很有可能导致CPU长时间被占用,系统卡住。内核为了解决该问题,针对实时任务的执行时间进行限定。在proc目录下有两个参数:

  • sched_rt_period_us: 表示最大的调度时长(可以理解为100%的CPU带宽),大小范围从-1INT_MAX-1,默认是1s,
  • sched_rt_runtime_us: 表示实时任务最大可运行时长,默认是0.95s,表示实时任务可以使用0.95s的CPU时间,而其他调度类的进程可以使用余下的0.05s
1
2
3
4
5
6
7

# cat /proc/sys/kernel/sched_rt_period_us
1000000
# cat /proc/sys/kernel/sched_rt_runtime_us
950000


通过调节这两个参数,我们可以限制实时类调度任务的时间片分配,从而确保其他任务可以执行。除此之外,通过设定cgroup的配置CONFIG_RT_GROUP_SCHED,也可以通过控制分组来限定某些分组的实时任务占用的时间片;启动该配置后,在对应的cgroup目录可以看到如下配置:

1
2
3
4

cpu.rt_period_us


通过设定该参数,可以在不同的分组采用不一样的实时任务分配策略,确保某些分组比如后台的实时任务不长时间占用CPU,从而解决其他任务无法抢占到CPU的问题

总结

Linux这种通用的操作系统来说,进程的调度要考虑的因素非常多:既要考虑到低延迟的任务处理,降低响应延时,比如音频、UI的渲染,同时要考虑并发处理多个任务,保持系统的高吞吐量,这两个目标通常是相互冲突的,需要在仔细权衡。在使用实时调度策略的时候,我们还是要谨慎处理,避免实时任务竞争CPU引起的问题。

参考资料

原文作者:Jason Wang

更新日期:2024-12-04, 20:24:52

版权声明:本文采用知识共享署名-非商业性使用 4.0 国际许可协议进行许可

CATALOG
  1. 1. Linux内核的实时调度
  2. 2. 实时调度坑之一-实时任务长时间占据CPU
  3. 3. 实时调度坑之二-优先级设置不当
  4. 4. 如何限制实时任务的执行时间
  5. 5. 总结
  6. 6. 参考资料