唤醒实时进程时对目标cpu的选择策略与存在的问题

以下三篇文章阐述了唤醒实时进程时对目标cpu的选择策略与可能存在的问题。

1,对待实时进程(RT)抢占的问题

一个进程被唤醒,在linux中是调用try_to_wake_up函数,对于RT进程也不例外,对于一般进程而言,如果在一个cpu运行队列上被唤醒的进程的优先级大于该cpu的当前进程,那么就会发生抢占,而如果两个进程都是RT进程则不会发生抢占,理由是cache的保持,如果发生抢占的话,被抢占的RT进程将丢失其所有的cache,可是这样做合理吗?
     先看一下RT进程的特征,这种进程一旦运行,除非自愿放弃cpu,一般是不会停止运行的,对于高优先级的RT进程总是优先运行,如果对于RT进程为了保持cache不抢占的话,除了cache的保持之外,其实是得不到任何优势的,并且,在这种情况下还会造成RT进程的乒乓效应,也就是造成RT进程频繁迁移,在很多RT进程争抢一个资源,比如lock的情况下,这种乒乓效应带来的弊端更加明显,低优先级的RT进程释放了lock,唤醒了高优先级的RT进程,由于此时当前cpu上的进程仍然是低优先级的进程,所有被唤醒的高优先级进程不得不被迁移到别的cpu上执行,如果低优先级的进程没有释放资源,而由于另一个原因睡眠了,此时它的优先级可能已被提升,当它再度被另一RT进程唤醒的时候,即使它拥有高优先级也不得不迁移到另外的cpu上,这些迁移动作肯定会影响性能的。可以说,RT进程调度的无条件不抢占特性大大削弱了高优先级RT进程的优势,高优先级RT进程除了在运行队列的位置靠前之外,没有多少优势可言了。
     确实,如果是由于非争抢资源被释放原因的唤醒,那抢占正在运行的rt进程确实对于cache优化是一种抵消,然而要知道即使这样我们也只是损失了相对低优先级rt进程的性能,我们得到的是避免了一次高优先级rt进程的迁移。另一方面,当我们面对多个rt进程争抢资源这种类型的睡眠/唤醒时,抢占的优势就明显了,有时一个又有很高优先级的rt进程在运行中由于得不到资源而睡眠,这种睡眠时间一般不会太久,只是一小会儿,它争抢的资源此时正在被一个低优先级的rt进程占据,此时它提升低优先级进程的优先级,以使它不被抢占地尽快完成资源的释放,这一切都是为了高优先级进程不会睡得太久,于是资源尽可能早的被释放了,高优先级进程被唤醒,此时应该知道,将此高优先级rt进程唤醒在它本来运行的cpu上是一个最佳的选择。
     这就是一个权衡的问题了,是保留cache还是最小化迁移,还是交给调度器吧,严格按照优先级调度会好一些,起码能照顾高优先级的RT进程,使得高优先级的RT进程的优势更加明显。于是,新的2.6.37内核对此进行了改进,补丁很简单:
--- a/kernel/sched_rt.c
+++ b/kernel/sched_rt.c
@@ -960,18 +960,18 @@ select_task_rq_rt(struct rq *rq, struct task_struct *p, int sd_flag, int flags)
        if (unlikely(rt_task(rq->curr)) &&
+           rq->curr->prio < p->prio &&
            (p->rt.nr_cpus_allowed > 1)) {
                int cpu = find_lowest_rq(p);
 
@@ -1491,6 +1491,8 @@ static void task_woken_rt(struct rq *rq, struct task_struct *p)
        if (!task_running(rq, p) &&
            !test_tsk_need_resched(rq->curr) &&
            has_pushable_tasks(rq) &&
+           rt_task(rq->curr) &&
+           rq->curr->prio < p->prio &&
            p->rt.nr_cpus_allowed > 1)
                push_rt_tasks(rq);
}

2,实时进程cpu的选择

