4、内核基础概念:任务(线程)、调度器、优先级、时间片、协作式与抢占式调度,以及线程状态转换

Zephyr RTOS 作为一个面向资源受限嵌入式系统的实时操作系统,其内核设计的核心目标是在确定性的时间内完成对计算资源(尤其是 CPU)的分配与回收。理解任务(线程)、调度器、优先级、时间片以及调度模式,是进行 Zephyr 开发的第一道门槛。本章将深入剖析这些基础概念,并结合 Zephyr 的具体实现,帮助开发者建立正确的并发编程心智模型,为后续的信号量、消息队列等同步机制打下坚实基础。

Zephyr 内核调度核心概念关系图 调度器 (Scheduler) 负责选择下一个运行线程 就绪队列 (Ready Queue) 优先级 0 (最高) 优先级 5 优先级 15 (最低) 线程状态 就绪 (Ready) 运行 (Running) 阻塞 (Blocked) 挂起 (Suspended) 获取最高优先级 上下文切换 抢占式调度 (Preemptive) 高优先级就绪 → 立即抢占 基于优先级 + 时间片 协作式调度 (Cooperative) 线程主动让出 CPU 无时间片,不抢占 Zephyr 使用基于优先级的调度算法,支持抢占式和协作式两种模式

4.1 任务(线程)的本质与内核对象

在 Zephyr 中,任务(Task)与线程(Thread)是同一个概念,内核文档中统一使用 k_thread 结构体表示。线程是 CPU 时间分配的基本单位,它包含独立的栈空间、线程控制块(TCB)以及调度所需的元数据。与 Linux 进程不同,Zephyr 的线程没有独立的地址空间(MPU 可选支持),所有线程共享同一个内存地址空间,这使得上下文切换开销极低(通常仅需几微秒)。

创建线程的核心 API 为 k_thread_create(),开发者需要显式提供线程栈、入口函数、优先级等参数。Zephyr 也提供了静态定义宏 K_THREAD_DEFINE(),在编译期就分配好资源,避免运行时动态内存分配的不确定性。

核心原理:线程的栈大小直接决定了其可调用的函数嵌套深度和局部变量大小。Zephyr 提供 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 时间。

调度器在以下时机触发重新调度:

  1. 线程主动调用 k_yield() 或 k_sleep()。
  2. 线程因等待信号量、消息队列等内核对象而阻塞。
  3. 中断服务程序(ISR)退出时,若唤醒了更高优先级的线程。
  4. 时间片到期(仅对抢占式同优先级线程)。

4.2.1 调度模式对比

特性抢占式调度协作式调度
触发方式高优先级线程就绪时立即切换当前线程主动让出(k_yield/阻塞)
时间片支持,默认 2.5ms(可配置)不支持,运行直到主动让出
适用场景实时性要求高的任务临界区保护、对延迟敏感但需原子性的操作
风险优先级反转、竞态条件线程饿死其他线程(若不让出)
实战建议:在 Zephyr 中,ISR 总是抢占所有线程(包括协作式线程)。因此,即使使用协作式调度,也必须保证 ISR 的执行时间尽可能短,否则会破坏系统的实时性。

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 时间,系统整体响应满足实时性要求。

风险提示:协作式线程的优先级高于所有抢占式线程时,必须确保其不会长时间占用 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 间切换,则可能存在优先级反转或时间片过小的问题。

总结复盘:Zephyr 的调度模型虽然简洁,但实际工程中需要结合具体硬件和业务场景灵活配置。核心要点包括:1) 协作式线程必须短小且主动让出;2) 抢占式线程的优先级设计要避免反转;3) 时间片是保证公平性的最后防线;4) 状态转换的监控是定位死锁和性能瓶颈的利器。建议在项目初期就建立线程状态监控机制,避免后期排查困难。