Skip to content

调度程序有一个简单的任务:它从就绪线程(就绪队列)的队列中选取一个线程作为当前活动的正在运行的线程,并将 CPU 交给它。 决定选择哪个线程作为运行线程是 100% 确定性的,因此规则确定每个线程的重要性(称为优先级)。由于调度程序是 RTOS 调度程序,因此它不考虑线程的公平性或执行历史记录,这意味着您作为固件开发人员必须通过为每个线程设置正确的优先级来决定线程如何共享给定的 CPU。

Context switch 上下文切换

在线程执行期间,将使用 CPU 寄存器,并访问 RAM 和 ROM。组合的资源(包括处理器寄存器和堆栈)构成了线程的上下文。 线程遵循顺序代码流,而不知道何时会被抢占(被调度程序)或中断(被 ISR)。考虑这样一种情况:在执行减去存储在两个 CPU 寄存器(R0 和 R1)中的值( 0x05 和 0x05 )的指令之前,线程被抢占。当线程被抢占时,其他线程将运行,并且很可能会修改 CPU 寄存器值。当线程重新调度时,它不知道这些更改,如果它在减法中使用修改后的 CPU 寄存器值,则可能导致不正确的结果。 为防止出现此类错误,线程恢复时的上下文必须与抢占前的上下文完全相同。RTOS 通过在线程被抢占时保存其上下文,并在其恢复执行前恢复其上下文来确保这一点。这种在抢占线程时保存上下文、在恢复线程时恢复上下文的过程称为上下文切换 。 请注意,上下文切换确实会消耗一些时间,因为它涉及到数据复制。作为固件开发人员,应尽可能减少固件中上下文切换的次数。而且,中断也会发生上下文切换。

Thread types

Preemptable threads 抢占线程

可抢占线程是用户应用程序最常用的线程。之所以称其为可抢占线程,是因为如果存在优先级更高的线程,调度程序可以抢占它们。

Cooperative threads 合作线程

合作线程的创建方式与可抢占线程相同,只是传递给 K_THREAD_DEFINE() 的优先级为负数。合作线程的主要特点是调度程序无法抢占它们的优先级,这意味着合作线程将一直运行,直到它通过故意休眠、等待、调用使线程未就绪的 API 或屈服而停止运行为止。 合作线程的主要用途是执行调度器锁定。将任务作为合作线程执行时,可以确保其他线程不会抢先执行任务,因此不必担心同步和锁定问题。 合作线程用于某些子系统、网络堆栈和设备驱动程序,以实现互斥(调度器锁定)。它们也可用于某些对性能要求极高的用户应用程序。

Meta-IRQ threads Meta-IRQ 线程

Meta-IRQ 线程是一种特殊的合作线程。虽然这类线程不是为用户应用程序设计的,但仍需注意。Meta-IRQ 线程用于设备驱动程序的 "下半部分 "工作负载,随着硬件 ISR 的结束而触发。 中断可以在任何时候异步发生,如果中断发生在一个合作线程运行时,执行将保证返回到被中断的合作线程。但是,如果 ISR 有特别紧急的工作需要在中断后立即在线程上下文中完成,解决办法是使用 Meta-IRQ 线程。 通过将驱动程序的 "下半部分"(例如蓝牙低功耗堆栈)指定为 Meta-IRQ 线程,就能保证中断紧接着触发该线程。

Thread Priority 线程优先级

调度器根据优先级区分可抢占线程和合作线程:优先级为负的线程被归类为合作线程,优先级为非负的线程被归类为可抢占线程。 非负优先级的数量可通过 Kconfig 符号 CONFIG_NUM_PREEMPT_PRIORITIES 进行配置,默认为 15。主线程的优先级为 0,而空闲线程的默认优先级为 15。如果在延迟模式下使用日志记录器模块,则日志记录器线程的优先级为 14。 同样,负优先级的数量可通过 Kconfig 符号 CONFIG_NUM_COOP_PRIORITIES 进行配置,默认为 16。这意味着优先级-1 至-16 可用于合作线程,系统工作队列线程作为优先级为-1 的合作线程执行。 线程启动后,可以动态更改其初始优先级。这也意味着,如果优先级从非负变为负,可抢占线程就有可能变成合作线程,反之亦然。

