Threads
线程是 RTOS 调度程序(本主题后面将介绍)争夺 CPU 时间的最小逻辑执行单元。
在 nRF Connect SDK 中,主要有两种类型的线程:协作线程(优先级值为负)和可抢占线程(优先级非负)。协作线程的优先级为负,使用非常有限。因此,它们不在本课程的讨论范围内。
在任何给定时间,线程可以处于下列状态之一。
正在运行
正在运行的线程是当前正在由 CPU 执行的线程。这意味着 RTOS 的调度程序已将此线程选为应获取 CPU 时间的线程,并将此线程的上下文加载到 CPU 寄存器中。
可运行
当线程与其他线程或其他系统资源没有其他依赖关系以继续执行时,该线程被标记为“可运行”。此线程正在等待的唯一资源是 CPU 时间。调度程序将这些线程包含在调度算法中,该算法在当前正在运行的线程改变其状态后选择下一个要运行的线程。这也称为“就绪”状态。
不可运行
如果线程有一个或多个因素阻碍其执行,则视为未就绪,无法被选为当前线程。例如,这可能是因为它们正在等待某些尚不可用的资源,或者它们已被终止或暂停。调度程序不会将这些线程纳入调度算法中以选择下一个要运行的线程。这也称为“未就绪”状态。
System threads(系统线程)
系统线程是 Zephyr RTOS 在初始化期间自动生成的一种线程。默认情况下始终会生成两个线程,即主线程和空闲线程。
Main Thread(主线程)
主线程执行必要的 RTOS 初始化并调用应用程序main()函数(如果存在)。如果没有提供用户定义的 main(),主线程将正常退出,但系统仍可正常运行。
Idle Thread(空闲线程)
空闲线程在没有其他工作要做时运行,要么运行空循环,要么如果支持的话,将激活电源管理以节省电量(Nordic 设备就是这种情况)。
User-Create threads(用户创建的线程)
除了系统线程之外,用户还可以定义自己的线程来分配任务。例如,用户可以创建一个线程来委托读取传感器数据,另一个线程来处理数据,等等。线程被分配了优先级,它指示调度程序如何为线程分配 CPU 时间。我们将在练习 1 中深入介绍如何创建用户定义的线程。
Workqueue threads(工作队列线程)
nRF Connect SDK 中的另一个常见执行单元是工作项,它只不过是一个由称为工作队列线程的专用线程调用的用户定义函数。
工作队列线程是专门用于处理以“先进先出”方式从内核对象(称为工作队列)中拉出的工作项的线程。每个工作项都有一个指定的处理函数,调用该函数来处理工作项。它的主要用途是将非紧急工作从 ISR 或高优先级线程转移到低优先级线程。(这跟Linux的ISR的下半部分极其相似)
系统可以有多个工作队列线程,默认线程称为系统工作队列,可供任何应用程序或内核代码使用。处理系统工作队列中的工作项的线程是系统线程,如果将工作项提交到系统工作队列,则无需创建和初始化工作队列。
如上图所示,ISR 或高优先级线程将工作提交到工作队列中,专用工作队列线程按照先进先出 (FIFO) 的顺序取出工作项。从队列中取出工作项的线程在处理完一个工作项后总是让出,这样其他同等优先级的线程就不会长时间被阻塞。
将工作委托为工作项而不是专用线程的优点是,由于所有工作项都共享一个堆栈(工作队列堆栈),因此工作项比线程更轻,因为没有分配堆栈。
Threads Priority(线程优先级)
线程被分配一个整数值来表示其优先级,该值可以是负数也可以是非负数。数值越小,优先级越高,这意味着优先级为 4 的线程的优先级将高于优先级为 7 的线程。同样,优先级为 -2 的线程的优先级将高于优先级为 4 和优先级为 7 的线程。
调度程序根据线程的优先级将其分为两类:协作型线程和可抢占型线程。优先级为负的线程被归类为协作型线程。协作型线程一旦成为当前线程,就会一直保持当前状态,直到执行了使其处于未就绪状态的操作为止。
另一方面,具有非负优先级的线程被归类为可抢占线程。一旦可抢占线程成为当前线程,只要有协作线程或具有更高或相同优先级的可抢占线程准备就绪,它就可能随时被替换。
与可抢占线程相关的非负优先级的数量可通过 Kconfig 符号配置CONFIG_NUM_PREEMPT_PRIORITIES,默认情况下等于15。 主线程的优先级为 0,而空闲线程的优先级默认为 15。
类似地,与协作线程关联的 负优先级的数量可通过 Kconfig 符号进行配置CONFIG_NUM_COOP_PRIORITIES,默认情况下等于 16。由于协作线程的使用有限,因此我们在基础课程中不涉及协作线程。
Scheduler(调度器)
和物理世界中的任何事物一样,CPU 时间是一种有限的资源,当应用程序具有多个并发逻辑时,无法保证有足够的 CPU 时间让所有逻辑同时运行。这就是调度程序的作用所在。调度程序是 RTOS 的一部分,负责在任何给定时间调度正在运行的任务(即使用 CPU 时间)。它使用调度算法来确定下一个要运行的任务。
Rescheduling point(重新调度的点)
我们知道,nRF Connect SDK 使用的 RTOS 是 Zephyr。Zephyr RTOS 默认是无滴答 RTOS。无滴答 RTOS 完全由事件驱动,这意味着它不是通过定期的定时器中断来唤醒调度程序,而是基于称为重新调度点的事件来唤醒。
重新调度点是调度程序被调用来选择下一个要运行的线程的瞬间。每当 Ready 线程的状态发生变化时,就会触发重新调度点。以下是一些重新调度点的示例:
- 当一个线程调用时
k_yield(),该线程的状态从“运行”变为“就绪”。 - 通过提供/发送内核同步对象(比如信号量、互斥锁或警报)来解除线程阻塞,会导致线程的状态从“未就绪”更改为“就绪”。
- 当接收线程使用数据传递内核对象从其他线程获取新数据时,数据接收线程的状态将从“等待”更改为“就绪”。
- 当启用时间分片(练习 2 中介绍)且线程已经连续运行了允许的最大时间片时间时,线程的状态将从“运行”更改为“就绪”。
ISRs(## 中断服务子系统)
中断服务例程 ( ISR ) 由设备驱动程序和协议栈异步生成。它们没有调度。这包括回调函数,它们是 ISR 的应用程序扩展。重要的是要记住,ISR会抢占当前线程的执行,从而允许以非常低的开销进行响应。只有在所有 ISR 工作完成后,线程执行才会恢复。因此,重要的是确保 ISR(包括回调函数)不包含耗时的工作或涉及阻塞功能,因为它们会使所有其他线程都处于饥饿状态。耗时或涉及阻塞的工作应使用工作项或其他适当机制交给线程,正如我们将在练习 3 中看到的那样。