调度关键路径里调整优先级导致hardlockup

发布于:2025-05-24 ⋅ 阅读:(18) ⋅ 点赞:(0)

一、背景

在之前的博客 内核调度代码关键路径下的低延迟唤醒_linux条件变量唤醒存在延迟-CSDN博客 里,我们讲到了在调度代码关键路径下我们得尤其注意一些函数的调用,比如在调度关键代码里调用wake_up的逻辑就可能造成系统卡死、假死甚至panic等一系列异常情况。在之前的博客 内核调度代码关键路径下的低延迟唤醒_linux条件变量唤醒存在延迟-CSDN博客 里的第三章里,我们给出了一个定制tasklet的实现,来解决在调度关键逻辑里低延迟的进行一些与调度相关逻辑的所改造的tasklet的schedule逻辑(具体细节见之前的博客)。

这篇博客里,我们讨论在调度关键路径里进行优先级调整的逻辑这么一个使用场景。之所以要做这么一个调度逻辑里的优先级调整是为了解决之前的博客 rt-linux下的底层锁依赖因cgroup cpu功能导致不相干进程的高时延问题-CSDN博客 里提到的底层锁依赖因cgroup cpu功能导致不相干进程的高时延问题。

这篇博客里,我们在第二章里会讲到直接在调度关键路径里调整优先级会造成hardlockup的问题及问题原理,所谓hardlockup也就是长时间关硬中断,通过watchdog会检测到并打印核上的堆栈。我们在第三章里会针对第二章里的错误实现给出改法。

关于硬中断关闭后的堆栈抓取方法,见之前的博客  硬中断关闭后的堆栈抓取方法_arm64是否支持hardlock-CSDN博客 ,里面有除了watchdog方式这种大粒度(秒级)的关硬中断检测的原理介绍,也有定制的小粒度(毫秒级)的关硬中断检测的方案和实现细节介绍。

二、调度关键路径里调整优先级导致死锁

2.1 call trace的调用链分析

触发watchdog的hardlockup的堆栈打印截图如下:

上图里可以看到是在调度关键代码里的put_prev_task_fair调用的put_prev_entitty里进行了同步的优先级调整逻辑(即调用sched_setscheduler_nocheck)导致了一个hardlockup。

可以从上图看到在sched_setscheduler_nocheck里会调用task_rq_lock,我们逐步分析一下。

如下图sched_setscheduler_nocheck先是调用了_sched_setscheduler:

_sched_setscheduler调用了__sched_setscheduler:

而__sched_setscheduler则会调用task_rq_lock函数:

上图里的task_rq_lock的第一个参数p就是传入给__sched_setscheduler的第一个参数p,即要调整的优先级的task_struct指针。

task_rq_lock函数里,需要占用两个锁,一个是task_struct的pi_lock锁,pi即priority inherit,优先级继承有关;另外一个锁是rq的锁,也就是task_struct所在的rq上的锁,如下图:

上面call trace里是因为什么锁导致的死锁呢?我们看一下下图里的offset:

4f0+70是560,也就是下图位置:

搜索代码:

raw_spin_lock_nested(&rq->__lock, subclass);

搜到唯一一处如下:

也就是和rq的lock有关,继续搜raw_spin_rq_lock_nested,搜到了如下图里的raw_spin_rq_lock:

而task_rq_lock里就是用了这个raw_spin_rq_lock这个接口:

所以该死锁是与这个rq的lock有关。

2.2 为什么rq的lock会死锁?

上面一节我们分析到了sched_setscheduler_nocheck在调整进程优先级时会调用task_rq_lock继而占用进程所在的rq上的rq锁。

要造成死锁,还得是之前的逻辑已经占用了rq锁才会造成死锁。

我们再看一下call trace:

我们看一下call trace里的__schedule函数的实现,可以很明显的看到它是占用了rq的lock的:

call trace里反应的内核调度动作就是在把prev的进程切出去,然后挑一个next来运行。

所以上图里prev = rq->curr;就是通过rq拿到当前的运行的进程也就是prev,在做prev进程切出去时需要占用rq的锁,我们从下图可以看到pick_next_task是在rq_lock之后执行的:

而pick_next_task里会调用put_prev_task:

而put_prev_task调用的是调度类里的put_prev_task的实现:

而fair调度类的put_prev_task的实现就是put_prev_task_fair:

put_prev_task_fair里会调用put_prev_entity:

下面我们只需要确定__schedule里的rq的lock是在pick_next_task之后才释放的就可以了。

可以从下图里看到,在__schedule函数里,执行pick_next_task之后,有一个判断逻辑,判断prev是不是next,不过,无论prev是不是next,如下图里可以看到rq的锁都会在这个if else被释放:

raw_spin_rq_unlock_irq(rq);会释放rq的锁,另外,context_switch里也会释放rq的锁,如下图,调用了finish_task_switch:

finish_task_switch调用了finish_lock_switch:

finish_lock_switch里有rq的解锁逻辑,如下:

三、如何调整逻辑防止死锁

我们这么修改逻辑,使用之前的博客 内核调度代码关键路径下的低延迟唤醒_linux条件变量唤醒存在延迟-CSDN博客 里 第三章 里说的方法,schedule一个定制的tasklet(这块细节参考之前的博客),然后在tasklet的回调函数里创建一个work,在work的回调逻辑里去做优先级调整设置。

为什么不在tasklet回调函数里直接做优先级调整设置呢?有两方面考虑,一方面,优先级调整设置可能会耗时较长,让软中断的处理比较耗时导致阻塞后面的软中断执行;另外一方面,软中断处理函数属于中断上下文,普通linux里软中断在硬中断之后及ksoftirqd里执行,rt-linux系统里,它是会在ksoftirqd、ktimer及其他任意一个进程在enable bh时立马执行的,它执行所属的环境情况比较多,这么做会带来一些不可预测的情况,而把它放在work里做,就如用户态执行chrt命令一样,在一个确定的可预测的一个进程上下文里执行,它所带来的可能的影响也是可预估的,效果也相对可控。


网站公告

今日签到

点亮在社区的每一天
去签到