实时(real-time)进程cmcld在CPU2的运行队列(run queue)中静坐了14.6秒,一直未能得到运行机会,当时CPU2上正在运行的进程JobWrk6653处于内核态,由于SLES内核是非抢占式的,所以cmcld无法抢占CPU2。但是,这台机器有288个CPU,只有CPU2在忙,其余287个CPU都是空闲状态,为什么cmcld进程一味死等CPU2、不能到其他CPU上去运行呢?

考虑这个问题要抓住两个点:

1,实时进程在唤醒时怎么进了CPU2的运行队列,而没有选择其他CPU?

2,在运行队列中的实时进程怎么未能被migrate到其他CPU上?

第2点留待下次讨论,今天先研究第1点。

实时进程被唤醒时会选择哪个CPU呢?这是由select_task_rq_rt()决定的。

调用路径是try_to_wake_up -> select_task_rq -> select_task_rq_rt

从以下的select_task_rq_rt 源程序可以看到它是如何挑选CPU的:它先找到被唤醒进程上次运行的CPU,如果此CPU上正在运行的进程比被唤醒的进程优先级更低,那么被唤醒的进程就还留在此CPU上、不会寻找其它CPU--无论其它CPU空闲与否;只有两种情况下会寻找别的CPU给被唤醒的进程用:一是此CPU上正在运行的进程是实时类型并且该进程绑定在此CPU上,二是此CPU上正在运行的进程的优先级不比被唤醒的进程低。

这个算法背后的逻辑是:两个进程争夺CPU时,优先级更高的进程总想挤走优先级更低的,哪怕对方是先来的,哪怕其他CPU都是空闲的,不管先来后到,也不管有没有其他空闲CPU可用。这真是非常霸道呀,假想对话如下:

“你放开那个CPU,那是我用惯了的,”

“可是我先来的呀。”

“我的优先级比你高,”

“旁边别的CPU都闲着呢,你用那些不行吗?”

“要走你走。”

但是,优先级更低的进程并不是想挤走就能挤走的,有时候会当钉子户,我们这个案例就是碰到了挤不走的钉子户。

那么,刚被唤醒的优先级更高的进程作为后来者,打算如何挤走先来的优先级更低的进程呢?是通过一个称为wakeup preemption的过程。我们接着看源程序。

try_to_wake_up通过select_task_rq()给被唤醒的进程选择CPU之后,再调用ttwu_queue()把被唤醒的进程插入到目标CPU的运行队列中;

ttwu_queue的操作有点眼花缭乱,基本思路是先判断执行唤醒任务的CPU与目标CPU是否共享L3 cache,如果共享的话就直接把被唤醒的进程插入运行队列并进行后续操作,如果不共享的话就采用IPI中断的方式通知目标CPU自己把进程插入运行队列。在此不细述,有兴趣的自己阅读源码。

被唤醒的进程进入目标CPU的运行队列以后,check_preempt_curr()函数会检查被唤醒的进程是否有权抢占当前进程,如果有权抢占的话就调用resched_task()触发wakeup preemption。

resched_task()只是给当前进程设置一个 TIF_NEED_RESCHED 标志,触发抢占(preemption),并不实际切换进程。

在resched_task()触发preemption(抢占)之后,什么时候切换进程呢?这里分两种情况:

1,用户态抢占(user preemption)的时机

  • 在系统调用和中断返回用户态的时候,会检查当前进程的 TIF_NEED_RESCHED 标志,发现置位则切换进程;

  • 运行中的进程主动调用schedule切换进程。

2,内核态抢占(kernel preemption)的时机

  • 在我们这个案例中,SLES内核关闭了内核抢占,所以根本就不会发生内核态抢占。但如果允许内核抢占的情况下,切换进程的时机是:

  • 中断结束并返回到内核空间之前;

  • 重新打开内核抢占的时候,preempt_enable会调用preempt_schedule检查 TIF_NEED_RESCHED标志,发现置位的话就切换进程。

