概念

调度程序确定在任何时间点执行哪个线程,此线程称为当前线程
在不同的时间点,调度程序有机会更改当前线程的标识。这些点称为重新计划点。一些潜在的重新安排点是:

  • 将线程从运行状态转换为挂起或等待状态,例如通过k_sem_take()k_sleep()
  • 将线程转换为就绪状态,例如通过k_sem_give()k_thread_start()
  • 处理中断后返回到线程上下文
  • 当正在运行的线程调用k_yield()

当线程自愿启动将自身转换为挂起或等待状态的操作时,它将处于休眠状态

每当调度程序更改当前线程的标识时,或者当前线程的执行被 ISR 替换时,内核首先保存当前线程的 CPU 寄存器值。当线程稍后恢复执行时,将恢复这些寄存器值。

调度算法

内核的调度程序选择优先级最高的就绪线程作为当前线程。当存在多个具有相同优先级的就绪线程时,调度程序会选择等待时间最长的线程。
线程的相对优先级主要由其静态优先级确定。但是,如果启用了最早截止时间优先调度CONFIG_SCHED_DEADLINE,并且所选的线程具有相同的静态优先级,则认为具有较早截止时间的线程具有更高的优先级。因此,当启用了最早截止时间优先调度时,仅当两个线程的静态优先级和截止时间相等时,才会将其视为具有相同的优先级。可以通过k_thread_deadline_set()设置线程的截止时间。

ISR 的执行优先于线程的执行,因此当前线程的执行可能随时被 ISR 替换,除非中断已被屏蔽。这适用于协作线程和抢占线程。

内核的就绪队列算法:

Kconfig选项 作用 描述
CONFIG_SCHED_DUMB 单链表就绪队列 调度程序将绪队列实现为一个简单的无序列表,具有非常快的单线程恒定时间性能和非常低的代码大小。在系统线程并发少情况下可以使用此算法。
CONFIG_SCHED_SCALABLE 红/黑树就绪队列 调度程序将就绪队列将实现为红色/黑色树。这具有相当慢的恒定时间插入和删除开销,并且在大多数平台上需要额外的大约2kb代码。在系统线程并发多情况下可以使用此算法。
CONFIG_SCHED_MULTIQ 多队列就绪队列 调度程序将就绪队列将实现为列表数组,每个优先级一个(最多 32 个优先级)。它只产生很小的代码大小开销,并且在几乎所有情况下都以非常低的常数因子在O(1)时间内运行。但是它需要相当大的RAM预算来存储这些列表头。并且它不能实现按照线程的等待时常排序,且与SMP芯片不兼容。

等待队列的算法:

Kconfig选项 作用 描述
CONFIG_WAITQ_SCALABLE 平衡树的等待队列 如果有很多线程在等待各自的资源可以选择此选项,但此方式会造成在大多数平台上需要额外的大约2kb代码
CONFIG_WAITQ_DUMB 双链表的等待队列 如果有很多线程在等待统一资源可以选择此选项。相对于平衡树它很节省代码空间。

协作时间切片

一旦协作线程成为当前线程,它将保持当前线程,直到它执行完或者协作线程变为未就绪状态。因此,如果协作线程执行时间过长,则可能会导致其他线程的调度出现不可接受的延迟。
33.Zephyr的线程调度 - 图1
为了解决这个问题,协作线程可以不时地自愿放弃CPU,以允许其他线程执行。线程可以通过两种方式放弃 CPU:

  • k_yield(): 将线程放在调度程序的就绪线程优先级列表的后面,然后调用调度程序。
  • k_sleep(): 使线程在指定的时间段内处于未就绪状态。

抢占式时间切片

一旦抢占式线程成为当前线程,它将保持当前线程,直到更高优先级的线程准备就绪或者直到该线程成为未就绪状态。因此,如果抢占式线程执行时间过长,则可能会导致其他相同优先级的线程调度出现不可接受的延迟。
33.Zephyr的线程调度 - 图2
为了解决这个问题,抢占式线程可以执行协作时间切片,或者可以使用调度程序的时间切片功能来允许执行具有相同优先级的其他线程。
33.Zephyr的线程调度 - 图3
调度程序将时间划分为一系列时间片,其中片以系统时钟计时来度量。时间片大小是可配置的,但此大小可以在应用程序运行时更改。
在每个时间片结束时,调度程序检查当前线程是否可抢占,如果是,则线程隐式调用k_yield()。这为具有相同优先级的其他就绪线程提供了在再次调度当前线程之前执行的机会。如果没有相同优先级的线程准备就绪,则当前线程仍然是当前线程。
但如果出现高优先级线程,那就会直接执行高优先级的线程。

线程锁定

不希望在执行关键操作时被抢占的抢占线程可以通过调用k_sched_lock()来指示调度程序暂时将其视为协作线程
关键操作完成后,抢占线程必须调用k_sched_unlock()以恢复其正常的抢占状态。
如果线程调用k_sched_lock()之后执行使其未就绪的操作,则调度程序将切换锁定线程并允许其他线程执行。当锁定线程再次成为当前线程时,将保持其不可抢占状态。

线程休眠

线程可以调用k_sleep()以将其处理延迟指定的时间段。在线程休眠期间,CPU将被放弃以允许其他就绪的线程执行。一旦经过指定的延迟,线程就准备就绪,并有资格再次调度。

忙等待

线程可以调用k_busy_wait()来执行在指定时间段内延迟处理,而不会将CPU放弃给另一个就绪的线程。