在本单元中,我们将讨论 nRF Connect SDK 中的执行单元,即线程、ISR,此外我们还将讨论调度器及其默认行为。
线程是 RTOS 调度器(本主题稍后介绍)竞争 CPU 时间的最小逻辑执行单元。
在 nRF Connect SDK 中,主要有两种类型的线程:协作式线程(优先级值为负)和抢占式线程(优先级为非负)。协作式线程具有负优先级,且用途非常有限。因此,它们不在本课程的范围内。
线程在任何给定时间都可能处于以下状态之一。
运行中 (Running):运行中的线程是当前正由 CPU 执行的线程。这意味着 RTOS 的调度器已经选择该线程获取 CPU 时间,并将该线程的上下文加载到了 CPU 寄存器中。
可运行 (Runnable):当一个线程与其他线程或其他系统资源没有进一步执行的依赖关系时,该线程被标记为“可运行”。该线程唯一等待的资源就是 CPU 时间。调度器将这些线程包含在调度算法中,以便在当前运行线程改变状态后选择下一个要运行的线程。这也被称为“就绪 (Ready)”状态。
不可运行 (Non-runnable):具有一个或多个阻止其执行因素的线程被视为未就绪,且不能被选为当前线程。例如,这可能是因为它们正在等待尚未可用的某些资源,或者它们已被终止或挂起。调度器不会将这些线程包含在选择下一个运行线程的调度算法中。这也被称为“未就绪 (Unready)”状态。
系统线程是 Zephyr RTOS 在初始化期间自动生成的线程类型。默认情况下总是会生成两个线程:主线程 (main thread) 和空闲线程 (idle thread)。
主线程执行必要的 RTOS 初始化,并调用应用程序的 main() 函数(如果存在)。如果没有提供用户定义的 main(),主线程将正常退出,尽管系统仍将保持全功能运行。
空闲线程在没有其他工作可做时运行,它要么运行一个空循环,要么(如果支持)激活电源管理以节省功耗(Nordic 设备即是如此)。
除了系统线程外,用户还可以定义自己的线程来分配任务。例如,用户可以创建一个线程来委派读取传感器数据,创建另一个线程来处理数据,依此类推。线程被分配了优先级,这会指示调度器如何为该线程分配 CPU 时间。我们将在练习 1 中深入探讨如何创建用户定义的线程。
nRF Connect SDK 中另一个常见的执行单元是工作项 (work item),它不过是一个由名为工作队列线程的专用线程调用的用户定义函数。
工作队列线程是专用线程,用于以“先进先出”的方式处理从名为工作队列的内核对象中提取的工作项。每个工作项都有一个指定的处理函数,调用该函数来处理工作项。其主要用途是将非紧急工作从 ISR 或高优先级线程卸载到低优先级线程。
一个系统可以有多个工作队列线程,默认的一个被称为系统工作队列,可供任何应用程序或内核代码使用。处理系统工作队列中工作项的线程是一个系统线程,如果向系统工作队列提交工作项,则无需创建和初始化工作队列。

如上图所示,ISR 或高优先级线程将工作提交到工作队列中,专用工作队列线程按照先进先出 (FIFO) 的顺序提取工作项。从队列中提取工作项的线程在处理完一个工作项后总是会出让 (yield) CPU,这样其他同优先级的线程就不会被长时间阻塞。
将工作委派为工作项而不是专用线程的优势在于,由于工作项全部共享一个栈(即工作队列栈),因此工作项比线程更轻量,因为不需要分配额外的栈。
我们将在练习 3 中深入探讨工作项和工作队列线程。
线程被分配一个整数值来表示其优先级,该值可以是负数或非负数。数值越小优先级越高,这意味着优先级为 4 的线程将比优先级为 7 的线程获得更高的优先级。同样,优先级为 -2 的线程将比优先级为 4 和 7 的线程都具有更高的优先级。
调度器根据优先级将线程分为两种类型:协作式和抢占式。具有负优先级的线程被归类为协作式线程。一旦协作式线程成为当前线程,它将保持该状态,直到它执行了使其变为未就绪的操作。
另一方面,具有非负优先级的线程被归类为抢占式线程。一旦抢占式线程成为当前线程,如果一个协作式线程或具有更高或相等优先级的抢占式线程变为就绪状态,它可能随时被替换。
与抢占式线程相关的非负优先级数量可通过 Kconfig 符号 CONFIG_NUM_PREEMPT_PRIORITIES 进行配置,默认情况下等于 15。主线程的优先级为 0,而空闲线程的默认优先级为 15。
同样,与协作式线程相关的负优先级数量可通过 Kconfig 符号 CONFIG_NUM_COOP_PRIORITIES 进行配置,默认情况下等于 16。由于协作式线程用途有限,我们不在基础课程中介绍它们。
就像物理世界中的任何事物一样,CPU 时间是一种有限的资源,当一个应用程序具有多个并发逻辑时,不能保证有足够的 CPU 时间让所有逻辑同时运行。这就是调度器发挥作用的地方。调度器是 RTOS 的一部分,负责调度在任何给定时间运行哪些任务(即使用 CPU 时间)。它使用调度算法来确定下一个应该运行的任务。
可能运行的线程数量等于应用核心的数量。例如,在 nRF52840 上,有一个应用核心,允许一次运行一个线程。
众所周知,nRF Connect SDK 使用的 RTOS 是 Zephyr。Zephyr RTOS 默认是无滴答 (tickless) RTOS。无滴答 RTOS 完全由事件驱动,这意味着它不是通过周期性的定时器中断来唤醒调度器,而是基于称为重调度点的事件来唤醒。
重调度点是调度器被调用以选择下一个运行线程的时间点。每当就绪线程的状态发生变化时,都会触发重调度点。重调度点的一些示例如下:
k_yield() 时,线程的状态从“运行中”变为“就绪”。中断服务程序 (ISR) 由设备驱动程序和协议栈异步生成。它们是不受调度的。这包括回调函数,它们是 ISR 的应用程序扩展。重要的是要记住,ISR 会抢占当前线程的执行,从而以极低的开销进行响应。只有在所有 ISR 工作完成后,线程执行才会恢复。因此,务必确保 ISR(包括回调函数)不包含耗时的工作或涉及阻塞功能,因为它们会使所有其他线程处于饥饿状态。耗时或涉及阻塞的工作应使用工作项或其他适当机制移交给线程,正如我们将在练习 3 中看到的那样。