4. 内核基础概念:线程、优先级、时间片与状态机
好,咱们今天聊点实在的。线程、优先级、时间片、状态机、线程栈、TCB——这几个词,说白了就是 RTOS 的骨架。你把这套东西吃透了,Zephyr 的内核调度在你眼里就跟透明的一样。
我记得刚接触 RTOS 那会儿,总觉得“线程”不就是个函数嘛,跑起来就行了。结果第一次在产品里遇到优先级反转,系统直接卡死,我才意识到——嗯,这些东西真不能想当然。
4.1 线程(Thread)到底是什么?
在 Zephyr 里,线程就是一条独立的执行流。每个线程有自己的栈、自己的优先级、自己的状态。你可以把它理解成一个“轻量级任务”——它和别的线程共享 CPU,但各自保存现场。
我个人习惯把线程分成三类:
- 主线程:系统启动后自动创建的,跑你的
main()函数 - 用户线程:你通过
k_thread_create()或K_THREAD_DEFINE创建的 - 系统线程:内核内部用的,比如空闲线程(idle thread)
关键点:Zephyr 的线程在创建时就已经分配好了栈空间和 TCB,不是在运行时动态分配的。这一点和 Linux 的 pthread 完全不同,别搞混了。
4.2 优先级——谁先跑谁后跑
优先级决定了调度器该先让谁上 CPU。Zephyr 用的是数值越小优先级越高的规则——0 是最高优先级。
| 优先级范围 | 说明 | 典型用途 |
|---|---|---|
| 0 ~ (CONFIG_NUM_COOP_PRIORITIES - 1) | 协作式优先级(Cooperative) | 关键控制逻辑,不能被抢占 |
| CONFIG_NUM_COOP_PRIORITIES ~ (CONFIG_NUM_PRIORITIES - 1) | 抢占式优先级(Preemptive) | 普通任务、通信协议栈 |
这里有个坑——我曾经在一个项目中把两个线程设成相同优先级,结果发现它们轮流跑,但其中一个总是“饿死”。为什么?因为时间片没开。后面咱们会聊到。
避坑指南:我曾经在协作式线程里写了一个死循环,结果整个系统都停了。协作式线程不会主动让出 CPU,你必须手动调用 k_yield() 或者让它阻塞。否则,低优先级线程永远没机会跑。
4.3 时间片——公平轮转的秘诀
时间片(Time Slice)是给同优先级线程用的。当多个线程优先级相同时,调度器让每个线程跑一个固定时长,然后切换到下一个。这个固定时长就是时间片。
Zephyr 里时间片是全局配置的,单位是系统 tick。你可以在 prj.conf 里设置:
CONFIG_TIMESLICING=y
CONFIG_TIMESLICE_SIZE=5 # 每个线程跑 5 个 tick
CONFIG_TIMESLICE_PRIORITY=0 # 只对优先级大于等于 0 的线程生效
我个人建议:时间片别设太小,否则上下文切换开销会吃掉你的 CPU。5~10 个 tick 是个不错的起点。
4.4 线程状态机——线程的“人生”
每个线程在它的生命周期里会经历几个状态。Zephyr 的线程状态机比一些 RTOS 要简洁,但核心逻辑是一样的。
你看这张图,线程的状态流转其实很直观。我刚开始学的时候,总以为“阻塞”和“挂起”是一回事——其实不是。阻塞是因为资源没到位,被动等待;挂起是主动暂停,谁叫醒谁恢复。
4.5 线程栈——每个线程的“私人空间”
每个线程都有自己的栈,用来存局部变量、函数调用链、中断上下文。Zephyr 的线程栈是在编译时就分配好的,不是 malloc 出来的。
定义线程栈有两种方式:
// 方式一:静态定义(推荐)
K_THREAD_STACK_DEFINE(my_stack, 1024);
// 方式二:动态分配(不常用)
k_thread_stack_t *my_stack = k_thread_stack_alloc(1024, 0);
栈大小怎么定?我有个经验法则:先给个 2048 字节,跑起来后用 k_thread_stack_analyze() 看实际用了多少,再调整。别一开始就给 64 字节——我曾经这么干过,结果栈溢出,数据被悄悄覆盖,查了两天才找到原因。
小技巧:Zephyr 支持栈溢出检测(CONFIG_STACK_SENTINEL=y)。开启后,内核会在栈底放一个“哨兵值”,一旦被覆盖就触发异常。调试阶段一定要开这个。
4.6 线程控制块(TCB)——线程的“身份证”
每个线程在内核里都有一个对应的 TCB,Zephyr 里叫 struct k_thread。它记录了线程的所有信息:
| 字段 | 说明 | 我常用的场景 |
|---|---|---|
base |
线程基础属性(优先级、状态标志) | 调试时查看线程状态 |
callee_saved |
上下文切换时保存的寄存器 | 理解切换开销 |
stack_info |
栈起始地址和大小 | 检查栈使用情况 |
thread_group |
线程组归属 | 批量操作线程 |
join_queue |
等待该线程结束的其他线程 | 实现线程同步 |
TCB 是内核调度器的“操作手册”。调度器切换线程时,就是先保存当前线程的上下文到它的 TCB,再从目标线程的 TCB 里恢复上下文。说白了,TCB 就是线程的“快照”。
深入理解:Zephyr 的 TCB 是静态分配的,放在全局数据区。这意味着你创建的线程数量受限于编译时的配置(CONFIG_MAX_THREAD_BYTES)。别想着动态创建几千个线程——嵌入式系统不是服务器。
4.7 把它们串起来——一个完整的线程示例
说了这么多,咱们看个实际例子。下面这段代码创建了两个线程,一个高优先级跑控制逻辑,一个低优先级做日志输出:
#include <zephyr/kernel.h>
/* 定义线程栈 */
K_THREAD_STACK_DEFINE(ctrl_stack, 2048);
K_THREAD_STACK_DEFINE(log_stack, 1024);
/* 定义 TCB */
struct k_thread ctrl_thread;
struct k_thread log_thread;
/* 线程函数 */
void ctrl_entry(void *arg1, void *arg2, void *arg3)
{
while (1) {
/* 控制逻辑 */
k_sleep(K_MSEC(10)); /* 主动让出 CPU */
}
}
void log_entry(void *arg1, void *arg2, void *arg3)
{
while (1) {
/* 日志输出 */
k_sleep(K_MSEC(100));
}
}
void main(void)
{
/* 创建线程 */
k_thread_create(&ctrl_thread, ctrl_stack,
K_THREAD_STACK_SIZEOF(ctrl_stack),
ctrl_entry, NULL, NULL, NULL,
5, 0, K_NO_WAIT);
k_thread_create(&log_thread, log_stack,
K_THREAD_STACK_SIZEOF(log_stack),
log_entry, NULL, NULL, NULL,
10, 0, K_NO_WAIT);
/* 主线程也做点事 */
while (1) {
k_sleep(K_SECONDS(1));
}
}
你看,控制线程优先级是 5,日志线程是 10。数值越小优先级越高,所以控制线程会优先跑。日志线程只有在控制线程阻塞时才有机会上 CPU。
嗯,这里要注意:k_thread_create() 的最后一个参数 K_NO_WAIT 表示创建后立即加入就绪队列。如果你传 K_FOREVER,线程会先挂起,等你手动 k_thread_start() 才跑。
好了,这一章的内容就到这儿。线程、优先级、时间片、状态机、栈、TCB——这六个概念是 Zephyr 内核调度的基石。你把这些搞清楚了,后面讲调度算法、同步机制的时候,就会觉得顺理成章。