rt调度器bug导致宕机的排查

  1. 背景
  2. 排查

背景

​ 在测试系统抖动时候,我们有时候会使用cyclictest这个工具。

​ 但是,在Ubuntu22上测试的时候几乎每次都会宕机,其他系统上没有出现这个问题

​ 经过排查,最终确定与内核rt调度器的一个bug有关

排查

首先我们当前已知的触发宕机的条件是:

  • 系统上有multipathd这个进程(这是一个io多路径的进程,用于一个设备上路径损坏的时候可以使用另一个路径)

  • 系统上运行cyclictest

首先根据宕机的栈我们可以确定是softlockup引起的宕机,而且每次宕机都是宕在multipathd这里,所以可以明确的是宕机由multipathd引起的,kill掉multipathd进程就不会出现这个问题了。

那么为什么加载了multipathd服务就会出问题?继续排查

crash分析一下vmcore,首先虽然每次core在不同的地方,但是我们可以明确的是都是core在multipathd这个进程

看一下multipathd进程共有7个线程

root@XXX:~# pidstat -p 4087 -t
Linux 5.15.0-72-generic (XXX) 	01/20/2025 	_x86_64_	(384 CPU)

11:31:06 AM   UID      TGID       TID    %usr %system  %guest   %wait    %CPU   CPU  Command
11:31:06 AM     0      4087         -    0.00    0.00    0.00    0.00    0.00   279  multipathd
11:31:06 AM     0         -      4087    0.00    0.00    0.00    0.00    0.00   279  |__multipathd
11:31:06 AM     0         -      4097    0.00    0.00    0.00    0.00    0.00   287  |__multipathd
11:31:06 AM     0         -      4098    0.00    0.00    0.00    0.00    0.00    87  |__multipathd
11:31:06 AM     0         -      4099    0.00    0.00    0.00    0.00    0.00    87  |__multipathd
11:31:06 AM     0         -      4100    0.00    0.00    0.00    0.00    0.00   272  |__multipathd
11:31:06 AM     0         -      4101    0.00    0.00    0.00    0.00    0.00    80  |__multipathd
11:31:06 AM     0         -      4102    0.00    0.00    0.00    0.00    0.00    80  |__multipathd

然后找了一个还没宕机的相同环境的系统,通过cat /proc/XXX/tasks/XXX/stack看一下每个线程在做什么

然后发现multipathd一共7个线程,其中三个线程在poll,三个线程在queue等待唤醒,剩下的那个线程是第六个,在进行一些check工作

而通过vmcore确认,发现每次core掉的multipathd线程其实就是第6个线程,也就是在check的线程

该线程正常的栈如下

root@XXX:~# cat /proc/4087/task/4101/stack
[<0>] hrtimer_nanosleep+0x99/0x120
[<0>] common_nsleep+0x44/0x50
[<0>] __x64_sys_clock_nanosleep+0xd2/0x160
[<0>] do_syscall_64+0x59/0xc0
[<0>] entry_SYSCALL_64_after_hwframe+0x61/0xcb

看上去它执行的其实就是: https://github.com/opensvc/multipath-tools/blob/master/multipathd/main.c 中的checkerloop函数,大致过程就是检查路径然后sleep

至此可以确认是multipathd的第六个线程也就是checkerloop线程引起的宕机

再看一下相关进程的优先级和调度类

#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

##这是触发宕机的multipathd线程,rt线程,RR策略,也就是时间片的rt线程
crash> task ff1d16af4e8f8000|grep policy
  policy = 2,
  mempolicy = 0x0,
crash> task ff1d16af4e8f8000|grep sched_class
  sched_class = 0xffffffffa1eea290 <rt_sched_class>,
