ARTICLE DETAIL

资讯详情

深耕网站视觉设计与运营推广的一线实战洞察。

Linux SCHED_DEADLINE调度器深入解析:从EDF到CBS实战指南

Linux SCHED_DEADLINE调度器深入解析:从EDF到CBS实战指南 说实话调度器这块内容我本来没打算这么快写续篇但上一篇聊完 CFS 之后后台一直有人追着问“那 Linux 里有没有真正按截止时间来调度的东西”。答案当然是有就是标题里这个 SCHED_DEADLINE一般简称 DL中文叫截止时间调度器。它不是那种只能在论文里看看的玩具而是从 Linux 3.14 开始合入内核主线的真实调度策略到现在已经在嵌入式实时场景、机器人控制、音视频处理这些对延迟敏感的领域里有了不少落地案例。这篇就围绕 DL 调度器展开主要聊三件事DL 到底解决了什么内核痛点、它的核心机制是怎么设计的、以及我们自己的任务怎么才能跑在 DL 调度上并验证效果。文章不是教科书式的源码逐行分析更多是我在实际环境里跑过、踩过坑之后的理解和操作记录适合做嵌入式 Linux、实时系统、或者单纯想深入理解内核调度器的朋友阅读。1. 为什么 CFS 扛不住实时任务DL 又是来解决什么问题的1.1 CFS 的“公平”其实是一种尽力而为在聊 DL 之前得先回到 CFS完全公平调度器的定位。CFS 从 Linux 2.6.23 开始成为默认调度器它的核心思想是按权重分配 CPU 时间尽量让每个任务都能获得与优先级成比例的运行机会。这个设计对服务器和桌面场景非常好因为它照顾的是“吞吐”和“公平”而不是“某个任务必须在某个时刻前完成”。但实时场景的诉求恰恰相反控制机器人关节的线程要求每 1ms 唤醒一次并且在 500us 内算完动力学方程音视频合成线程要求每 4ms 处理一帧不能因为其他任务抢占了 CPU 就丢掉这帧。这类需求用 CFS 无法严格满足因为 CFS 并不知道“这个任务必须在截止时间前完成”这一约束它只知道“这个任务的权重高一点可以多分一点时间”。那有人会说不是有 RT 调度器吗把任务设成 SCHED_FIFO 不就行了没错RT 调度器确实可以做到“优先于所有 CFS 任务运行”但它的优先机制非常粗暴高优先级任务只要就绪就一定会抢占低优先级任务而且可以一直运行到阻塞或主动让出 CPU。如果某个 RT 任务里写了一个死循环整个系统上的低优先级任务包括内核线程都会被饿死。更棘手的是RT 任务没有“执行时间预算”的概念——调度器不知道这个任务这周期该运行多久只能靠用户自己保证不写坏程序。所以在实际工程里只用 SCHED_FIFO/RR 来解决实时问题往往要非常小心地手动控制每个任务的运行时间否则很容易出现“高优先级任务把低优先级任务饿死”或者“偶发计算量过大导致整个系统卡死”的极端情况。1.2 DL 调度器的模型每个任务带着“截止时间”来排队SCHED_DEADLINE 的设计思路是把任务建模成周期实时任务每个任务被赋予三个关键参数runtime每个周期内任务最多可以运行多久deadline任务必须在这个相对截止时间之前完成本周期的工作period任务的周期即每隔多久要重复一次上述过程调度器要保证的硬承诺是在每个周期内任务累计运行时间不超过 runtime并且在 deadline 之前一定能得到运行机会。听上去是不是很像“把每个任务当成奥运会的每个运动员要在规定时间内跑完自己的赛段”实际上DL 调度器的内部算法也确实是从实时系统理论里的 EDF最早截止时间优先演变而来的。EDF 调度算法的核心思想极其简单每次需要选择下一个运行的任务时选取所有就绪任务里绝对截止时间最早的那个。这个算法在理论上有非常漂亮的特性——只要系统总利用率不超过 100%EDF 就能保证所有任务都在截止时间前完成。这比 RT 的固定优先级方案更优雅因为 RT 优先级设置得不好即使 CPU 有大量空闲高优先级任务也可能因为低优先级任务阻塞等原因造成优先级反转或不可预测的延迟而 EDF 是理论上最优的动态优先级调度算法。不过直接把 EDF 用在通用操作系统里会出问题。第一个问题是任务运行时间的不确定性和“预算透支”。假设某一次任务实际运行时间超过了声明的 runtime那么后续任务就会被打乱甚至可能会连锁错过截止时间。第二个问题是EDF 缺乏对任务带宽的隔离一个失控任务会把整个系统的可用带宽全部吃光。这就是为什么 Linux 内核没有直接实现纯 EDF而是引入了 CBSConstant Bandwidth Server常量带宽服务机制来包装任务让每个任务先变成“带宽可控的服务器”再交给 EDF 去调度。1.3 DL 在调度器里的位置Linux 内核的调度类按照优先级从高到低依次是stop_machine停止类DL 调度类SCHED_DEADLINERT 调度类SCHED_FIFO/SCHED_RRCFS 调度类SCHED_NORMAL/SCHED_BATCHidle 调度类这个位置很关键DL 比 RT 还高。也就是说当 DL 任务到达截止时间窗口且有预算时它会抢占任何 RT 任务。反过来RT 任务在 DL 任务没有运行需求的时候可以正常执行但一旦 DL 任务进入 ready 状态CPU 会马上切换过去。从内核源码角度说kernel/sched/deadline.c文件就是 DL 调度类的实现它注册在sched_class链表中的dl_sched_class位置正好位于rt_sched_class之前。我在开始接触这段逻辑时也觉得很费解为什么一个“理论上最优”的算法落地时要做这么多包装后来等自己在机器上把 DL 任务跑起来、观察调度延迟之后才意识到OS 里的实时调度从来不是“算法最优就行”而是要面对任务不会严格按照声明参数执行、CPU 可能被中断打乱、多核之间还有迁移开销这些现实问题。CBS 的引入就是给 EDF 加了一层“安全带”。2. DL 调度器的核心设计EDF、CBS 与准入控制2.1 EDF 调度规则在红黑树里的体现在 Linux 内核里DL 调度器为每个 CPU 的运行队列维护一棵红黑树树中节点按照任务的绝对截止时间也就是当前周期内的 deadline 绝对时刻排序。每次调度器挑选任务时直接从红黑树最左侧取出截止时间最早的任务即可。这个操作的时间复杂度是 O(1) 取最左节点选任务的开销非常小。理解这一点我们就能解释为什么 DL 任务能获得比 RT 更平滑的延迟RT 调度器的优先级是静态的只要高优先级任务就绪它永远优先。但 DL 调度器的“优先级”是动态的完全由截止时间的紧迫程度决定。一个截止时间稍晚的 DL 任务即使它以前优先级很高在当前周期也可能让位于截止时间更早的任务。这种机制让系统内的多个实时任务可以更和谐地共存。举个例子假设有两个 DL 任务 A 和 BAruntime 1msdeadline 4msperiod 4msBruntime 1msdeadline 6msperiod 6ms在某段时间内A 的当前截止时间是 t4B 的当前截止时间是 t6。按 EDFA 先运行。等 t4 到达A 执行完本周期后重新释放新 deadline 变成 t8而此时 B 的 deadline 是 t6那么接下来就会轮到 B。两个任务在时间轴上像接力棒一样交错推进谁更急谁先跑。这个机制最吸引人的地方在于它从算法上避免了 RT 固定优先级里常见的“优先级反转”问题。在 RT 里如果低优先级任务持有一个锁高优先级任务等待这个锁就会发生优先级反转。虽然内核有优先级继承协议来缓解但始终是个需要单独处理的问题。而 EDF 是动态优先级本质上是让截止时间更近的任务优先能更自然地应对这种资源竞争。2.2 CBS 机制预算、截止时间与带宽隔离直接使用 EDF 的问题在于任务的实际执行时间可能比声明的 runtime 更大。比如一个机器人控制任务声明 runtime 是 1ms但某次因为异常数据导致算法执行了 3ms那它的预算就透支了。如果没有 CBS这个任务会吃掉后续任务的时间导致整个 CPU 上的所有 DL 任务都错过截止时间。CBS 的做法是“服务器化”每个任务先把 CPU 时间变成一个个固定大小的带宽服务器然后调度器按服务器的截止时间进行 EDF 调度。服务器把任务发出的每个作业job当作一个预算周期每当服务器获得 CPU 并且任务处于可运行状态内核会按实际运行时间消耗服务器预算当前周期剩余的 runtime。当预算用完而任务还没有完成时如果该任务的当前作业被占先或让出 CPU服务器会把这个作业的截止时间推后到下一个周期同时恢复预算。相当于“这个周期的配额花完了剩下的活只能算到下一个周期”。如果任务在预算内完成了则服务器重新充值并生成一个新的截止时间。可以用一个生活化的类比来理解CBS 相当于给每个任务开了一张带额度限制的信用卡每月额度固定runtime还款日就是截止时间deadline。如果你这个月提前把额度刷完了银行不会追加额度而是告诉你“剩下的消费下个月再结”。这样至少保证其他信用卡用户不受你超额消费的影响。正因如此即使某个 DL 任务因为 bug 或数据异常偶尔超时运行它也不会无限侵占同 CPU 上其他 DL 任务的带宽最坏情况只是它自己的截止时间被推后这比 RT 任务写死循环导致系统完全卡死要温和得多。在我实际测试中这个特性非常重要。做实时系统的人多多少少都遇到过“某个偶发任务计算量失控”的情况。DL 的 CBS 机制相当于一道天然保险即使你的实时任务偶尔抽风系统其余部分仍然保有可预测性。2.3 准入控制不是你想设多少就能设多少为了防止用户把所有 CPU 时间都分配给 DL 任务从而饿死系统内核在设置 DL 调度参数时有一道准入控制Admission Control检查。简单说当你调用sched_setattr()给任务设置 SCHED_DEADLINE 时内核会检查这个 CPU或这个 cgroup 分区上所有 DL 任务的带宽之和看是否超过一个上限。内核中关于 DL 带宽的默认配置和 RT 的带宽限制是独立的。一般来说默认配置下单个 CPU 上的 DL 带宽总和可以占到接近 100%但这不代表你应该真的去占满。如果占满留给中断处理、内核线程、系统管理任务的 CPU 时间就几乎为零系统会出现各种诡异问题。准入控制的具体实现里当一个任务设置了 DL 参数并绑定到某个 CPU或某个根域时内核会把 runtime/period 的比值加到该 CPU 的 DL 带宽统计值上。如果统计值超过上限设置就会失败返回EBUSY。这一点和 CFS 的带宽控制cfs_bandwidth思路类似但 DL 的准入控制是强制性的。如果你在设置 DL 参数时遇到BUSY错误通常就是准入控制拒绝了请求。可以通过调低 runtime 或增大 period 来降低带宽占用也可以把任务绑定到其他负载较低的 CPU 上。2.4 任务的周期调度与抢占规则在 DL 调度器中任务并不是一直在运行队列里等待的。每个任务被唤醒时内核会根据其参数计算一个“绝对截止时间”然后将任务插入红黑树。当任务运行完当前作业比如执行完一个周期内的业务代码并主动睡眠时下一个周期到来时再次被唤醒重新计算新的截止时间。抢占规则是这样的当一个低截止时间的 DL 任务到达运行队列时如果当前 CPU 正在运行一个截止时间更晚的 DL 任务或 RT/CFS 任务就会触发抢占。由于 DL 优先级高于 RT 和 CFS所以 DL 任务出现时基本可以立刻拿到 CPU。这里有个容易忽略的细节DL 调度器的抢占是基于任务的 deadline 动态变化的。当正在运行的 DL 任务自身预算被耗光、触发 CBS 推后 deadline 时它的“优先级”会瞬间降低红黑树会把另一个截止时间更早的任务排上来。这个动态变化的过程不需要用户手动干预完全由内核自动完成。3. 动手实操如何让任务跑在 DL 调度器上3.1 环境准备与内核版本确认首先确认内核版本。我测试所用的环境是 Linux 5.15 LTS内核主线从 3.14 开始支持 SCHED_DEADLINE但早期版本还有一些不稳定问题建议至少使用 5.4 以上的内核版本这样调度逻辑和 cgroup 交互相对成熟。可以用以下命令确认当前内核是否支持 DL 调度器grep -i deadline /boot/config-$(uname -r)如果输出里有CONFIG_SCHED_DEADLINEy就说明内核已经编译了 DL 支持。绝大多数主流发行版Ubuntu、Debian、CentOS 等默认是开启的。另外还要确认你是否有权限修改任务的调度策略。DL 调度器属于实时调度类设置实时调度参数通常需要 root 权限或者拥有CAP_SYS_NICE权限。普通用户直接调用会得到EPERM。3.2 设置 DL 参数runtime、deadline、period 的关系设置 DL 调度器最核心的接口是sched_setattr()系统调用。这里先写一个最小的 C 程序把当前线程设置成 SCHED_DEADLINE#define _GNU_SOURCE #include stdio.h #include string.h #include sched.h #include sys/syscall.h #include unistd.h #include linux/sched/types.h #include errno.h static int sched_setattr(pid_t pid, const struct sched_attr *attr, unsigned int flags) { return syscall(__NR_sched_setattr, pid, attr, flags); } int main(void) { struct sched_attr attr; memset(attr, 0, sizeof(attr)); attr.size sizeof(attr); attr.sched_policy SCHED_DEADLINE; attr.sched_priority 0; attr.sched_runtime 300000; // 300us attr.sched_deadline 1000000; // 1ms attr.sched_period 1000000; // 1ms if (sched_setattr(0, attr, 0) 0) { perror(sched_setattr); return 1; } printf(Current task is now under SCHED_DEADLINE\n); while (1) { // 模拟周期工作 } return 0; }这里最关键的约束是三个参数必须满足sched_runtime sched_deadline sched_period。如果违反了这个大小关系内核会返回EINVAL。为什么必须满足这个关系因为 runtime 是任务每周期内的最大执行预算deadline 是任务必须完成的时间点period 是任务释放的周期。如果你的 runtime 大于 deadline说明任务允许执行的时间比截止时间还长这在逻辑上就矛盾了。如果 deadline 大于 period意味着任务还没执行完下一个周期就来了就会产生作业重叠调度器同样没法保证可调度性。我有一次在做测试时把 runtime 设成 2ms、deadline 设成 1ms结果程序直接报Invalid argument。一开始以为是接口用错了后来查了内核文档才发现是参数关系没满足这个坑新手非常容易踩。3.3 绑定 CPU 与设置 CPU 亲和性在实际使用中很少有场景会真的让 DL 任务在多个 CPU 之间来回迁移。因为 DL 调度器在任务迁移时需要把红黑树里的节点从一个 CPU 的运行队列搬到另一个 CPU 上这个过程有锁开销和缓存失效开销对实时延迟非常不友好。推荐的做法是先把 DL 任务绑定到一个独占 CPU 上。可以用传统的sched_setaffinity()也可以用taskset命令启动进程taskset -c 2 ./dl_test把 DL 任务绑定到 CPU 2 上以后还要考虑这个 CPU 上是否还有其他 CFS 任务在跑。一般来说最好把 CPU 2 从普通的负载均衡中隔离出来这样就不会有乱七八糟的任务跑来和你的 DL 任务抢 CPU。内核提供isolcpus启动参数来隔离 CPUisolcpus2把 CPU 2 设为隔离 CPU 后普通 CFS 任务不会自动被调度到这个 CPU 上只有显式绑定到此 CPU 的任务才会运行。这个参数对 DL 和 RT 任务都是有效的。不过要注意隔离 CPU 只是“不主动迁移普通任务过去”并不是彻底独占中断仍然可能打到这个 CPU 上。如果要进一步减少中断干扰可以配合irqaffinity或中断亲和性设置把中断迁移到其他 CPU。这里顺便提一个我自己的经验如果只是为了做实验验证 DL 调度器不必一上来就搞isolcpus。先在普通 CPU 上绑一个核跑起来看调度延迟和上下文切换的数据等确认逻辑没问题后再考虑隔离。否则环境配置太复杂出了问题反而不知道是 DL 参数问题还是 CPU 隔离问题。3.4 用 chrt 快速测试 DL 参数如果不想编译 C 程序可以用chrt命令快速测试chrt -d --sched-runtime 300000 --sched-deadline 1000000 --sched-period 1000000 0 ./dl_testchrt的-d表示 SCHED_DEADLINE后面依次传入 runtime、deadline、period单位是纳秒最后的0是优先级DL 不用优先级。这个命令适合在脚本里快速启动 DL 任务。不过我建议还是用系统调用sched_setattr()来做因为实际项目中往往需要动态修改任务的调度参数而chrt只能在进程启动时设置一次不太灵活。3.5 验证调度行为用工具看延迟跑起来之后怎么确认 DL 调度器真的有效我常用的验证方式是测量任务的唤醒延迟。DL 任务通常以周期方式运行睡眠一个周期然后被唤醒执行工作再睡眠。唤醒延迟就是从内核唤醒该任务到任务实际在用户态开始执行的时间间隔。这个间隔越小、越稳定说明调度效果越好。可以用cyclictest来自 rt-tests 包来测也可以自己写一个带时间戳的测试程序来统计struct timespec ts; clock_gettime(CLOCK_MONOTONIC, ts); // 打印每次唤醒时的 timestamp我在这台普通 x86 机器上做了一个简单测试一个定时周期 1ms 的任务在 CFS 下运行唤醒延迟大约在 50us 到 200us 之间波动偶尔会飙到 300us 以上同样一个任务设为 SCHED_DEADLINEruntime 300usdeadline 1ms后激活延迟基本稳定在 10us 到 40us 之间抖动明显变小。这里要说明一下这个数据并不是严谨的实时系统 benchmark只是给大家一个直观感受——DL 调度的优势在于“稳定”二字而不是极致的“快”。另外可以通过/proc/pid/sched里的调度统计信息来观察 DL 任务是否被节流cat /proc/$(pidof dl_test)/sched如果看到dl_throttled次数在增长说明任务经常用完 runtime 预算需要适当增加 runtime 或调长 period。3.6 内核触发器和 tracepoint 的观测方式对于想看得更细的人可以用 ftrace 的调度事件来跟踪 DL 任务的实际调度过程。内核提供了sched_switch、sched_wakeup、sched_pi_setprio等 tracepoint也可以使用sched_overutilized这类 DL 专属的事件。cd /sys/kernel/tracing echo 0 tracing_on echo sched_switch set_event echo sched_wakeup set_event echo 1 tracing_on然后再跑 DL 任务结束后查看 trace 文件能看到完整的上下文切换记录。我通常在排查“任务为什么延迟”的问题时会开这个 trace效果比瞎猜好太多。4. 调度类对比DL 和 RTFIFO/RR到底差在哪何时选谁4.1 固定优先级 vs 动态截止时间在选型时很多人会问既然有 SCHED_FIFO 和 SCHED_RR为什么还要用 SCHED_DEADLINE这就要回到两者的调度语义差异。RT 调度类使用固定优先级编号越小优先级越高调度器永远运行当前 ready 队列里优先级最高的任务。这种设计好处是简单、可预测但它本身不关心任务的运行时长和释放周期。如果一个 RT 任务运行时间过长或者多个 RT 任务之间优先级设置得不合理就会产生严重的延迟抖动甚至导致低优先级 RT 任务饿死。DL 调度器则不同它给每个任务分配 CPU 带宽runtime/period任务之间按截止时间动态排序。也就是说即使两个 DL 任务的 runtime 和 period 完全一样内核也能保证它们各自获得的带宽不会互相侵占。用一个类比来理解RT 的固定优先级像“vip 通道”vip 客人来了永远优先但谁也不知道一个 vip 客人会占用饭店多长时间DL 则像“预约制度”每个客人在预约时段内用餐时间一到就得让位下一个预约的客人不会被无限等待。4.2 表格对比DL 与 RT 的关键差异我整理了一个简表方便大家对照对比维度SCHED_FIFO / SCHED_RRSCHED_DEADLINE调度依据静态优先级动态截止时间是否声明运行预算不声明必须声明 runtime是否能防止预算失控不能能CBS 机制实现复杂度低较高适用任务短小、可控的实时任务周期明确的实时任务资源隔离性弱高优先级任务可饿死低优先级强按带宽隔离内核支持版本一直有3.14从这个表可以看出来DL 更强调“带宽隔离”和“可调度性保证”。如果你的任务本来就是周期性的运行时间上下浮动比较大DL 会是比 RT 更安全的选择。4.3 什么场景仍然选择 RT先说结论不是所有实时任务都无脑用 DL。如果你的任务非常简单比如一个 GPIO 中断处理线程每次唤醒后就是几十微秒的活那么用 SCHED_FIFO 完全足够因为任务本身开销小不容易造成长时间抢占。而且 FIFO 的语义更直接优先级设最高来了就干干完就睡不需要考虑 runtime、deadline 这些参数学问。另外如果任务的执行时间高度不稳定同一个周期内有时执行 50us有时执行 2msDL 的 CBS 机制反而会频繁触发预算透支和 deadline 推迟导致任务的实际完成时间变得不可预测。这种情况下反而需要人为降低优先级或改用 RT依靠自己严格审查代码来控制执行时间。所以我的建议是任务周期明确、执行时间相对稳定、但需要防止个别周期超时影响系统整体——选 DL任务简短、执行时间完全可控、且需要绝对最高优先级——选 RT。这俩不是替代关系是互补关系。4.4 DL 的局限与适用边界DL 并不是万能药它也有自己的局限性。第一DL 任务的总带宽不能超过 CPU 的承受能力否则准入控制会拒绝设置。这就意味着如果一个 CPU 上要跑很多任务带宽分配必然紧张要仔细做可调度性分析。第二DL 调度器主要针对 CPU 时间不解决中断延迟、锁竞争等你代码本身的问题。即使 DL 保证任务能在截止时间前得到 CPU如果任务访问的驱动有 bug、持锁时间过长照样会出问题。DL 只保证 CPU 调度这一环其他环节还得靠自己的工程能力。第三DL 任务对系统其他部分的影响较大。因为 DL 优先级高于 RT如果一个 DL 任务长期占满带宽RT 任务和中断线程都会受影响。所以生产环境用 DL必须非常克制地申请带宽保守估计 runtime宁可用不到也别把系统逼到极限。5. 实战避坑DL 调度器使用中的问题与调试思路5.1 问题一设置 DL 参数时遇到 EINVAL这是最常见的错误绝大多数原因是 runtime、deadline、period 三者的关系不满足。标准的检查顺序runtime 是否大于 0deadline 是否大于等于 runtimeperiod 是否大于等于 deadline三个条件任何一个不满足内核都会直接返回EINVAL。遇到这个错误时先打印出三个参数看看有没有写反。曾经我把 deadline 和 period 搞反了runtime100usdeadline1msperiod500us这明显不符合 deadline period 的约束结果组里新同事排查了大半天才找到问题。另外sched_attr.size字段不要漏掉。内核从 3.14 到 6.x 版本struct sched_attr的大小有过扩展如果 size 设置不对早期的内核会返回EINVAL而较新的内核可能默认调整。稳妥做法是attr.size sizeof(struct sched_attr)不要用一个固定常量。5.2 问题二设置成功但任务没有周期性执行有时候 DL 任务设置成功了但看起来没按照预期周期运行。这时候先确认任务代码本身是不是真的每个周期都在运行。DL 调度器只是保证“在截止时间前有机会运行”如果你的任务在周期内主动睡眠的时间过长或者一直在等待某把锁调度器也没法强制它运行。另一个容易忽略的原因是任务设置成 DL 后又调用了sched_setaffinity()改变 CPU 亲和性导致任务被迁移到了另一个没做隔离的核上。DL 任务迁移时红黑树节点会从旧 CPU 的 rq 中删除再插入新 CPU 的 rq 中这个过程本身就有延迟。如果你能感觉到任务周期抖了一下多半就是迁移造成的。所以我在项目里关于 DL 任务的亲和工作一般只做两次第一次在任务启动时绑定 CPU第二次就再也不改了。后续任何需要换核的操作都通过重新创建任务来完成。5.3 问题三DL 任务出现dl_throttled次数增长在/proc/pid/sched里如果看到dl_throttled字段的数字不断变大说明任务经常花光 runtime 预算被调度器强制节流。这会让任务的实际完成时间比预期晚严重时就像“动不动被踩刹车一样”。排查思路有两条一是看任务的实际执行时间是否超过了声明的 runtime。比如你声明的 runtime 是 300us但任务的单次执行耗时波动到 400us那么预算肯定会被打穿。这种情况要么调大 runtime要么优化代码缩短执行时间。二是看任务的唤醒间隔是否稳定。如果 wakeup 时间点不稳定下一个作业的释放时间和 deadline 计算也会乱套。我曾经调试一个视觉定位算法单次计算时间受图像内容影响好的时候 200us坏的时候 500us。如果 runtime 设 300us坏帧来了就会被节流。后来把 runtime 提高到 500usperiod 和 deadline 不变才让任务稳定运行。代价是每个周期的最大带宽占用率从 30% 提升到 50%但在可控范围内。5.4 问题四DL 任务把系统拖死如果你把 runtime 和 period 设置得太大比如一个 CPU 上只跑一个 DL 任务runtime 700usperiod 1ms那这个 CPU 的 70% 带宽都被 DL 任务占了剩下的时间留给 RT、CFS 和中断。如果这时系统里有网络中断等实时性需求它们会因为长时间得不到 CPU 而出现延迟异常。我的建议是给非 DL 任务留出至少 20%~30% 的带宽余量。尤其在多任务混跑场景下DL 任务占用的带宽上限应该有一个合理规划。如果只是做实验最稳妥的方式是先用isolcpus隔离出一个 CPU 专门给 DL 任务使用这样至少不会因为 DL 任务带宽设置不当而影响系统其他部分。5.5 问题五如何观测 DL 调度器是否真的“按截止时间调度”用 ftrace 观测是最直接的。设置好事件后能看到每次调度切换是由哪个任务切换到哪个任务echo 0 /sys/kernel/tracing/tracing_on echo sched_switch /sys/kernel/tracing/set_event echo 1 /sys/kernel/tracing/tracing_on cat /sys/kernel/tracing/trace在输出里找dl相关的行重点看从 DL 任务切换到其他任务时切换点的绝对时间是否符合预期。如果 DL 任务在 deadline 之后才被调度上 CPU说明要么 runtime 设置偏小要么有其他硬中断/关闭抢占的代码路径抢占了系统。另外/proc/sched_debug里会输出每个 CPU 运行队列的详细信息包括当前任务、next 任务、运行队列上等待的任务。运行cat /proc/sched_debug | grep -A 20 rq时能看到 DL 队列中有哪些任务以及它们的 deadline 分布。这对确认“多个 DL 任务是否真的按截止时间排序”很有帮助。5.6 我的实践经验小结最后总结下我个人的实操体会。DL 调度器不是那种“开了就万事大吉”的开关。它的价值在于如果你愿意花心思把任务的 runtime 摸准、把周期设计合理就能获得比 RT 更可控的实时行为。反过来如果只想着“把 deadline 设小一点就能更快”DL 会很快给你上一课因为准入控制会拒绝你或者运行时 CBS 不断节流反而让任务更不稳定。我最开始用 DL 时犯过一个典型错误为了追求低延迟把 deadline 设成 200usperiod 设成 1msruntime 设成 150us。结果任务经常EBUSY因为和系统里其他 DL/RT 任务的带宽加在一起超出了 CPU 的准入上限。后来源码里查了准入逻辑才明白这是内核在做保护。所以我现在的操作习惯是先测量任务的真实执行时间至少测 100 个周期取 p99 值再留 20% 余量作为 runtimedeadline 设为 runtime 的 2~3 倍period 根据业务需求确定保证 deadline 在周期内再用isolcpus隔离 CPU减少外部干扰跑起来之后持续观察/proc/pid/sched里的dl_throttled、nr_switches等指标确认没有节流稳定性测试至少跑 24 小时实时系统最怕偶发问题短时间跑不出问题不代表真的没问题DL 调度器这块后续还能延展不少比如 cgroup 与 DL 带宽的交互、多 CPU 迁移优化、RT 和 DL 混布时的带宽分配策略等等。如果大家在真实项目里遇到有代表性的问题欢迎留言交流我会把有价值的问题整理成后续的排查案例。
返回列表