)
7. 实时调度与Deadline调度器实现Linux内核不仅提供了面向普通进程的CFS调度器还提供了实时调度器和Deadline调度器用于满足对响应延迟、执行确定性有严格要求的场景比如工业控制、车载系统、机器人、金融交易等。这两个调度器的优先级高于CFS只要有可运行的实时/Deadline进程CFS的普通进程就不会被调度。很多工程师对实时调度的认知停留在「优先级高的先运行」却不理解实时调度的实现细节、带宽限制、优先级反转等核心问题最终导致实时进程占满CPU、系统hang住、实时性不达标等严重问题。本章节基于Linux 6.6 LTS内核完整拆解实时调度器与Deadline调度器的实现、工程使用与避坑指南。7.1 实时调度的核心概念与分类实时系统的核心要求是确定性任务必须在规定的截止时间内完成而不是追求最大的吞吐量。实时系统分为两类硬实时系统任务必须在截止时间内完成超时会导致系统灾难性的后果比如航空航天、车载自动驾驶、工业机器人控制软实时系统任务希望在截止时间内完成偶尔超时不会导致严重后果比如音视频播放、金融交易、实时数据采集。Linux内核提供了两种实时调度机制POSIX实时调度器基于优先级的调度支持SCHED_FIFO和SCHED_RR两种调度策略优先级范围0~99数值越大优先级越高属于软实时调度Deadline调度器基于截止时间的调度支持SCHED_DEADLINE调度策略优先级高于POSIX实时调度器能提供硬实时能力满足确定性的截止时间要求。核心优先级规则调度器的优先级从高到低依次是停机调度类 Deadline调度类 实时调度类 CFS调度类 空闲调度类。只要高优先级的调度类有可运行的进程低优先级的进程永远不会被调度。7.2 POSIX实时调度器SCHED_FIFO与SCHED_RRPOSIX实时调度器是Linux最早支持的实时调度机制符合POSIX.1b标准提供了两种调度策略核心都是基于固定优先级的抢占式调度。7.2.1 核心调度策略详解1. SCHED_FIFO先进先出实时调度SCHED_FIFOFirst In First Out是最简单的实时调度策略核心规则固定优先级每个SCHED_FIFO进程有一个固定的实时优先级范围1~990保留给CFS调度器使用数值越大优先级越高无时间片限制SCHED_FIFO进程一旦被调度就会一直运行直到它主动放弃CPU调用sleep、wait、sched_yield等、被更高优先级的实时进程抢占、或者进程退出同优先级调度相同优先级的SCHED_FIFO进程按先进先出的顺序调度只有当前运行的进程主动放弃CPU同优先级的下一个进程才会被调度绝对抢占只要有更高优先级的SCHED_FIFO进程变为就绪状态就会立即抢占当前运行的低优先级进程无论低优先级进程正在做什么。2. SCHED_RR轮询实时调度SCHED_RRRound Robin是SCHED_FIFO的扩展核心规则和SCHED_FIFO完全一致唯一的区别是相同优先级的SCHED_RR进程有固定的时间片。时间片大小默认值和CFS的调度延迟一致可通过/proc/sys/kernel/sched_rr_timeslice_ms设置默认100ms调度规则相同优先级的SCHED_RR进程按时间片轮询调度进程用完时间片后会被放到同优先级就绪队列的末尾调度下一个同优先级的进程抢占规则和SCHED_FIFO一致更高优先级的进程会立即抢占低优先级进程永远无法抢占高优先级进程。7.2.2 实时调度器的内核实现实时调度器的核心数据结构是struct rt_rq每个CPU的全局就绪队列struct rq中都有一个独立的实时就绪队列管理该CPU上的所有实时进程。1. 实时就绪队列rt_rqstruct rt_rq { // 每个优先级对应一个就绪队列共100个优先级0~99 struct rt_prio_array active; // 最高优先级的就绪进程的优先级 unsigned int highest_prio; // 就绪队列中的实时进程总数 unsigned int nr_running; // 带宽控制相关字段 u64 runtime; u64 runtime_expires; int rt_throttled; // 过载相关字段 int overloaded; struct list_head pushable_tasks; }; // 优先级数组每个优先级对应一个链表 struct rt_prio_array { DECLARE_BITMAP(bitmap, MAX_RT_PRIO1); // 优先级位图快速找到最高优先级的非空队列 struct list_head queue[MAX_RT_PRIO]; // 每个优先级的就绪进程链表 };核心设计亮点用数组位图的方式管理就绪队列每个优先级对应一个链表用位图标记哪些优先级有就绪进程找到最高优先级的就绪进程只需要找到位图中第一个置1的位时间复杂度O(1)比CFS的红黑树更快满足实时性要求每个CPU有独立的rt_rq最小化多核之间的锁竞争提升多核扩展性。2. 核心调度流程进程入队实时进程被唤醒时会根据它的优先级加入对应CPU的rt_rq的优先级链表中更新位图和highest_prio如果新进程的优先级高于当前运行的进程立即触发抢占选核逻辑调度器选下一个进程时直接取rt_rq中highest_prio对应的链表头的进程O(1)时间复杂度tick处理每个时钟tick到来时检查当前运行的实时进程是否是SCHED_RR策略如果是减少它的时间片时间片用完后把它放到同优先级链表的末尾触发调度主动放弃CPU进程调用sched_yield()时会把自己放到同优先级链表的末尾触发调度。7.2.3 实时调度的带宽控制如果实时进程进入死循环占满CPU会导致所有低优先级的CFS进程、甚至内核线程都无法运行系统会完全hang住无法响应ssh、串口输入甚至无法处理中断。为了避免这个问题Linux内核提供了实时带宽控制机制限制实时进程的CPU使用率。核心参数/proc/sys/kernel/sched_rt_period_us限流周期默认1000000us1s/proc/sys/kernel/sched_rt_runtime_us每个周期内实时进程能使用的总CPU时间默认950000us0.95s也就是实时进程最多占用95%的CPU时间预留5%给普通进程和内核线程如果设置为-1表示关闭带宽限制实时进程可以占用100%的CPU时间。工作机制每个周期开始时内核会重置每个CPU的实时运行时间配额实时进程每运行1us运行时间就加1us当运行时间超过sched_rt_runtime_us时实时进程会被限流无法被调度直到下一个周期开始带宽控制是per-CPU的每个CPU的实时进程配额是独立的。7.2.4 优先级反转问题与解决方案优先级反转是实时系统中最常见的致命问题也是很多工程师踩坑的地方。什么是优先级反转高优先级的进程需要获取一个被低优先级进程持有的锁而低优先级进程又被中等优先级的进程抢占无法释放锁导致高优先级进程被中等优先级进程阻塞优先级完全失效无法满足实时性要求。举个例子进程A优先级90最高需要获取锁lock进程B优先级50中等进程C优先级10最低持有锁lock进程C持有lock时被进程B抢占无法释放lock进程A需要lock被阻塞只能等待进程C释放lock但进程C被进程B抢占无法运行最终结果中等优先级的进程B抢占了CPU最高优先级的进程A无法运行出现优先级反转。解决方案优先级继承协议PILinux内核通过优先级继承协议Priority Inheritance解决优先级反转问题核心规则当高优先级进程等待一个被低优先级进程持有的锁时低优先级进程的优先级会被临时提升到高优先级进程的优先级低优先级进程释放锁后优先级会恢复到原来的水平这样中等优先级的进程无法抢占低优先级进程低优先级进程可以快速执行完成释放锁让高优先级进程继续运行。工程实现Linux内核的互斥锁pthread_mutex_t原生支持优先级继承只需要在初始化锁时设置PTHREAD_PRIO_INHERIT属性pthread_mutexattr_t attr;pthread_mutex_t mutex;pthread_mutexattr_init(attr);// 设置优先级继承属性pthread_mutexattr_setprotocol(attr, PTHREAD_PRIO_INHERIT);pthread_mutex_init(mutex, attr);避坑提醒实时系统中所有进程间共享的锁必须开启优先级继承否则一定会出现优先级反转问题导致实时性不达标甚至系统死锁。7.3 Deadline调度器SCHED_DEADLINEDeadline调度器是Linux 3.14版本引入的硬实时调度器优先级高于POSIX实时调度器基于**最早截止时间优先Earliest Deadline First, EDF**算法能提供确定性的截止时间保证满足硬实时系统的要求。POSIX实时调度器是基于优先级的无法保证任务的截止时间而Deadline调度器直接基于任务的截止时间调度能精确控制任务的运行时间和截止时间是Linux硬实时系统的首选。7.3.1 核心参数与调度模型Deadline调度器的每个任务都有三个核心参数单位都是纳秒Runtime任务在每个周期内需要的最大运行时间也就是任务的最坏执行时间WCETPeriod任务的执行周期也就是任务每隔多久需要执行一次Deadline任务的相对截止时间也就是任务在每个周期内必须在多长时间内完成默认等于Period。举个例子一个工业控制任务需要每隔20ms执行一次每次执行最多需要2ms必须在10ms内完成那么它的参数就是Runtime 2ms 2000000nsPeriod 20ms 20000000nsDeadline 10ms 10000000ns核心调度规则最早截止时间优先调度器永远选择截止时间最早的就绪任务来运行硬截止时间保证只要任务的总利用率不超过CPU核心数调度器就能保证所有任务都能在截止时间内完成接纳控制创建Deadline任务时内核会做接纳控制检查计算任务的利用率Runtime/Period如果总利用率超过CPU核心数会拒绝创建任务保证系统的可调度性绝对抢占只要有更早截止时间的任务变为就绪状态就会立即抢占当前运行的任务无论当前任务的优先级如何。7.3.2 Deadline调度器的内核实现Deadline调度器的核心数据结构是struct dl_rq每个CPU的全局就绪队列struct rq中都有一个独立的Deadline就绪队列管理该CPU上的所有Deadline任务。1. Deadline就绪队列dl_rqstruct dl_rq { // 红黑树按任务的绝对截止时间排序 struct rb_root_cached root; // 就绪队列中的Deadline任务总数 unsigned int nr_running; // 带宽控制相关字段 u64 runtime; u64 deadline; int dl_throttled; // 过载相关字段 int overloaded; struct list_head pushable_dl_tasks; };核心设计用带缓存的红黑树管理就绪任务按任务的绝对截止时间排序截止时间最早的任务在红黑树的最左节点缓存最左节点选核时间复杂度O(1)每个CPU有独立的dl_rq支持多核负载均衡和任务迁移保证实时性原生支持带宽控制和接纳控制保证系统的可调度性。2. 核心调度流程接纳控制创建Deadline任务时内核计算任务的利用率U Runtime / Period如果系统中所有Deadline任务的总利用率超过CPU核心数会拒绝创建任务保证所有任务都能在截止时间内完成任务入队任务被唤醒时计算它的绝对截止时间加入dl_rq的红黑树中如果新任务的截止时间早于当前运行的任务立即触发抢占选核逻辑调度器选下一个任务时直接取红黑树的最左节点截止时间最早的任务运行时间控制任务运行时消耗它的Runtime当Runtime耗尽时任务会被限流直到下一个周期开始Runtime重置任务重新变为就绪状态周期更新每个周期开始时内核会重置任务的Runtime更新绝对截止时间把任务重新加入就绪队列。7.3.3 Deadline调度器与POSIX实时调度器的核心区别特性POSIX实时调度器SCHED_FIFO/RRDeadline调度器SCHED_DEADLINE调度依据固定优先级任务的截止时间实时性保证软实时无法保证截止时间硬实时只要通过接纳控制就能保证截止时间优先级1~99数值越大优先级越高优先级高于所有POSIX实时进程资源利用率无限制可能占满CPU接纳控制限制总利用率保证可调度性多核支持弱负载均衡能力有限强原生支持多核任务迁移和负载均衡适用场景简单的软实时场景优先级明确的任务硬实时场景有明确截止时间的周期性任务7.4 实时系统的工程实践与避坑指南7.4.1 实时进程的正确使用规范1.绝对禁止实时进程死循环实时进程的优先级高于所有普通进程和内核线程如果实时进程进入死循环没有主动放弃CPU会导致系统完全hang住哪怕开启了带宽控制也会有5%的CPU预留可能无法满足系统基本运行需求。最佳实践实时进程的主体必须是周期性的每次执行完成后调用sleep()等待下一个周期绝对不能写死循环实时进程的执行时间必须严格控制不能超过设定的Runtime/时间片。2.实时进程必须绑定CPU核心实时进程如果在多个CPU之间迁移会导致缓存失效、调度延迟增加无法满足实时性要求。最佳实践给每个实时进程绑定一个独立的CPU核心避免和其他进程共享CPU预留1~2个CPU核心给系统内核和中断处理不要把所有CPU都分配给实时进程把中断亲和性设置到非实时进程的CPU核心上避免中断打断实时进程。3.必须开启实时内核PREEMPT_RT标准Linux内核的实时性只能达到毫秒级要实现微秒级的硬实时必须开启CONFIG_PREEMPT_RT补丁也就是实时内核。PREEMPT_RT补丁的核心优化把自旋锁替换成可睡眠的rt_mutex支持优先级继承把中断处理函数线程化中断可以被实时进程抢占最小化关闭抢占的临界区降低最大调度延迟优化定时器精度支持高精度hrtimer。最佳实践硬实时场景必须使用PREEMPT_RT实时内核标准内核只能用于软实时场景。4.必须处理优先级反转问题实时系统中只要有锁的共享就一定会出现优先级反转问题必须开启优先级继承。最佳实践所有实时进程使用的互斥锁必须设置PTHREAD_PRIO_INHERIT属性禁止使用自旋锁自旋锁会关闭抢占增加调度延迟尽量减少锁的持有时间降低优先级反转的风险。5.禁止在实时进程中执行耗时操作实时进程的执行时间必须严格控制禁止在实时进程中执行以下操作调用malloc/free内存分配可能会触发缺页异常导致不确定的延迟执行磁盘IO、网络IOIO操作会导致阻塞延迟不可控调用系统调用除了最基础的时间、调度相关的系统调用其他系统调用都可能导致不确定的延迟使用复杂的算法执行时间不可控。最佳实践实时进程只执行核心的实时控制逻辑其他耗时操作都交给普通进程处理通过无锁队列通信。7.4.2 高频问题排查与解决方案实时进程调度延迟过高排查流程用cyclictest工具测试系统的最大调度延迟确认是否符合实时性要求检查是否开启了PREEMPT_RT实时内核标准内核的延迟无法达到微秒级检查实时进程是否绑定了固定的CPU核心是否和其他进程/中断共享CPU检查是否有其他更高优先级的实时进程在运行抢占了目标进程检查锁的使用是否正确是否出现了优先级反转用ftrace跟踪调度器的执行流程找到延迟的根源。系统hang住无法响应核心原因实时进程占满了CPU导致普通进程和内核线程无法运行。解决方案开启实时带宽控制不要把sched_rt_runtime_us设置为-1预留至少5%的CPU给系统给实时进程绑定部分CPU核心预留至少1个CPU核心给系统严格审核实时进程的代码避免死循环保证进程会主动放弃CPU。实时进程的截止时间无法满足排查流程检查任务的Runtime设置是否小于任务的最坏执行时间检查任务的总利用率是否超过了CPU核心数是否通过了接纳控制检查是否有更高优先级的进程/中断抢占了目标进程检查锁的持有时间是否过长是否出现了优先级反转用perf sched记录调度事件查看任务的调度延迟和执行时间。