crash> task ff1d16af4e8f8000 |grep prio
  prio = 0,
  static_prio = 120,
  normal_prio = 0,
  rt_priority = 99,
    prio = 0,
    prio_list = {

##这是cyclictest的线程,rt线程,fifo策略
crash> task ff1d16af6f935e80 |grep policy
  policy = 1,
  mempolicy = 0x0,
crash> task ff1d16af6f935e80 |grep sched_class
  sched_class = 0xffffffffa1eea290 <rt_sched_class>,
crash> task ff1d16af6f935e80 |grep prio
  prio = 19,
  static_prio = 120,
  normal_prio = 19,
  rt_priority = 80,
    prio = 140,
    prio_list = {

##cyclictest的优先级是80比multipathd的99要低

至此可以确定multipathd的checkerloop线程和cyclictest一样都是rt线程优先级,且multipathd的chekerloop线程优先级是99比cyclictest的80要高

好了,上面都已经明了了,再继续分析一下core

看一下触发softlockup的cpu的timer,果然是有任务都超时了还没有执行(其他的cpu上没有这个问题,但是cpu0上挂了好多好多ghes_poll_func,但是这个与本core应该没关系,我们不受这个场外因素影响)

该cpu上的rq如下,确实有timer超时了

crash> timer -C 240
JIFFIES
4306062733

TIMER_BASES[240][BASE_STD]: ff1d176b4f4207c0
  EXPIRES        TTE        TIMER_LIST     FUNCTION
  4306058428   -4305  ffffffffa2e83ee0  ffffffffa097a210  <clocksource_watchdog>
  4306097530   34797  ff1d176b4f41fc20  ffffffffa0861530  <mce_timer_fn>
TIMER_BASES[240][BASE_DEF]: ff1d176b4f421a40
  EXPIRES        TTE     TIMER_LIST     FUNCTION
  (none)


crash> runq -c 240
CPU 240 RUNQUEUE: ff1d176b4f430b80
  CURRENT: PID: 4069   TASK: ff1d16af4e8f8000  COMMAND: "multipathd"
  RT PRIO_ARRAY: ff1d176b4f430e40
     [  0] PID: 4069   TASK: ff1d16af4e8f8000  COMMAND: "multipathd"
     [ 19] PID: 1392590  TASK: ff1d16af58b48000  COMMAND: "cyclictest"
  CFS RB_ROOT: ff1d176b4f430cc0
     [120] PID: 3073   TASK: ff1d16af5f4b8000  COMMAND: "kworker/240:2"
     [100] PID: 3898   TASK: ff1d16af6d128000  COMMAND: "kworker/240:1H"

那么有可能是什么原因导致了stop_work一直没有更新时间戳呢?首先想到的就是是不是multipathd执行的内容陷入到内核中去有死循环或者死锁之类的?但是看了下multipathd中checkerlopp函数的代码又感觉没什么问题,而且他会执行nanosleep睡眠,也就是理论上并不会一直占用cpu,因为sleep会进行schedule让出cpu。

到这里仿佛没什么头绪了。于是把/etc/multipath.conf配置文件中加上verbosity 4,然后multipath -r然后systemctl restart multipathd,让multipathd打印更详细的日志,然后再触发一下cyclictest导致的宕机,看了下没什么问题感觉,还是在执行checkerloop中的定时判断检查,然后就突然宕机了。

这是一张图片

根据开启multipathd的log我们可以确定chekerloop线程其实是有执行nanosleep的,没有什么异常

所以这时候大胆推测一下是不是和rt调度器有关系?看一下vmcore中checkerloop线程所在的cpu上有没有挂一些ipi任务。

crash> p call_single_queue:240
per_cpu(call_single_queue, 240) = $1 = {
  first = 0xff1d176e5011d878
}
crash> list 0xff1d176e5011d878
ff1d176e5011d878
crash> call_single_data_t ff1d176e5011d878
struct call_single_data_t {
  node = {
    llist = {
      next = 0x0
    },
    {
      u_flags = 35,
      a_flags = {
        counter = 35
      }
    },
    src = 0,
    dst = 0
  },
  func = 0xffffffffa091f400 <rto_push_irq_work_func>,
  info = 0xfc4ab0200000001
}

确实发生问题的cpu上call_single_queue上挂着任务呢,而其他的cpu中是没有挂任务的,挂的任务是rto_push_irq_work_func。那是不是这个这个cpu上一直在处理ipi导致触发了softlockup的watchdog呢?

还是看看这个rto_push_irq_work_func函数是做什么的,看上去应该是rt调度的负载均衡相关的东西

void rto_push_irq_work_func(struct irq_work *work)
{
        struct root_domain *rd =
                container_of(work, struct root_domain, rto_push_work);
        struct rq *rq;
        int cpu;

        rq = this_rq();

        /*
         * We do not need to grab the lock to check for has_pushable_tasks.
         * When it gets updated, a check is made if a push is possible.
         */
         //has_pushable_tasks就是检查rq中的rq->rt.pushable_tasks是否为空
         //push_rt_task的作用是,如果当前cpu有超过1个的rt任务,就检查一下没有在运行的任务是不是可以
         //迁移到其他的正在运行低优先级的任务的cpu上去
        if (has_pushable_tasks(rq)) {
                raw_spin_rq_lock(rq);
                while (push_rt_task(rq, true))
                        ;
                raw_spin_rq_unlock(rq);
        }

        raw_spin_lock(&rd->rto_lock);

        /* Pass the IPI to the next rt overloaded queue */
        //发ipi给下一个rt overloaded queue?什么意思?应该是值发ipi给下一个过载的cpu,让下一个cpu也去执行负载均衡吧?
        cpu = rto_next_cpu(rd);

        raw_spin_unlock(&rd->rto_lock);

        if (cpu < 0) {
                sched_put_rd(rd);
                return;
        }

        /* Try the next RT overloaded CPU */
        //入队一个irq work
        irq_work_queue_on(&rd->rto_push_work, cpu);
}

至此可以猜测是该cpu在处理rt调度器负载均衡相关的任务

先大概看一下谁会去调用这个函数?

balance_rt
pull_rt_task
tell_cpu_to_push
     irq_work_queue_on(&rq->rd->rto_push_work, cpu);

看一下pick_next_task中什么时候会去调用balance呢?删除fair class中的一部分没用的内容,可以看到如果prev的sched_class不是小于等于fair的话(显然cyclictest和multipathd是rt class不满足这个条件),就会去先执行put_prev_task_balance,而put_prev_task_balance是干什么呢?其实就是从prev所属的sched class开始遍历并执行每个class的balance,对于rt来说,那自然就是balance_rt函数

__pick_next_task(struct rq *rq, struct task_struct *prev, struct rq_flags *rf)
{
..........
        if (likely(prev->sched_class <= &fair_sched_class &&
                   rq->nr_running == rq->cfs.h_nr_running)) {
..............
        }

restart:
        put_prev_task_balance(rq, prev, rf);
        for_each_class(class) {
                p = class->pick_next_task(rq);
                if (p)
                        return p;
        }


static void put_prev_task_balance(struct rq *rq, struct task_struct *prev,
                                  struct rq_flags *rf)
{
。。。。。。
        const struct sched_class *class;
        for_class_range(class, prev->sched_class, &idle_sched_class) {
                if (class->balance(rq, prev, rf))
                        break;
        }
。。。。。。
        put_prev_task(rq, prev);
}

所以我们基本可以得出,cyclictest和multipathd调度出去的时候应该都会执行balance。

因此从balance_rt开始看

static int balance_rt(struct rq *rq, struct task_struct *p, struct rq_flags *rf)
{
        if (!on_rt_rq(&p->rt) && need_pull_rt_task(rq, p)) {
                rq_unpin_lock(rq, rf);
                pull_rt_task(rq);
                rq_repin_lock(rq, rf);
        }

        return sched_stop_runnable(rq) || sched_dl_runnable(rq) || sched_rt_runnable(rq);
}

##如果当前rq中最高优先级的任务还不如正在运行的prio高,就需要拉任务下来???
##这块看core怎么感觉有些问题
static inline bool need_pull_rt_task(struct rq *rq, struct task_struct *prev)
{
        /* Try to pull RT tasks here if we lower this rq's prio */
        return rq->online && rq->rt.highest_prio.curr > prev->prio;
}

pull_rt_task的大致过程:

static void pull_rt_task(struct rq *this_rq)
{
        int this_cpu = this_rq->cpu, cpu;
        bool resched = false;
        struct task_struct *p, *push_task;
        struct rq *src_rq;
        int rt_overload_count = rt_overloaded(this_rq);

        //没有overload的cpu就返回
        if (likely(!rt_overload_count))
                return;

        /*
         * Match the barrier from rt_set_overloaded; this guarantees that if we
         * see overloaded we must also see the rto_mask bit.
         */
        smp_rmb();

        /* If we are the only overloaded CPU do nothing */
        //若当前cpu一个overload的,就什么都不做
        if (rt_overload_count == 1 &&
            cpumask_test_cpu(this_rq->cpu, this_rq->rd->rto_mask))
                return;

#ifdef HAVE_RT_PUSH_IPI
        if (sched_feat(RT_PUSH_IPI)) {
                //通知其他cpu push task?
                tell_cpu_to_push(this_rq);
                return;
        }
#endif
        //遍历所有在mask中的cpu
        for_each_cpu(cpu, this_rq->rd->rto_mask) {
                //跳过当前cpu
                if (this_cpu == cpu)
                        continue;

                //获取源rq,也就是遍历的cpu的rq作为源rq,因为需要从这些东西上push任务上去给之前的cpu pull
                src_rq = cpu_rq(cpu);

                /*
                 * Don't bother taking the src_rq->lock if the next highest
                 * task is known to be lower-priority than our current task.
                 * This may look racy, but if this value is about to go
                 * logically higher, the src_rq will push this task away.
                 * And if its going logically lower, we do not care
                 */
                 //如果src rq中highest_prio.next,也就是下一个要调度的rt进程的prio大于当前的rq中的highest_prio.curr就跳过?
                 //也就是,src rq中的下一个要调度的任务的优先级比当前小就跳过?
                if (src_rq->rt.highest_prio.next >=
                    this_rq->rt.highest_prio.curr)
                        continue;

                /*
                 * We can potentially drop this_rq's lock in
                 * double_lock_balance, and another CPU could
                 * alter this_rq
                 */
                push_task = NULL;
                double_lock_balance(this_rq, src_rq);

                /*
                 * We can pull only a task, which is pushable
                 * on its rq, and no others.
                 */
                 //从src rq中挑一个最高优先级的任务
                p = pick_highest_pushable_task(src_rq, this_cpu);

                /*
                 * Do we have an RT task that preempts
                 * the to-be-scheduled task?
                 */
                 //从src rq中push上来的任务是不是优先级比当前cpu上的最高优先级的任务的优先级高?
                 //是的话抢占?
                if (p && (p->prio < this_rq->rt.highest_prio.curr)) {
                        WARN_ON(p == src_rq->curr);
                        WARN_ON(!task_on_rq_queued(p));

                        /*
                         * There's a chance that p is higher in priority
                         * than what's currently running on its CPU.
                         * This is just that p is waking up and hasn't
                         * had a chance to schedule. We only pull
                         * p if it is lower in priority than the
                         * current task on the run queue
                         */
                         //如果p比他自己的cpu中正在运行的任务的优先级高,那就不管
                        if (p->prio < src_rq->curr->prio)
                                goto skip;
                        //如果p禁止migration,就先获取这个task?
                        //如果咩禁止,就从src rq中删除,然后设置task到本cpu,然后激活这个task?
                        if (is_migration_disabled(p)) {
                                push_task = get_push_task(src_rq);
                        } else {
                                deactivate_task(src_rq, p, 0);
                                set_task_cpu(p, this_cpu);
                                activate_task(this_rq, p, 0);
                                resched = true;
                        }
                        /*
                         * We continue with the search, just in
                         * case there's an even higher prio task
                         * in another runqueue. (low likelihood
                }
skip:
                double_unlock_balance(this_rq, src_rq);
                //如果是上面的禁止迁移的task,就向目标cpu发stop_work,让目标cpu执行push_work?
                if (push_task) {
                        raw_spin_rq_unlock(this_rq);
                        stop_one_cpu_nowait(src_rq->cpu, push_cpu_stop,
                                            push_task, &src_rq->push_work);
                        raw_spin_rq_lock(this_rq);
                }
        }

        if (resched)
                resched_curr(this_rq);
}




tell_cpu_to_push
irq_work_queue_on
__smp_call_single_queue
send_call_function_single_ipi

继续看一下core

enqueue_task_rt/sched_rt_rq_enqueue
enqueue_rt_entity
__enqueue_rt_entity
inc_rt_tasks
inc_rt_migration
rt_set_overload

static inline void rt_set_overload(struct rq *rq)
{
	if (!rq->online)
		return;

	cpumask_set_cpu(rq->cpu, rq->rd->rto_mask);
	/*
	 * Make sure the mask is visible before we set
	 * the overload count. That is checked to determine
	 * if we should look at the mask. It would be a shame
	 * if we looked at the mask, but the mask was not
	 * updated yet.
	 *
	 * Matched by the barrier in pull_rt_task().
	 */
	smp_wmb();
	atomic_inc(&rq->rd->rto_count);
}            ........

现在可以看到rto_mask确实被置位了,那么怎么置位的呢?

大致就是在enqueue或者dequeue的时候会inc或者dec对应的task,在这里就会判断是否overload了

enqueue_task_rt/sched_rt_rq_enqueue
enqueue_rt_entity
__enqueue_rt_entity
inc_rt_tasks
inc_rt_migration
rt_set_overload

static inline void rt_set_overload(struct rq *rq)
{
	if (!rq->online)
		return;

	cpumask_set_cpu(rq->cpu, rq->rd->rto_mask);
	/*
	 * Make sure the mask is visible before we set
	 * the overload count. That is checked to determine
	 * if we should look at the mask. It would be a shame
	 * if we looked at the mask, but the mask was not
	 * updated yet.
	 *
	 * Matched by the barrier in pull_rt_task().
	 */
	smp_wmb();
	atomic_inc(&rq->rd->rto_count);
}

(vmcore中的root_domain的overload是1表明,enqueue se之后,把overload设置成1之后,该cpu上就没有再dequeue过rt任务了,因为一直都在overload?所以就是一直在占用cpu?)

至此可以猜测是multipathd那个cpu overload了,导致一直有ipi中断需要处理,让它把task push上去

这表示,出问题的cpu上有两个rt任务了,可迁移的任务有一个,其实也就是multipathd,因为cyclictest我们肯定是绑核的,不能迁移

这正好可以对应上update_rt_migration函数设置overhead

crash> rq ff40900c4ddf0b80 |grep rt_nr_total
    rt_nr_total = 2,
crash> rq ff40900c4ddf0b80 |grep rt_nr_migratory
    rt_nr_migratory = 1,


static void update_rt_migration(struct rt_rq *rt_rq)
{
        if (rt_rq->rt_nr_migratory && rt_rq->rt_nr_total > 1) {
                if (!rt_rq->overloaded) {
                        rt_set_overload(rq_of_rt_rq(rt_rq));
                        rt_rq->overloaded = 1;
                }
        } else if (rt_rq->overloaded) {
                rt_clear_overload(rq_of_rt_rq(rt_rq));
                rt_rq->overloaded = 0;
        }
}

那么为什么overhead了就会宕机呢???

联系之前开启multipathd的日志,难不成和check线程的每次tick一秒钟有关系?不应该呀,这个是有sleep 1 的操作的会让出cpu,怎么还会softlockup呢?

所以现在的线索和疑点如下:

  1. 宕机应该和rt进程的负载均衡有关,因为宕机的cpu上会发现挂着rto_push_irq_work_func的irq work,这个应该是cpu上rt进程overhead导致的,一般认为cpu上挂了大于1个的rt进程的时候就认为overheade了(当然还有别的一些判断条件,比如进程是否能迁移到其他cpu之类的,不过从core中可以看出,这个绝对是overhead了,从root_domain的rto_mask可以看出),需要负载均衡一下
  2. 为什么multipathd的其他线程没有导致宕机?都是checkerloop这个线程导致的?

还是需要有问题的环境去调试一下,但是开了ftrace之后问题就不好复现了。于是写了个bpftrace脚本监控一下rto_push_irq_work_func函数。

可以确定的是,在出问题时rto_push_irq_work_func函数调用激增,那么谁会调用这个呢?看了下只有tell_cpu_to_push和rto_push_irq_work_func函数。而tell_cpu_to_push是被pull_rt_task调用的,也就是其他cpu需要向这个异常cpu通知拉任务下来的时候会调用。另一处调用就是rto_push_irq_work_func里调用了,这个有可能是自己给自己发ipi然后调用这个rto_push_irq_work_func,然后在这个函数里再自己给自己发ipi导致这个死循环一直发ipi让这个cpu去处理softirq?但是看了下这块的函数逻辑,理论上应该不会,他应该是找当前cpu的next cpu,也就是rto cpumask中当前cpu的下一个overlaod的cpu去调用,rd->rto_cpu的赋值看上去问题不大。

要搞清楚是谁谁让异常cpu调用一直处理中断,还是要trace清楚,是谁大量调用的rto_push_irq_work_func函数。

perf probe ‘rto_push_irq_work_func’

perf record -e probe:rto_push_irq_work_func -aR sleep 30

然后又perf probe了一下tell_cpu_to_push,大概就明白了

cyclictest调度的时候会触发rt balance,这个就会发ipi让异常cpu push任务上去,但是短时间内大量的发的话就处理不过来了

https://lore.kernel.org/lkml/xhsmhpm3vb6ws.mognet@vschneid.remote.csb/T/

然后看到社区有人遇到过类似的问题

所以这个过程就很清晰了:

  1. 开启cyclictest,每个cpu上会有一个绑核的cyclictest的rt进程,他的优先级是80
  2. ubuntu22上会有一个multipathd进程,他有一个优先级为99的chckerloop的rt线程
  3. 当multipathd的chckerloop运行时,他所在的cpu上(假设是X号cpu)还存在cyclictest的rt线程,因此有两个线程,所以系统会认为这个cpu overload了
  4. 其他cpu上的cyclictest运行时,进行调度或者sleep(这会进行schedule,schedule到swapper的话,其实就是往低优先级的任务进行调度了)的时候,发现需要负载均衡,于是就使用tell_cpu_to_push(这个过程会增加rq->rd->rto_loop_next)向X发送ipi让它去push task
  5. X上现在正运行multipathd的checkerloop,而且该cpu上的cyclictest是绑核的没办法migration,因此也就没有pushable_tasks,也就不会把task push上去。然后在rto_push_irq_work_func中就会继续执行rto_next_cpu去找下一个overload的cpu,通过rto_next_cpu代码逻辑可以知道,他是从X开始从rto_mask中寻找下一个overload的cpu,很明显,它找不到,因为只有X是overload的,于是此时rd->rto_cpu被置为-1,但是他还会去尝试去判断rto_loop_next == rto_loop,如果不相等的话,就把rto_loop赋值成rto_loop_next,然后继续循环,很明显,再进入下一轮循环后,rto_cpu此时的值是-1,那在找cpu的时候就会把X给return了,因此又会去调用irq_work_queue_on往自己所在的X上发ipi执行rto_push_irq_work_func,然后X上唤醒irq work去执行rto_push_irq_work_func还是没办法执行把task push上去,因为只有multipathd的checkerloop是pushable的,但是他正在运行,所以此时是没有pushable task的,而其他cpu也还在一直给它发ipi,他就一直不断处理中断,最终导致softlockup

那么这该怎么解决呢?一个是像那个patch一样如果没有pushable task的话就直接返回?或者如果cpu是当前cpu的话就返回,不让他自己给自己发ipi(这个是不是不太合理,应该是允许这样的情况发生的吧)?或者能不能从overload的判定下手(如果是每个cpu上都有绑核的cyclictest这种rt进程的情况,并且有更高优先级的rt运行在一个cpu上的时候,这个是不是就不判定overload???不对,也不合理,因为我们没办法假设每个cpu上绑核的rt进程的任务是什么样的,因此,只要某个cpu上有可push的任务的时候,其实就应该判定为overload的)?

https://lkml2.uits.iu.edu/hypermail/linux/kernel/1704.2/04980.html

结合这个的话,看上去可能确实存在问题啊?rto_loop_next是用来标记cpu往低优先级的任务进行调度(比如cyclictest sleep的话进行schedule到swapper),而cyclictest会进行很多这样的重复操作,这就有可能导致rto_loop老是追不上rto_loop_next,从而导致除了cyclictest的cpu往X上发ipi,X cpu上执行rto_push_irq_work_func时候也会给自己发ipi。

这样合理嘛?不让cpu自己给自己发ipi了,应该可以减少很多没用的ipi,但是应该还是没办法解决cyclictest的那些cpu给X发ipi

diff --git a/kernel/sched/rt.c b/kernel/sched/rt.c
index bd66a46b06ac..2ebb7f75ff79 100644
--- a/kernel/sched/rt.c
+++ b/kernel/sched/rt.c
@@ -2165,8 +2165,12 @@ static int rto_next_cpu(struct root_domain *rd)

                rd->rto_cpu = cpu;

-               if (cpu < nr_cpu_ids)
-                       return cpu;
+               if (cpu < nr_cpu_ids) {
+                       if (cpu != smp_processor_id())
+                               return cpu;
+                       else
+                               return -1;
+               }

                rd->rto_cpu = -1;

看来社区已经有人解决了

sched/rt: Make rt_rq->pushable_tasks updates drive rto_mask(https://lore.kernel.org/lkml/20230811112044.3302588-1-vschneid@redhat.com/T/)

这个解法妙,是从根本上解决问题的,产生这个问题的根本原因并不是说rto_push_irq_work_func给自己发ipi,而是由大量的cyclictest rt进程的sleep导致的schedule检测到需要进行rt balance,那么rt balance怎么做呢,它就找之前已经在root_domain中是否已经设置了overload,并在rto_mask中寻找overload的cpu,然后向这些overload的cpu发送ipi起执行rto_push_irq_work_func这个irq work,这个rto_push_irq_work_func函数的主要内容是什么呢?就是把pushable task push上去。

而为什么cyclictest导致宕机了呢?因为在softlockup的那个cpu上,存在cyclictest和multipathd两个rt进程,cyclictest是绑核的不可迁移,只有multipathd是可迁移的,但是按照当前的设置方式来说,当enqueue multipathd的时候,因为它是可迁移的,所以rt_nr_migratory此时是1,而rt_nr_total是2大于1(因为有之前的cyclictest rt进程在),所以就符合了update_rt_migration函数中的判断,所以就认为该cpu上是overload的。但实际上,当multipathd运行的时候,pushable task队列中并没有可以push的task,为什么呢?因为只有multipathd是可push的但是它正在运行,所以这就导致其他的cyclictest的cpu检测到这个multipathd的cpu overload了,去给他发ipi,但是这个cpu执行rto_push_irq_work_func函数相当于并没有什么实质操作,因为此时他没有可push的任务,这就导致的问题的产生,其他的cyclictest的cpu还是会一直给它发ipi,因为他们一直认为他是overload的,但是这个cpu上并没有把任何task push上去,所以就相当于一直空执行rto_push_irq_work_func,最终导致了softlockup。

所以,产生本问题的根因应该是:这种情况下(两个rt线程,一个rt线程是绑核的,另一个是不绑核的可以迁移的,但是这个可迁移的task正在cpu上运行)就不应该把这个cpu判定为overload,因为它当前就没有可以push的task。

sched/rt: Make rt_rq->pushable_tasks updates drive rto_mask这个patch怎么解决的这个问题呢?他把原本的判定overload的方式去掉了,原本的判断时机是在enqueue_rt_entity/dequeue_rt_entity中最终调用到update_rt_migration来判断是否overload(也就是根据rt_nr_migratory和rt_nr_total)。把这个判断时机放到了enqueue_pushable_task和dequeue_pushable_task中,这俩是在enqueue_task_rt/dequeue_task_rt会调用。如下所示,在enqueue_task_rt中,以前是在enqueue_rt_entity直接检测并设置overload的,现在放到了enqueue_pushable_task中。假设还是cyclictest导致宕机的那种情况,当multiptahd enqueue的时候,进入if (!task_current(rq, p) && p->nr_cpus_allowed > 1)判断,因为要先判断一下task_current(rq, p),也就是当前的task是不是正在running的,如果是的话,就不设置overload,所以就不会出现这种情况了,妙啊,还得是社区大佬们

static void
enqueue_task_rt(struct rq *rq, struct task_struct *p, int flags)
{
        struct sched_rt_entity *rt_se = &p->rt;

        if (flags & ENQUEUE_WAKEUP)
                rt_se->timeout = 0;

        check_schedstat_required();
        update_stats_wait_start_rt(rt_rq_of_se(rt_se), rt_se);

        enqueue_rt_entity(rt_se, flags);

        if (!task_current(rq, p) && p->nr_cpus_allowed > 1)
                enqueue_pushable_task(rq, p);
}

转载请注明来源,欢迎对文章中的引用来源进行考证,欢迎指出任何有错误或不够清晰的表达。可以在下面评论区评论,也可以邮件至 857879363@qq.com