4、内核基础概念:任务(线程)、调度器、优先级、时间片、协作式与抢占式调度,以及线程状态转换
Zephyr RTOS 作为一个面向资源受限嵌入式系统的实时操作系统,其内核设计的核心目标是在确定性的时间内完成对计算资源(尤其是 CPU)的分配与回收。理解任务(线程)、调度器、优先级、时间片以及调度模式,是进行 Zephyr 开发的第一道门槛。本章将深入剖析这些基础概念,并结合 Zephyr 的具体实现,帮助开发者建立正确的并发编程心智模型,为后续的信号量、消息队列等同步机制打下坚实基础。
4.1 任务(线程)的本质与内核对象
在 Zephyr 中,任务(Task)与线程(Thread)是同一个概念,内核文档中统一使用 k_thread 结构体表示。线程是 CPU 时间分配的基本单位,它包含独立的栈空间、线程控制块(TCB)以及调度所需的元数据。与 Linux 进程不同,Zephyr 的线程没有独立的地址空间(MPU 可选支持),所有线程共享同一个内存地址空间,这使得上下文切换开销极低(通常仅需几微秒)。
创建线程的核心 API 为 k_thread_create(),开发者需要显式提供线程栈、入口函数、优先级等参数。Zephyr 也提供了静态定义宏 K_THREAD_DEFINE(),在编译期就分配好资源,避免运行时动态内存分配的不确定性。
CONFIG_ISR_STACK_SIZE 和 CONFIG_MAIN_STACK_SIZE 等配置项,但用户线程必须自行评估栈需求。栈溢出会导致内存踩踏,且难以排查,建议开启 CONFIG_THREAD_STACK_INFO 和 CONFIG_DEBUG_THREAD_INFO 进行监控。
4.1.1 线程属性与配置参数
| 参数 | 说明 | 典型值/注意事项 |
|---|---|---|
stack_size | 线程栈大小(字节) | 最小 512B,建议 1KB 起步;ISR 上下文额外占用 |
priority | 调度优先级 | 数值越小优先级越高;-CONFIG_NUM_COOP_PRIORITIES 到 CONFIG_NUM_PREEMPT_PRIORITIES-1 |
options | 线程选项标志 | K_ESSENTIAL、K_FP_REGS、K_USER 等 |
delay | 延迟启动时间 | K_NO_WAIT 立即就绪,或 K_MSEC(100) 延迟 |
Zephyr 将优先级划分为两个区域:协作式优先级(负数到 -CONFIG_NUM_COOP_PRIORITIES)和抢占式优先级(0 到 CONFIG_NUM_PREEMPT_PRIORITIES-1)。协作式线程不会被其他线程抢占(除非主动让出 CPU),而抢占式线程则可能被更高优先级的就绪线程打断。
4.2 调度器工作机制与优先级管理
Zephyr 的调度器采用基于优先级的就绪队列算法。内核维护一个位图(bitmap)来快速定位当前最高优先级的非空队列,时间复杂度为 O(1)。当多个线程具有相同优先级时,调度器采用时间片轮转(Round-Robin)策略,确保每个线程都能获得 CPU 时间。
调度器在以下时机触发重新调度:
- 线程主动调用
k_yield()或k_sleep()。 - 线程因等待信号量、消息队列等内核对象而阻塞。
- 中断服务程序(ISR)退出时,若唤醒了更高优先级的线程。
- 时间片到期(仅对抢占式同优先级线程)。
4.2.1 调度模式对比
| 特性 | 抢占式调度 | 协作式调度 |
|---|---|---|
| 触发方式 | 高优先级线程就绪时立即切换 | 当前线程主动让出(k_yield/阻塞) |
| 时间片 | 支持,默认 2.5ms(可配置) | 不支持,运行直到主动让出 |
| 适用场景 | 实时性要求高的任务 | 临界区保护、对延迟敏感但需原子性的操作 |
| 风险 | 优先级反转、竞态条件 | 线程饿死其他线程(若不让出) |
4.3 时间片与协作式/抢占式调度实战
时间片(Time Slice)是抢占式调度中防止同优先级线程饿死的关键机制。Zephyr 通过 CONFIG_TIMESLICE_SIZE 配置全局时间片大小,也可通过 k_thread_time_slice_set() 为单个线程设置。当线程的时间片耗尽时,调度器将其移至队尾,并选择下一个同优先级线程运行。
协作式调度则完全依赖线程的自觉性。一个协作式线程若陷入死循环且不调用 k_yield(),将永久占用 CPU,导致系统假死。因此,协作式线程必须设计为短小精悍,或者定期让出 CPU。
4.3.1 案例拆解:混合调度模式下的传感器数据采集
场景:一个 IoT 设备需要以 100Hz 频率采集温度传感器数据,并通过 UART 发送。同时,有一个低优先级的 LED 闪烁任务。
初始设计:
// 传感器采集线程 - 高优先级抢占式
K_THREAD_DEFINE(sensor_thread, 1024, sensor_entry, NULL, NULL, NULL, 5, 0, 0);
// LED 闪烁线程 - 低优先级协作式
K_THREAD_DEFINE(led_thread, 512, led_entry, NULL, NULL, NULL, -2, 0, 0);
问题分析:传感器线程优先级为 5(抢占式),LED 线程优先级为 -2(协作式)。由于协作式优先级数值更小(更高),LED 线程一旦运行,传感器线程无法抢占它。若 LED 线程在 led_entry() 中执行 k_sleep(K_MSEC(500)),则传感器线程最多延迟 500ms,完全无法满足 100Hz 采样要求。
解决方案:将 LED 线程改为抢占式优先级(如 10),并设置时间片。这样传感器线程(优先级 5)可以随时抢占 LED 线程。
K_THREAD_DEFINE(led_thread, 512, led_entry, NULL, NULL, NULL, 10, 0, 0);
// 在 led_entry 中设置时间片为 10ms
k_thread_time_slice_set(&led_thread, K_MSEC(10), NULL);
结果对比:修改后,传感器线程的采样间隔稳定在 10ms(100Hz),LED 线程在传感器空闲时获得 CPU 时间,系统整体响应满足实时性要求。
while(1) 无让出循环,或使用 k_busy_wait() 替代 k_sleep() 时格外小心。
4.4 线程状态转换详解
Zephyr 线程在其生命周期内经历多种状态,理解状态转换是调试并发问题的前提。主要状态包括:就绪(Ready)、运行(Running)、阻塞(Blocked)、挂起(Suspended)以及终止(Terminated)。
4.4.1 状态转换路径
- 就绪 → 运行:调度器选择该线程,发生上下文切换。
- 运行 → 就绪:时间片耗尽、被更高优先级抢占、或主动
k_yield()。 - 运行 → 阻塞:等待信号量、消息队列、或调用
k_sleep()。 - 阻塞 → 就绪:等待的条件满足(如信号量被释放),线程被唤醒。
- 运行 → 挂起:调用
k_thread_suspend(),线程不再参与调度。 - 挂起 → 就绪:调用
k_thread_resume()。
Zephyr 还引入了延迟(Delayed)状态,即线程调用了 k_sleep() 或 k_msleep(),此时线程被放入定时器队列,直到超时后才转为就绪。
4.4.2 状态转换的调试技巧
使用 CONFIG_THREAD_MONITOR 和 CONFIG_THREAD_NAME 可以在 Shell 中查看线程状态。通过 kernel threads 命令,可以列出所有线程的优先级、状态、栈使用率等信息。例如:
uart:~$ kernel threads
Threads:
0x20000068 sensor_thread READY prio:5 stack: 1024 (80% used)
0x20000118 led_thread SUSPEND prio:10 stack: 512 (45% used)
0x200001c8 idle_thread READY prio:15 stack: 256 (10% used)
若发现线程长期处于 BLOCKED 状态,应检查其等待的内核对象是否被正确释放;若线程频繁在 READY 和 RUNNING 间切换,则可能存在优先级反转或时间片过小的问题。