回到我们的案例。cmcld以前是在CPU2上运行的,被唤醒的时候select_task_rq_rt()发现CPU2上虽然有一个进程JobWork6653正在运行,但它的优先级比cmcld低,所以仍然把cmcld放进了CPU2的运行队列,并触发了wakeup preemption:我们从vmcore中可以看到,'JobWrk6653'进程的TIF_NEED_RESCHED标志位已经被设置了,证明preemption已经被触发了:

虽然preemption已经被触发,但是始终未能完成进程切换,因为在SLES关闭了内核抢占的情况下,只能等进程回到用户态之后才能进行抢占,这一等就是14.6秒,"JobWrk6653"在内核态一直没有出来,它就这样当了一个钉子户...

这个案例表明,关闭内核抢占的情况下select_task_rq_rt()的霸道算法无法保证实时进程及时获得CPU。

3,可能存在的问题导致实时进程不实时

前文说到实时(real-time)进程cmcld在CPU2的运行队列(run queue)中静坐了14.6秒,虽然还有287个CPU处于空闲状态,cmcld却一直未能得到运行机会。我们已经解释了为什么cmcld一开始会进入CPU2的运行队列而未选择其他空闲状态的CPU,今天我们继续分析为什么cmcld未能被migrate到其他CPU上。

进程从一个CPU换到另一个CPU有个术语叫做migration。已经进入运行队列的进程发生migration只能通过负载均衡(load balance)触发。

在多CPU的系统上,使各个CPU之间的负载保持均衡是进程调度器的工作。在我们这个案例中,大量CPU空闲的情况下还有个进程在运行队列中等了14.6秒之久,显然进程调度器的负载均衡算法失灵了。

Linux的进程调度器是以模块化的方式提供的,允许多种调度算法并存,调度模块称为调度类(scheduling class),最常用的是CFS class和real-time class。而CFS类和real-time类的负载均衡算法是不一样的。

CFS的负载均衡发生在以下时刻:

  • 周期性的负载均衡(Active Balancing),通过时钟中断,scheduler_tick()会调用trigger_load_balance()触发SCHED_SOFTIRQ进行负载均衡操作;

  • 空闲时的负载均衡(Idle Balancing),当CPU进入idle状态的时候,会调用idle_balance(),试图从其他CPU的运行队列里pull(拉取)进程。

Real-time的负载均衡算法基于运行队列是否overload,所谓overload就是运行队列中的real-time进程数量超过了1个。发生负载均衡的时刻如下,请注意它并不考虑进程在队列中等待了多长时间,也没有像CFS的负载均衡那样的周期性操作:

  • 当进程进入real-time队列的时候,会检查队列是否overload,如果overload则调用push_rt_task试图从该队列中把进程push(推)到更空闲的CPU队列;

  • 当运行队列中的最高进程优先级降低的时候,会检查其他队列是否overload,如果有overload就调用pull_rt_task试图从其他队列中把优先级更高的进程pull(拉)过来。

回到我们的案例,cmcld是实时进程,归real-time调度器管,由于CPU2的运行队列里只有这一个实时进程,根据real-time的负载均衡算法,不满足overload的条件,所以既不会发生push也不会发生pull,结果cmcld就这么一直在CPU2的运行队列里待着...

设想如果cmcld是CFS进程,它还会在CPU2的运行队列中等那么长时间吗?并不会,因为CFS的负载均衡算法会发现CPU2上有两个进程,会把一个进程migrate到其他的空闲的CPU上去。

这个案例暴露了real-time调度器的不足之处:real-time调度器在选择CPU和进行负载均衡的时候,眼里只有real-time进程,没有考虑CFS等其他类型的进程的影响,比如它认为队列里只有一个real-time进程就不算overload、无论有没有CFS进程,这通常不会有问题,因为CFS优先级低于real-time,对real-time进程造不成影响,然而它没有考虑到特殊情况:在非抢占式内核里,CFS进程可能会因为陷在内核态而造成real-time进程无法抢占。所以说,也许real-time调度器还可以做得更好一点。

发布了158 篇原创文章 · 获赞 115 · 访问量 37万+

猜你喜欢

转载自blog.csdn.net/yiyeguzhou100/article/details/103500931
今日推荐