Scheduler locking and disabling interrupts

调度程序锁定和禁用中断 调度器锁定是实时操作系统中的一种机制,它暂时锁定或禁用调度器,以防止不同线程或进程之间的上下文切换。调度器锁定可确保代码的特定部分或关键区域以原子方式执行,不受其他线程的干扰。

  • 对于合作线程来说,它是自动完成的。合作线程内置了调度器锁定机制。
  • 对于常规的可抢占式线程,有两个与调度器锁定相关的函数:k_sched_lock() 用于锁定线程,k_sched_unlock() 用于解锁线程。k_sched_lock() 函数能有效地将当前线程提升到合作优先级,即使没有配置合作优先级。该函数并不是应用代码广泛使用的机制。 调度器锁定并不能阻止中断。要保护关键代码段不被调度程序抢占和不被 ISR 中断,可以使用 irq_lock()irq_unlock() 函数。

Threads with equal priority

具有同等优先级的线程 可以有多个具有相同优先级的线程,但专用于空闲线程的优先级除外( CONFIG_NUM_PREEMPT_PRIORITIES 默认为 15)。 默认行为:调度程序将运行在就绪队列中第一个被设置为就绪的线程。 时间分割:每个具有相同优先级的线程都有固定的运行时间。时间用完后,调度程序将抢占当前线程,允许其他同等优先级的线程运行。nRF Connect SDK 中的时间切分只影响具有相同优先级的线程。使用 CONFIG_TIMESLICING Kconfig 符号可启用时间片。 最早截止时间优先(EDF)调度:固件开发人员必须调用 k_thread_deadline_set(),为每个线程提供预计截止时间。当存在多个优先级相同的线程时,调度器将选择截止时间最早(周期最短)的线程。启用此选项的 Kconfig 符号是 CONFIG_SCHED_DEADLINE。如果启用 EDF,每个线程设置截止时间的责任在于开发人员,而不是 RTOS。

Rescheduling Points 重新安排点

nRF Connect SDK 使用的 Zephyr 内核默认为无 tick 内核。它移除了由系统 tick 硬件产生的周期性定时器中断或 "ticks",因此具有显著的省电优势。在传统的操作系统内核中,无论系统的工作量如何,都会以固定的时间间隔(例如每隔几毫秒)产生一个周期性定时器中断,即系统 tick。这个 tick 是包括调度在内的各种操作系统功能的参考点。 许多计算机系统在空闲或低功耗状态下花费大量时间,不需要频繁的定时器中断。在这种情况下,产生不必要的滴答声会导致功耗增加和效率降低。 调度程序不依赖于固定的时间间隔来检查哪个线程应该是下一个运行的线程,而是依赖于称为重新调度点的东西。重新调度点是调度程序被调用以选择下一个运行线程的瞬间。只要就绪线程的状态发生变化,就会触发重新调度点。 重新调度点的一些示例如下 当线程调用 k_yield() 时,线程的状态将从“正在运行”更改为“就绪”。接下来可能会运行其他一些线程

  • 如果一个线程通过调用 k_sleep() 进入睡眠状态,则接下来需要运行其他线程。
  • 通过提供或发送内核同步对象(如信号量、互斥锁或警报)来取消阻止线程会导致线程的状态从“未就绪”更改为“就绪”。
  • 当接收线程使用传递内核对象的数据从其他线程获取新数据时,接收线程的状态将从“等待”更改为“就绪”。
  • 如果启用了时间切片,并且线程已连续运行了允许的最大时间片时间,则线程的状态将从“正在运行”更改为“就绪”。

Released under the GPL License.