上一篇《cfs调度类深入解刨------最新内核细节分析1》中将kernel 7.0及以上版本中(较于kernel 6.6)的基础概念做了一些总结,并且再次引出了"代理调度运行"的概念。
本篇文章讲述kernel 7.1版本中的"代理调度运行"设计用意及相关功能。
代理调度运行(SCHED_PROXY_EXEC)
任务优先级反转从 1991 年 Linux 诞生之初、由于支持多任务和进程同步锁就原生存在的经典理论缺陷。举个经典的例子,高优先级任务被低优先级任务阻塞,导致中优先级任务比高优先级任务先执行的现象:
- 低优先级(任务A):如获取了共享内存的总线锁(互斥锁)。
- 高优先级(任务C):需要获取同一个总线锁,因此被挂起并等待任务A释放锁。
- 中优先级(任务B):它不需要锁,但它的优先级高于任务A,因此它抢占了任务A的CPU时间。
Linux 2.6.18 版本中,内核正式合入了 RT-Mutex(实时互斥锁)机制和 PI-Futex(优先级继承 Futex),当高优先级任务C阻塞于低优先级任务A持有的锁时,任务A临时提升为任务C的优先级,从而避免中等优先级的任务B抢占,让任务A快速执行完并释放锁。这首次在上游代码中预防了实时任务的ABC反转,但大部分服务器内核未开启/未使用RT-Mutex等机制(成本代价偏高,并且cfs基本调度类的任务还可以靠某个时刻检测,持锁顺延等手段),因此各家公司内部流传着自己的治理手段。
EEVDF到来的时代,新型服务器逻辑cpu数量已经达到512核心,更多的容器及任务数量,更复杂的迁移场景(如任务驱逐形式迁移,高优先级调度类任务频繁抢占等),任务优先级反转成为不可忽视并且持续演进的死锁问题。
7.0及以上版本中(kernel 6.17中增加),调度代理运行(SCHED_PROXY_EXEC)功能已经愈发稳定,执行逻辑如下(__schedule流程):
- 选取下一个任务。
- rq队列下一个调度类赋值为下一个任务调度类。
- rq队列贡献任务(donor)赋值为下一个任务。
- 下一任务属于阻塞状态,从贡献任务向互斥锁的所属者(owner)遍历(找出哪个任务拿到的互斥锁):
4.1. 互斥锁没有所属者,任务锁阻塞状态清空(未持有互斥锁或者在非预期的互斥锁上阻塞则打印一次警告),贡献任务可以运行了,返回贡献任务。
4.2. 所属者不在rq队列中或者所属者的调度实体支持延迟出列(sched_delayed),rq队列贡献任务赋值为rq队列空闲任务(互斥锁持有者偷懒了,贡献任务只能先放一放等互斥锁持有者让出锁了再说,并且当前只能让空闲任务运行避免锁过久等待,优先级反转等问题),设置重新调度标志并返回rq队列空闲任务。
4.3. 所属者所在的cpu不是当前cpu,当前rq队列设置重新调度标志并返回rq队列空闲任务,将贡献任务迁移到所属者所在的cpu队列中(贡献任务在这里没啥用了,去所属者的cpu吧,等他让出锁就解放了。但是在7.0版本中还是上一个想法,贡献任务还是等互斥锁持有者让出锁了再说,说明7.1版本确实进化了,与其等待不如加入)。
4.4. 所属者包含正在被迁移标志,设置重新调度标志并返回rq队列空闲任务(所属者也不保了,等他稳定了再说)。
4.5. 所属者不在rq队列中或者所属者所在的cpu不是当前cpu,返回null(重试吧,所属者可能发生了变动)。
4.6. 所属者是贡献任务,设置重新调度标志并返回rq队列空闲任务(???ttwu_runnable发力的时刻到了,调度rq->idle,以便ttwu_runnable()能够获取rq锁,并将所有者标记为运行状态)。
4.7. 返回所属者(理想的场景,锁到擒来)。
上面的调度代理运行可以看出这个功能只为了一件事,就是当前待运行的任务没有拿到锁,必须想尽一切办法让所属者先运行并让出锁,这段期间cpu运行空闲任务浪费cpu也能接受(出现这种锁等待场景说明硬件也快到达处理瓶颈了),毕竟死锁代价远远大于等待时间。
延迟出列(sched_delayed)
配合代理调度运行(SCHED_PROXY_EXEC)出的功能,当任务因为互斥锁阻塞等待时不用移出队列(包含DEQUEUE_SLEEP标志,不包含延迟出列或出列限流标志,并且没有运行资格),再次执行入列函数时需要先移出队列再加回队列,更新cfs_rq队列负载等。更新调度实体虚拟滞后值时,在开启DELAY_ZERO的情况下,调度实体的虚拟滞后值最大为0,避免窃取运行机会(理论上已经运行了足够的时间)。
延迟出列包括以下几种重要信息:
- 尝试唤醒函数(ttwu)和__sched_fork函数检测到任务具有延迟出列(sched_delayed = 1)打印警告。
- 选取下一个任务时,cfs_rq队列的next具有延迟出列(sched_delayed = 1)打印警告。
- 再次入列(requeue_delayed_entity)不具有延迟出列(sched_delayed = 0)打印警告。
- cfs抢占函数中具有延迟出列(sched_delayed = 1)的任务不具备抢占能力。
- 任务具有延迟出列(sched_delayed = 1)时,cfs_rq队列的层级可运行数量不包括这个任务。